标题如此拗口, 我也是无可奈何🤣
本文会涉及到两个方面:
- H5 支付时调起微信或支付宝 App;
- 调起微信或支付宝 App 完成支付操作后,返回到自己的原来的 App。
公司业务需求,需客户端嵌套一个完整的 H5 开发的网页,其中带有 H5 的微信支付和支付宝支付。微信支付一直无法打开页面,无法支付;支付宝支付可以打开支付宝的网页,如下图
最初讨论的解决办法是:走到支付时,通过 js 桥来调起原生支付。如果 H5 页面是同一公司同事开发的,这倒是个简单快捷的方法。但是结合公司情况,考虑到后期可能会接入其他公司的 H5 页面,联调起来会很麻烦,所以还是决定不通过桥解决。
1. H5 支付时调起微信或支付宝 App
H5 支付调起微信或支付宝 App 的原理都一样,以 WK 为例,都是在 decidePolicyForNavigationAction
代理方法里面拦截 URL,再用 [[UIApplication sharedApplication] openURL:navigationAction.request.URL];
调起。
不同的只是拦截的字段有区分,微信需拦截的字符串为 @"weixin://wap/pay"
, 支付宝拦截的字符串为 @"alipay://alipayclient"
。简单代码如下:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// 在发送请求之前,决定是否跳转
NSString *url = navigationAction.request.URL.absoluteString;
if ([url containsString:@"weixin://wap/pay"] || [url containsString:@"alipay://alipayclient"]) {
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
至此,H5 支付可以成功调起微信或支付宝的 App 进行支付了。
支付宝还有另一种调起的方式, 在支付宝的开发文档中有提到, 这里也贴一下地址, 有兴趣的可以去试下: 支付宝手机网站支付转App支付
但是,如果代码写到这里就完的话,可能会出现两种情况:
- 使用微信支付, 操作完成, 仍停留在微信, 不会像原生调起支付那样返回自己的APP;
- 支付宝支持,操作完后,调起了 Safari,打开的网页就是之前在 WK 里打开的页面
ps:查资料时,有网友微信支付完成时,也会调起 Safari,但我调试过程中未出现这种情况
接下来解决第二个问题,完成支付操作后,返回自己的 App
2. 调起微信或支付宝 App 完成支付操作后,返回到自己的 App
老规矩,先贴上参考的链接
微信返回参考 https://www.jianshu.com/p/90db7dfb075c
支付宝返回参考 https://www.jianshu.com/p/0d8dd04fe94e
以上两篇文章里, 非常详细的描述了解决的整个过程, 包括解决过程中遇到哪些问题, 从哪些方面思考得到灵感, 最终如何一步步找到解决办法, 非常建议去看看, 了解下. 这里就不照搬了, 直接讲解决步骤了...
这部分内容得再拆分成两部分, 一个支付宝的, 一个微信的
支付宝支付返回到 App
- 拦截到支付宝支付的 URL (就是 URL 里包含 @"alipay://alipayclient") 时, 对 URL 进行 URL 解码;
解码后的 URL 如下:
alipay://alipayclient/?{"requestType":"SafePay","fromAppUrlScheme":"alipays","dataString":"XXX"}
- 解码后得到一字符串, 字符串里包含了一个json 串. 把 json 部分截取出来, 再转成 dictionary, dictionary 里面将会有一个 key 为 fromAppUrlScheme 的键值对, 把 fromAppUrlScheme 的值改成自己 App 的 scheme;
- 把第二步得到 dictionary 再转成 json, 再对已经改了 fromAppUrlScheme 值的 json 进行 URL 编码;
- 把第三步编码好的字符串, 替换掉第一步拦截的 URL 的 json 部分... 注意!!! 这里替换的只是 URL 的 json 部分!!! 只是 URL 的 json 部分!!! 替换后得到一个新的的 URL;
- 拿第四步得到的带有自己 APP 的 scheme 的 URL, 去调起支付宝 App(就是文章第一部分说的调起 App 那样的调起)
需要注意的一个地方是, 进行 URL 编码时, 不是 URL 整体进行编码, 只有 json 那部分需要编码, 如果对完整的 URL 进行编码, 那么我们用来识别支付宝的字符串的那部分 (@"alipay://alipayclient") 也会被编码, 从而导致无法调起支付宝
基本实现如下:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// 在发送请求之前,决定是否跳转
NSString *url = navigationAction.request.URL.absoluteString;
// 支付宝
if ([url containsString:@"alipay://alipayclient"]) {
NSMutableString *param = [NSMutableString stringWithFormat:@"%@", (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)url, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding))];
NSRange range = [param rangeOfString:@"{"];
// 截取 json 部分
NSString *param1 = [param substringFromIndex:range.location];
if ([param1 rangeOfString:@"\"fromAppUrlScheme\":"].length > 0) {
id json = jsonToClass(param1); // 这里为伪代码, 自行转成 dictionary
if (![json isKindOfClass:[NSDictionary class]]) {
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
NSMutableDictionary *dicM = [NSMutableDictionary dictionaryWithDictionary:json];
dicM[@"fromAppUrlScheme"] = 自己App的scheme;
NSString *jsonStr = classToJson(dicM); // 这里为伪代码, 自行转成json
NSString *encodedString = (NSString*) CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)jsonStr, NULL, (CFStringRef)@"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8));
// 只替换 json 部分
[param replaceCharactersInRange:NSMakeRange(range.location, param.length - range.location) withString:encodedString];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:param]];
}
decisionHandler(WKNavigationActionPolicyCancel);
}
decisionHandler(WKNavigationActionPolicyAllow);
}
微信支付返回到 App
微信的问题比支付宝的稍微麻烦些. 首先麻烦的就是配置问题, 需要在微信开发者平台上进行了相应的配置. 如果配置不对, 请参照 微信支付开发步骤&常见问题.
提醒下, 微信开发者平台上的配置的有次数限制的, 每个月只能修改多少多少次, 所以配置时尽量把能想到的需要用到的都一起配置了, 省得开发的时候被这些细节问题浪费时间
- 拦截到微信支付的 URL
@"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?"
. (这里需要拦截的 URL 与调起微信的 URL 不是同一个) - H5 调起微信支付时, 需要设置 Referer 请求头, 所以直接拿请求头
newRequest.allHTTPHeaderFields = navigationAction.request.allHTTPHeaderFields
; - 给 Referer 赋值
[newRequest setValue:@"www.xxx.com://" forHTTPHeaderField: @"Referer"];
, 自己的 App 也要设置一个www.xxx.com
的 scheme. 并且取消此次加载.
解释下
www.xxx.com
, 其实就是公司的一个域名, 可以是 H5 支付的域名, 也可以是公司其他域名, 但必须确保这个域名存在于公司的微信开发者平台的配置中.
那么问题来了, 这既然是公司的一个域名, 又要把这个域名设置成 scheme, 那有可能出现这么一个问题: 同公司的其他 App 也可能配置了同样的 scheme. 所以, 这里的域名和 scheme 要保证唯一性. 至于怎么保证, 跟后台哥们商量下吧. 记得配置到微信开发者平台上!!!
- 重新加载修改了 Referer 的请求.
- 拦截包含
@"weixin://wap/pay"
的 URL, 调起微信.
针对微信这部分, 划几个重点:
- scheme 必须唯一, 不唯一的话随机打开一个 (公司有个 App 不知在什么情况下配了个跟某支付一毛一样的 scheme, 导致装有那个 App 的用户都不能用某支付来支付, 后来被发律师函了... )
@"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?"
会拦截两次. 拦截第一次时, 需要修改 Referer, 取消此次加载, 重新加载修改了请求头的请求; 虽然请求头修改了, 可是 URL 并没有修改, 所以, 重新加载之后拦截到的还是@"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?"
. 在这一步需做处理, 如不处理, 这一步就死循环.- 再次强调微信开发者平台的配置问题. 谁配谁知道!
基本实现如下:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// 在发送请求之前,决定是否跳转
//self.load 用来控制对 @"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?" 的拦截
NSString *url = navigationAction.request.URL.absoluteString;
if ([url containsString:@"weixin://wap/pay"]) {
self.load = NO;
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
decisionHandler(WKNavigationActionPolicyCancel);
}
else if ([url containsString:@"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?"] && !self.isLoad) {
NSURLRequest *request = navigationAction.request;
NSMutableURLRequest *newRequest = [[NSMutableURLRequest alloc] init];
newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
#warning scheme 要改
[newRequest setValue:@"www.xxx.cn://" forHTTPHeaderField: @"Referer"];
newRequest.URL = request.URL;
[webView loadRequest:newRequest];
self.load = YES;
decisionHandler(WKNavigationActionPolicyCancel);
}
else if ([url containsString:@"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?"]) {
self.load = NO;
decisionHandler(WKNavigationActionPolicyAllow);
}
decisionHandler(WKNavigationActionPolicyAllow);
}
至此, 已经完成文章开头所说的两个部分, 能调起也能返回了.
2019/8/23 更新
不愿意修改 header ? 反正就是不能改 header, 不接受上面微信的解决方案, 怎么办呢? 去翻了下 微信支付开发步骤&常见问题 , 还真的有新发现, 不知道是之前没注意还是新加的... 拼接 redirect_url
可以指定回调页面.
拿之前的 demo 简单的改改, 试了一下, 确实可以 👍
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// 在发送请求之前,决定是否跳转
//self.load 用来控制对 @"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?" 的拦截
NSString *url = navigationAction.request.URL.absoluteString;
if ([url containsString:@"weixin://wap/pay"]) {
self.load = NO;
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
decisionHandler(WKNavigationActionPolicyCancel);
}
else if ([url containsString:@"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?"] && !self.isLoad) {
NSURLRequest *request = navigationAction.request;
NSMutableURLRequest *newRequest = [[NSMutableURLRequest alloc] init];
// newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
// [newRequest setValue:@"www.xxx.cn://" forHTTPHeaderField: @"Referer"];
// newRequest.URL = request.URL;
// 这里 redirect_url 要传的值, 就是上面 Referer 的值
NSString *urlStr = [NSString stringWithFormat:@"%@&redirect_url=www.xxx.cn://", [request.URL absoluteString]];
newRequest.URL = [NSURL URLWithString:urlStr];
[webView loadRequest:newRequest];
self.load = YES;
decisionHandler(WKNavigationActionPolicyCancel);
}
else if ([url containsString:@"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?"]) {
self.load = NO;
decisionHandler(WKNavigationActionPolicyAllow);
}
decisionHandler(WKNavigationActionPolicyAllow);
}
简单点说就是不设置 Referer
, 把之前需要设置的 Referer
值, 直接作为 redirect_url
的值, 拼在 URL 后面. 但返回自有 App 两者显示的页面效果是不太相同的, 以支付失败为例(没有1分钱的支付就不存支付成功的🌰 🤣)
Referer
回到原有 App, 会停留在跳转前的页面(仿佛时间静止了)
redirect_url
回到原因 App, 打开了一个微信页面, 白屏... 手动返回会回到跳转前的页面
不过 微信支付开发步骤&常见问题 也有提到, 设置 redirect_url 后, 可能需要用户手动触发查单操作, 可能还需要 H5 那边做点啥子操作吧...... (看到这里, 我基本确定了, 这个是新加上去的!!!!!)