阅读 1974

[Android]微信h5支付的正确打开方式

对于微信支付,通常原生APP可能接入微信支付sdk的场景比较多,微信h5支付的使用场景是在WebView上。我们接入了一个第三方的业务,就是通过WebView去承载的,走的h5支付,结果发现支付流程存在一些问题,包括我们公司其他的app在之前接入过微信h5支付,体验也是不好。

前菜

在说问题之前先简单上几个前菜开胃,后面会用到

微信h5支付官方Wiki

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

微信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会存页面访问记录:

  • 在浏览器器上很明显的行为就是支持前进后退
  • 在h5上可以调用window.history,这个对象就是记录了页面堆栈
  • 在Android WebView上可以调用WebBackForwardList history = webView.copyBackForwardList(),同样也是记录了页面堆栈

当history的size>1,我们按返回键应当是先返回history,当size==1,直接返回页面Activity

@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,有两种方式

  • 主动loadUrl

    webView.loadUrl(url); //这就是自定义处理
    return true;
    复制代码

    但是这种方式会丢失掉请求的header,除非手动加上

    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);
    }
}

复制代码