在工作中对于h5支付这个,遇到的问题很多,所以想记录一下,以后说不定就忘记了,没对接过的,看一下也可以避免很多坑。
背景
因需要更灵活的运营场景,在app内需要接入第三方(微信、支付宝)h5支付
基本信息文档
微信h5支付开发及常见问题:pay.weixin.qq.com/wiki/doc/ap…
实践
基本的接入方式不做过多的说明,下面以项目的维度简单说明一下在使用h5支付的过程中碰到的问题,以及处理方式;
星座牌小游戏
概述:
星座小游戏是一个卡片类抽金币的小游戏,用户通过选择对应的牌来抽取大于支付金额等值的金币;
基本流程:
关键逻辑及处理方式
一、星座牌小游戏调起微信h5支付
// 打开微信支付
let payParams = JSON.parse(ret.data.payParams)
let openWxUrl = payParams.payUrl + "&redirect_url=" + encodeURIComponent(redirectUrl)
let hideFrame = document.createElement('iframe')
hideFrame.setAttribute('src', openWxUrl);
hideFrame.setAttribute('sandbox','allow-scripts allow-top-navigation allow-same-origin')
document.body.appendChild(hideFrame)
/*
这里更加优雅的可以用iframe onload 去处理,因为用户加载iframe的时间是受网络环境影响,直接2s后移除不可控
setTimeout(function() {
hideFrame.parentNode.removeChild(hideFrame);
}, 2000);
*/
hideFrame.onload = function(){
//其他逻辑
setTimeout(function() {
hideFrame.parentNode.removeChild(hideFrame);
}, 100);
}
上面的代码,有几个处逻辑处理,对应的,通过下面的几个问题解释;
-
为什么通过iframe的形式加载payUrl? 由星座牌小游戏的交互决定,因为用户在支付完之后,还需要在当前页面开奖,因此,通过iframe的形式处理,还能解决微信h5支付对于payUrl 加载的referer验证问题;
-
iframe 为什么要设置sandbox?,为什么要设定setTimeout去移除iframe?
- 在ios13发布的时候,ios出现了问题,微信h5支付没有调起成功;
- app在安卓的某个版本之后也出现了同样的问题,对应那个版本的app,app打包的安卓sdk版本做了更新
对应的异常log如下
message:Uncaught SecurityError: Failed to set the 'href' property on 'Location': The current window does not have permission to navigate the target frame to 'weixin://wap/pay?prepayid%3Dwx241530497040689dd1394a4f1296524900&package=3510274145&noncestr=1563953450&sign=7867a2dd7feb974db9285725cb2822d0'.
从log看到的代码error,其实不是业务本身的代码,而是payUrl加载后,支付页里面的"top.location.href"异常,关键代码如下
微信h5支付预下单后返回的payUrl关键逻辑代码如下:
//payUrl
var is_postmsg="";
if(is_postmsg=="1")
{
parent.postMessage(JSON.stringify({
action : "send_deeplink",
data : {
deeplink : "weixin://wap/pay?prepayid%3Dwx28141344091494d0c83d3c3cd472e50000&package=3738825662&noncestr=1609136025&sign=02d138bef13beb9222479014a4a4ea85"
}
}), "");
}
else
{
var url="weixin://wap/pay?prepayid%3Dwx28141344091494d0c83d3c3cd472e50000&package=3738825662&noncestr=1609136025&sign=02d138bef13beb9222479014a4a4ea85";
var redirect_url="https://ulink.com/ulink/lucky/index.html";
top.location.href=url;
if(redirect_url)
{
setTimeout(
function(){
top.location.href=redirect_url;
},
5000
);
}
else
{
setTimeout(
function(){
window.history.back();
},
5000);
}
}
从上面的代码块可以看到payUrl调起微信的逻辑
-
调起微信走的else的逻辑(这里的postmsg 的逻辑暂时未从微信官方文档内找到对于的配置方式),通过 top.loaction.href= url 的形式 加载 scheme调起;
-
如果有redirect_url,会设置个定时器, 5秒后重定向到redirect_url --- 这也是为什么星座牌小游戏需要添加一个remove iframe的逻辑的原因,
整体梳理下来,问题的原因就是app在用新的android sdk打包之后,webview的内核版本的提升,对应的内容安全策略(csp)调整(默认设置调整),阻止了payUrl通过top的方式直接访问父页面的api;那么这个问题是通过iframe设置sandbox属性解决:
/* sandbox
allow-forms 允许进行提交表单
allow-scripts 运行执行脚本
allow-same-origin 允许同域请求,比如ajax,storage
allow-top-navigation 允许iframe能够主导window.top进行页面跳转
allow-popups 允许iframe中弹出新窗口,比如,window.open,target=”_blank”
allow-pointer-lock 在iframe中可以锁定鼠标,主要和鼠标锁定有关
*/
hideFrame.setAttribute('sandbox','allow-scripts allow-top-navigation allow-same-origin')
3、redirectUrl是回调地址,微信是怎么跳回指定的地址的?redirectUrl为什么配置ulink的地址?
微信处理redirectUrl的逻辑,如下:
if(redirect_url)
{
setTimeout(
function(){
top.location.href=redirect_url;
},
5000);
}
else
{
setTimeout(
function(){
window.history.back();
},
5000);
}
redirectUrl为什么配置ulink的地址?
android在跳到微信支付完成后,点击支付成功页面的“完成”按钮,会回到调起微信支付的app,ios点击完成不会做这部分操作,在有配置redirectUrl的情况下会用safari打开redirectUrl;且redirectUrl也有域名的验证;
配置成ulink的地址,是为了解决ios在支付完成后回到app内的处理方式;
注:ios也有不能直接回到app的情况,而是通过safari打开了ulink的页面,这种情况,经过跟客户端同事的一同排查的结果是因为用户在安装app的时候,没有下载到apple-app-site-association这个文件导致;
app充值(内嵌、外部)
概述:
内嵌页h5充值页面,提供方便用户充值的页面,并且支持微信支付,支付宝支付,对应app内的充值;
基本逻辑:
同星座牌小游戏类似,不同的地方是开奖变成了用户支付完成状态的轮询;
一、微信h5支付
避免了星座牌小游戏上喷到的问题之后,暂时(这个在子app的充值弹窗上又遇到了新的问题)没其他的问题出现;
加载payUrl后交互时序图(非官方):
二、支付宝h5支付(支付宝h5支付有两种方式:1. 纯h5页面支付;2.h5调起支付宝app支付),以下为h5调起支付宝app支付的流程
支付宝h5支付,返回的不是payUrl,而是一段formDomString;
提交payForm后交互时序图(非官方):
<!-- 支付宝h5支付,接口返回的form信息 -->
<form name=\"punchout_form\" method=\"post\" action=\"https://openapi.alipay.com/gateway.do?charset=UTF-8&method=alipay.trade.wap.pay&sign=HfcRgFeT%2FSVj1soSQrBQYCV%2BaoQzrBVupczUmmjM0sQ2FqXlFHMqOti4EexmhSh3Ap%2FRAAG8MXlo%2FTbzVquR59bXe3deuTXc30S5cgsV9l00jaKPOKXdSfJah2r%2FR5onafKys9caXLaaQmVwtrSrWr5hMFz%2FmtfZvZWwch%2FFvJuVS0wlGT128GBG0KSiUue0g2Bs%2BVg%2B3WKhIiQLCBMKB7BuuyFCnvwnpjeLiGafjIYr6CNBn83uzac1QX9OBuzp91EVLGbBSwAFyyxALhporUh4pDe27SqJbwg15kQd6tDp2f7423M6AoQGkEDMdzaBWRTu2UrMenzaqDOpFpilHA%3D%3D&return_url=https%3A%2F%2Fapp.test.com%2Fstatic%2Fh5Conversion%2Findex.html¬ify_url=https%3A%2F%2Fapp.com%2Fcallback%2Fppywforkylin%2Falipay%2Falipay%2F5103092247759423283&version=1.0&app_id=2021001145660238&sign_type=RSA2×tamp=2020-12-28+15%3A28%3A39&alipay_sdk=alipay-sdk-java-3.4.49.ALL&format=json\">\n<input type=\"hidden\" name=\"biz_content\" value=\"{"body":"10金币","out_trade_no":"1231231233123123123","product_code":"QUICK_WAP_WAY","subject":"10金币","timeout_express":"2m","total_amount":"1"}\">\n<inp
1、以上form代码段直接通过innerHtml插入页面还是不行的,script内的submit并不会被执行; 因为HTML 5 中指定不执行由 innerHTML 插入的
//业务处理
$iframe.innerHtml = payForm;
//HTML 5 中指定不执行由 innerHTML 插入的 <script> 标签。
$iframe.qeuerySelector('form').submit();
提交Form表单后,支付宝部分代码 支付宝scheme唤起逻辑
// 安卓走iframe方式唤起
if (ua.indexOf('android')>-1 && !noIntentTest) {
canIntent = false;
}
/**
* open client
*/
_AP.open = function (params) {
if (!domLoaded && (ua.indexOf('360 aphone')>-1 || canIntent)) {
var arg = arguments;
delayToRun = function () {
_AP.open.apply(null, arg);
delayToRun = null;
};
return;
}
if (locked) {
return;
}
locked = true;
var o;
// START:: 回跳 scheme 处理
var iosScheme, androidScheme, backScheme;
if (backScheme) {
iosScheme = backScheme.ios;
androidScheme = backScheme.android;
try {
window.tracker.log({
code: 11,
msg: 'scheme来源: '+ JSON.stringify(backScheme),
sampleRate: 1,
});
}catch(e){
console.warn('scheme来源获取错误:', e)
}
}
if (typeof params === 'object') {
if (iosScheme) {
params.h5FromAppUrlScheme = iosScheme;
params.sourceSceneType = 'h5Route';
}
o = {
'ios': encodeURIComponent(JSON.stringify(params)),
'android': encodeURIComponent(params.dataString)
};
if (androidScheme) {
o.android = o.android + '&sourceSceneType=h5Route&h5FromAppUrlScheme=' + androidScheme
}
} else {
console.error('params error, pls use JSON format!')
}
// END
// params fault tolerance
if (typeof o.ios !== 'string') {
o.ios = '';
} else if(typeof o.android !== 'string') {
o.android = '';
}
// nonsupport Android intent
if (!canIntent) {
if(isAndroid) {
var alipaysUrl = 'alipays://platformapi/startApp?appId=54556654&orderSuffix=' + o.android +'#Intent;scheme=alipays;package=com.eg.android.AlipayGphone;end';
}
//fix for iOS QQ browser
else if (ua.indexOf('mqqbrowser') > -1) {
var alipaysUrl = 'alipay://alipayclient/?' + o.android;
}
else {
var alipaysUrl = 'alipay://alipayclient/?' + o.ios;
}
//FIXME: 直接判断ios,不判断os版本号
if ( ua.indexOf('qq/') > -1 || ( ua.indexOf('safari') > -1 && ua.indexOf('os 9_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 10_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 11_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 12_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 13_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 14_') > -1 ) ) {
var openSchemeLink = document.getElementById('openSchemeLink');
if (!openSchemeLink) {
openSchemeLink = document.createElement('a');
openSchemeLink.id = 'openSchemeLink';
openSchemeLink.style.display = 'none';
document.body.appendChild(openSchemeLink);
}
//openSchemeLink.href = alipaysUrl;
// oppo浏览器兼容写法
openSchemeLink.onclick = function() {
window.location.href = alipaysUrl;
};
// trigger click
openSchemeLink.dispatchEvent(customClickEvent());
}
else {
var ifr = document.createElement('iframe');
ifr.src = alipaysUrl;
ifr.style.display = 'none';
document.body.appendChild(ifr);
}
$('.J-startapp').attr('href', alipaysUrl);
}
//support Android intent
else {
var packageKey = 'AlipayGphone';
var intentUrl = 'alipays://platformapi/startApp?appId=54545544&orderSuffix='+o.android+'#Intent;scheme=alipays;package=com.eg.android.'+ packageKey +';end';
var openIntentLink = document.getElementById('openIntentLink');
if (!openIntentLink) {
openIntentLink = document.createElement('a');
openIntentLink.id = 'openIntentLink';
openIntentLink.style.display = 'none';
document.body.appendChild(openIntentLink);
}
//openIntentLink.href = intentUrl;
// oppo浏览器兼容写法
openIntentLink.onclick = function() {
window.location.href = intentUrl;
};
// trigger click
openIntentLink.dispatchEvent(customClickEvent());
}
setTimeout(function () {
locked = false;
}, 2500)
支付宝结果轮询,以及returnUrl 逻辑
//
//轮询
var payquery = function () {
if(stopQuery) { return }
var argumentsPayquery = arguments;
Zepto.ajax({
type: 'post',
url: '/h5/h5RoutePayResultQuery.json?h5_route_token=RZ42FugnN5SrDYUBQr3vTTeQg3magDmobilecashierRZ42&need_invoke_app=true', /*/h5/h5RoutePayResultQuery.json?h5_route_token=*/
data:{
'_input_charset': 'utf-8',
'params': $('input[name=params]').val(),
'session': 'RZ42FugnN5SrDYUBQr3vTTeQg3magDmobilecashierRZ42'
},
timeout: 30000,
dataType: 'json',
success: function (data) {
//已唤起支付宝客户端
if(data.data.invokeAlipay && time){
time = 0;
}
//成功
if(data.control_type == 'pay_success') {
if(data.data && data.data.returnUrl && data.data.returnUrl != '') {
window.location.replace(decodeURIComponent(data.data.returnUrl));
}
}
//继续轮询
else if (data.control_type == 'h5_route_need_pay_query') {
if(!data.data.stopQuery) {
setTimeout(function(){
argumentsPayquery.callee();
}, data.data.dismisstime);
}
}
}
});
}
子app-充值弹窗
概述:
app内需要有一个h5充值的半屏弹窗,只接入微信h5支付
问题:
- iphone用户,在h5调起微信app支付界面后,取消或者完成支付后没有返回app 原因:
- 客户端的ua问题
排查:
1、子app与主app的ua比较
子app:
userAgent%20/3.9.0_build119317%20NetType/WiFi%20Language/zh-Hans-CN),
主app: Mozilla/5.0%20(iPhone;%20CPU%20iPhone%20OS%2012_4_1%20like%20Mac%20OS%20X)%20AppleWebKit/605.1.15%20(KHTML,%20like%20Gecko)%20Mobile/15E148%20/1.8.8_build132540%20NetType/WiFi%20Language/zh-Hans-CN)
调试:通过whistle调整userAgent为添加mobile标识后,测试正常(推测微信支付界面点击完成或者取消用safari访问redirectUrl的逻辑中,有ua的判断逻辑)
表现:子app调起微信支付后,点击取消,不执行回调url