對於微信支付,一般原生APP可能接入微信支付sdk的場景比較多,微信h5支付的使用場景是在WebView上。咱們接入了一個第三方的業務,就是經過WebView去承載的,走的h5支付,結果發現支付流程存在一些問題,包括咱們公司其餘的app在以前接入過微信h5支付,體驗也是很差。php
在說問題以前先簡單上幾個前菜開胃,後面會用到java
pay.weixin.qq.com/wiki/doc/ap…android
在原WebView上加載,Webview會存頁面訪問記錄:web
window.history
,這個對象就是記錄了頁面堆棧WebBackForwardList history = webView.copyBackForwardList()
,一樣也是記錄了頁面堆棧當history的size>1,咱們按返回鍵應當是先返回history,當size==1,直接返回頁面Activityapi
@Override
public void onBackPressed() {
// webView.canGoBack()等同於webView.canGoBackOrForward(-1)
// webView.goBack()等同於webView.goBackOrForward(-1)
if (webView.canGoBack()) {
webView.goBack();
return;
}
super.onBackPressed();
}
複製代碼
經過重寫WebViewClient.shouldOverrideUrlLoading(WebView webView, String url)
,去攔截url瀏覽器
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
if (...) {
...
// true: 自定義處理
return true;
}
if (...) {
...
// false: 交給瀏覽器處理
return false;
}
// 默認處理,其實等同於return false
return super.shouldOverrideUrlLoading(webView, url);
}
複製代碼
在shouldOverrideUrlLoading中處理webview在原頁面加載url,有兩種方式微信
主動loadUrlapp
webView.loadUrl(url); //這就是自定義處理
return true;
複製代碼
可是這種方式會丟失掉請求的header,除非手動加上ide
Map<String, String> headers = new HashMap<>();
headers.put("referer", "商戶申請H5時提交的受權域名");
...
webView.loadUrl(url, headers);
return true;
複製代碼
不作處理處理,直接return false;
推薦這種方式,不影響請求頭參數,徹底是瀏覽器行爲,很穩!,咱們下文就會用這種方式完美地把微信h5支付所必須的referer頭傳回去微信支付
支付報錯:商家參數格式有誤,請聯繫商家解決,緣由是referer丟失,代碼實現上就是上文說的由於主動loadUrl,丟失了headers
保證調用微信h5支付的url在原WebView上加載,但咱們不主動loadUrl
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
...
if (isWXH5Pay(url)) {
try {
Uri uri = Uri.parse(url);
// 這裏要先解析出redirect_url,後面要用到
redirectUrl = uri.getQueryParameter("redirect_url");
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
...
return super.shouldOverrideUrlLoading(webView, url);
}
複製代碼
附上isWXH5Pay(url)
的實現
/** * 是不是微信h5支付的連接 */
public static boolean isWXH5Pay(String url) {
if (TextUtils.isEmpty(url)) {
return false;
}
return url.toLowerCase().startsWith("https://wx.tenpay.com");
}
複製代碼
產品跑過來講,xxx app(以前接入過微信h5支付)上是能夠正常打開的,yyy app上是有問題,我都還麼點支付,就彈出了支付完成頁。
結果我查了下微信h5支付的官方文檔,關於redirect_url:
因爲設置redirect_url後,回跳指定頁面的操做可能發生在:1,微信支付中間頁調起微信收銀臺後超過5秒 2,用戶點擊「取消支付「或支付完成後點「完成」按鈕。所以沒法保證頁面回跳時,支付流程已結束,因此商戶設置的redirect_url地址不能自動執行查單操做,應讓用戶去點擊按鈕觸發查單操做。
這就能夠解釋爲何用戶沒點支付,都會彈支付完成頁,因此回跳支付完成頁是正常的,但蓋在微信支付頁上面是不正常的,由於用戶沒法繼續進行支付操做,除非手速夠快,在5s內完成支付。
在xxx app上正常,是由於收到redirect_url請求後,在原WebView上加載,即:
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
...
if (isRedirectUrl(url)) {
//這裏url就是微信回傳的redirect_url
webView.loadUrl(url);
return true;
}
...
return super.shouldOverrideUrlLoading(webView, url);
}
複製代碼
yyy app不行是咱們的url攔截協議默認就是新開WebView加載url,沒有特殊處理redirect_url的加載,Android這裏就是新開了一個Activity,因此蓋在微信支付頁上面。
保證redirect_url在原WebView上加載,交給瀏覽器處理
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
...
if (isRedirectUrl(url)) {
return false;
}
...
return super.shouldOverrideUrlLoading(webView, url);
}
複製代碼
附上isRedirectUrl(url)
的實現
/** * 是不是微信h5支付的回跳url * {@link #redirectUrl}是load的時候從<a href="https://wx.tenpay.com/xxx?redirect_url=xxx">https://wx.tenpay.com/xxx?redirect_url=xxx<a/>的參數中解析出來了<br/> * 這裏直接equals * * @param url * @return */
public boolean isRedirectUrl(String url) {
if (TextUtils.isEmpty(url)) {
return false;
}
return url.equalsIgnoreCase(redirectUrl);
}
複製代碼
xxx app是正常的?我體驗了下,看似正常,但我也發現一個問題,在支付完成頁按返回的時候,返回的是一個空白頁,這不太好吧。並且接下來又會彈出微信支付頁,這就嚴重了。給用戶的感受就是很流氓!
問題來了,這個空白頁是怎麼產生的呢?
上文中**微信h5支付流程中url的「走向」**中,在發起微信h5支付時,wx.tenpay.com/xxx重定向到weix… scheme
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
...
if (!URLUtil.isNetworkUrl(url)) {
// 特殊 Scheme 處理,調用外部應用打開
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
webView.getContext().startActivity(intent);
} catch (Exception e) {
// 可能時沒有安裝微信app
e.printStackTrace();
}
return true;
}
...
return super.shouldOverrideUrlLoading(webView, url);
}
複製代碼
若是你有安裝微信app,正常就能跳轉到微信支付頁。
wx.tenpay.com/xxx這個頁面它就是個空白頁,但它有處理邏輯,好比,負責回跳redirect_url,我本來嘗試在用Intent.ACTION_VIEW打開weixin://xxx的同時,經過webView.goBack();
回退掉這個空白頁,可是發現沒法收到redirect_url了,因此在整一個支付流程過程當中,不能去幹掉這個空白頁
重寫Activity的onBackPressed()
方法,在回退WebView的history過程當中,跳過這個空白頁
@Override
public boolean onBackPressed() {
// back history
int index = -1; // -1表示回退history上一頁
String url;
WebBackForwardList history = mWebView.copyBackForwardList();
while (mWebView.canGoBackOrForward(index)) {
url = history.getItemAtIndex(history.getCurrentIndex() + index).getUrl();
if (URLUtil.isNetworkUrl(url) && !WXH5PayHandler.isWXH5Pay(url)) {
mWebView.goBackOrForward(index);
return;
}
index--;
}
super.onBackPressed();
}
複製代碼
列舉了咱們在接入微信h5支付過程當中遇到的幾個問題,並逐一分析,給出解決方案:
這就是Android WebView 在接入微信h5支付的正確打開方式,主要要求咱們對文章開頭提到的幾個「前菜」能好好消化,才能以更「正確」的方式品味微信h5支付這頓大餐。
封裝在WXH5PayHandler類中
/** * 微信h5支付處理類 * <p> * <a href="https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_4">微信h5支付Wiki<a/><br/> */
public class WXH5PayHandler {
public static final String REDIRECT_URL = "redirect_url";
/** * 發起h5支付的url */
private String h5Url;
/** * 喚起微信app支付頁的scheme協議url */
private String launchUrl;
/** * 回跳頁面url<br/> * 如,您但願用戶支付完成後跳轉至https://xxx<br/> * 看下官方文檔怎麼說: <br/> * 因爲設置redirect_url後,回跳指定頁面的操做可能發生在:1,微信支付中間頁調起微信收銀臺後超過5秒 2,用戶點擊「取消支付「或支付完成後點「完成」按鈕。所以沒法保證頁面回跳時,支付流程已結束,因此商戶設置的redirect_url地址不能自動執行查單操做,應讓用戶去點擊按鈕觸發查單操做。 */
private String redirectUrl;
/*-------------------- 步驟1:拿到h5支付連接,並在原WebView頁面打開 --------------------*/
/** * 是不是微信h5支付的連接 * * @param url * @return */
public static boolean isWXH5Pay(String url) {
if (TextUtils.isEmpty(url)) {
return false;
}
return url.toLowerCase().startsWith("https://wx.tenpay.com");
}
/** * 方案1: 推薦,直接return false, 調用{@link android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView, String)}默認處理 * <p> * 調用前請先調用{@link #isWXH5Pay(String)}判斷是不是微信h5支付 * * @param url * @return */
public boolean pay(String url) {
h5Url = url;
redirectUrl = getRedirectUrl(url);
return false;
}
/** * 方案2: 不推薦, 調用{@link WebView#loadUrl(String)}, 同時return true.<br/> * 但這樣會丟失掉{@param url}的請求頭參數, 如必需的referer, 這個時候要求調用{@link WebView#loadUrl(String, Map)} * <p> * 調用前請先調用{@link #isWXH5Pay(String)}判斷是不是微信h5支付 * * @param webView * @param url * @param headers 自定義的header, 其中必須包含微信H5支付所必需的referer * @return */
public boolean pay(WebView webView, String url, Map<String, String> headers) {
h5Url = url;
redirectUrl = getRedirectUrl(url);
webView.loadUrl(url, headers);
return true;
}
private String getRedirectUrl(String url) {
try {
Uri uri = Uri.parse(url);
return uri.getQueryParameter(REDIRECT_URL);
} catch (Exception e) {
return null;
}
}
/*-------------------- 步驟2:拿到喚起微信的scheme連接,並喚起微信app的支付頁 --------------------*/
/** * 是否將要喚起微信h5支付頁面 * * @param url 微信的scheme(weixin)開頭的url: weixin://wap/pay?xxx * @return */
public boolean isWXLaunchUrl(String url) {
if (TextUtils.isEmpty(url)) {
return false;
}
return url.toLowerCase().startsWith("weixin://");
}
/** * 調用{@link #h5Url}後會重定向到微信的scheme url去喚起微信app的h5支付頁面 * 調用前請先調用{@link #isWXLaunchUrl(String)}判斷是不是微信的scheme url * * @param url * @return */
public boolean launchWX(WebView webView, String url) {
launchUrl = url;
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
webView.getContext().startActivity(intent);
return true;
} catch (Exception e) {
// catch掉的話就內部打開
return false;
}
}
/*-------------------- 步驟3:等待微信回跳redirect_url, 並在原WebView頁面打開 --------------------*/
/** * 是不是微信h5支付的回跳url<br/> * 調用{@link #pay(String)}的時候從<a href="https://wx.tenpay.com/xxx?redirect_url=xxx">https://wx.tenpay.com/xxx?redirect_url=xxx<a/>的參數中解析出來了<br/> * 這裏直接equals * * @param url * @return */
public boolean isRedirectUrl(String url) {
if (TextUtils.isEmpty(url)) {
return false;
}
return url.equalsIgnoreCase(redirectUrl);
}
/** * 回跳頁面url, 在{@link android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView, String)}中調用 * * @see #redirectUrl */
public boolean redirect() {
// 原頁面打開
return false;
}
}
複製代碼
重寫WebViewClient.shouldOverrideUrlLoading(WebView webView, String url)
,調用WXH5PayHandler
public class XWebViewClient extends WebViewClient {
private WXH5PayHandler mWXH5PayHandler;
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (TextUtils.isEmpty(url)) {
return true;
}
Uri uri = null;
try {
uri = Uri.parse(url);
} catch (Exception e) {
e.printStackTrace();
}
if (uri == null) {
return true;
}
if (!URLUtil.isNetworkUrl(url)) {
// 處理微信h5支付2
if (mWXH5PayHandler != null && mWXH5PayHandler.isWXLaunchUrl(url)) {
mWXH5PayHandler.launchWX(view, url);
} else {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
view.getContext().startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
if (WXH5PayHandler.isWXH5Pay(url)) {
// 處理微信h5支付1
mWXH5PayHandler = new WXH5PayHandler();
return mWXH5PayHandler.pay(url);
} else if (mWXH5PayHandler != null) {
// 處理微信h5支付3
if (mWXH5PayHandler.isRedirectUrl(url)) {
boolean result = mWXH5PayHandler.redirect();
mWXH5PayHandler = null;
return result;
}
mWXH5PayHandler = null;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
複製代碼