对于微信支付,一般原生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);
}
}
复制代码