[Android]微信h5支付的正確打開方式

對於微信支付,一般原生APP可能接入微信支付sdk的場景比較多,微信h5支付的使用場景是在WebView上。咱們接入了一個第三方的業務,就是經過WebView去承載的,走的h5支付,結果發現支付流程存在一些問題,包括咱們公司其餘的app在以前接入過微信h5支付,體驗也是很差。php

前菜

在說問題以前先簡單上幾個前菜開胃,後面會用到java

微信h5支付官方Wiki

pay.weixin.qq.com/wiki/doc/ap…android

微信h5支付流程中url的「走向」

  1. 發起微信支付:wx.tenpay.com/cgi-bin/mmp…
  2. 重定向到:weixin://wap/pay?prepayid=[省略]&package=[省略]&noncestr=[省略]&sign=[省略]
  3. 經過微信的scheme(weixin)喚起微信支付頁
  4. 等待支付完成/取消支付/5秒超時,回跳redirect_url

關於WebView的history

在原WebView上加載,Webview會存頁面訪問記錄:web

  • 在瀏覽器器上很明顯的行爲就是支持前進後退
  • 在h5上能夠調用window.history,這個對象就是記錄了頁面堆棧
  • 在Android WebView上能夠調用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

經過重寫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支付過程當中遇到的幾個問題,並逐一分析,給出解決方案:

  1. Webview加載微信h5支付url,要帶上referer;
  2. 支付完成/取消支付/5秒超時,回跳redirect_url,在原webview頁面打開此頁面;
  3. 按返回鍵,回退Webview的history,處理掉空白頁。

這就是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);
    }
}

複製代碼
相關文章
相關標籤/搜索