对于微信支付,通常原生APP可能接入微信支付sdk的场景比较多,微信h5支付的使用场景是在WebView上。我们接入了一个第三方的业务,就是通过WebView去承载的,走的h5支付,结果发现支付流程存在一些问题,包括我们公司其他的app在之前接入过微信h5支付,体验也是不好。
前菜
在说问题之前先简单上几个前菜开胃,后面会用到
微信h5支付官方Wiki
pay.weixin.qq.com/wiki/doc/ap…
微信h5支付流程中url的“走向”
- 发起微信支付:wx.tenpay.com/cgi-bin/mmp…
- 重定向到:weixin://wap/pay?prepayid=[省略]&package=[省略]&noncestr=[省略]&sign=[省略]
- 通过微信的scheme(weixin)唤起微信支付页
- 等待支付完成/取消支付/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支付过程中遇到的几个问题,并逐一分析,给出解决方案:
- Webview加载微信h5支付url,要带上referer;
- 支付完成/取消支付/5秒超时,回跳redirect_url,在原webview页面打开此页面;
- 按返回键,回退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);
}
}