阅读 1349

H5支付踩坑记

在工作中对于h5支付这个,遇到的问题很多,所以想记录一下,以后说不定就忘记了,没对接过的,看一下也可以避免很多坑。

背景

因需要更灵活的运营场景,在app内需要接入第三方(微信、支付宝)h5支付

基本信息文档

微信h5支付开发及常见问题:pay.weixin.qq.com/wiki/doc/ap…

实践

基本的接入方式不做过多的说明,下面以项目的维度简单说明一下在使用h5支付的过程中碰到的问题,以及处理方式;

星座牌小游戏

概述:

星座小游戏是一个卡片类抽金币的小游戏,用户通过选择对应的牌来抽取大于支付金额等值的金币;

基本流程:

luckdraw.png

关键逻辑及处理方式

一、星座牌小游戏调起微信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);
}
复制代码

上面的代码,有几个处逻辑处理,对应的,通过下面的几个问题解释;

  1. 为什么通过iframe的形式加载payUrl?

由星座牌小游戏的交互决定,因为用户在支付完之后,还需要在当前页面开奖,因此,通过iframe的形式处理,还能解决微信h5支付对于payUrl 加载的referer验证问题;

  1. 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调起微信的逻辑

  1. 调起微信走的else的逻辑(这里的postmsg 的逻辑暂时未从微信官方文档内找到对于的配置方式),通过 top.loaction.href= url 的形式 加载 scheme调起;

  2. 如果有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后交互时序图(非官方): wxpayUrl.png

二、支付宝h5支付(支付宝h5支付有两种方式:1. 纯h5页面支付;2.h5调起支付宝app支付),以下为h5调起支付宝app支付的流程

支付宝h5支付,返回的不是payUrl,而是一段formDomString;

提交payForm后交互时序图(非官方): aliPayForm.png

<!-- 支付宝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&notify_url=https%3A%2F%2Fapp.com%2Fcallback%2Fppywforkylin%2Falipay%2Falipay%2F5103092247759423283&version=1.0&app_id=2021001145660238&sign_type=RSA2&timestamp=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

文章分类
前端
文章标签