You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Square API集成Cash App时的ontokenization事件触发异常问题

Cash App Pay集成:解决ontokenization提前触发的问题

问题背景

我通过Square API SDK完成Cash App Payment集成,使用Javascript Web SDK与PHP SDK,移动端应用通过WebView打开Cash App支付页面,当前流程:

  • Web SDK生成Cash App Payment按钮
  • 点击按钮跳转至Cash App页面,显示「Open Cash App」和「Get Cash App」按钮
  • 点击「Open Cash App」打开Cash App支付界面
  • 问题:跳转至Cash App支付界面时,ontokenization事件立即触发,获取到status=OK及token,即使用户未点击支付界面的「OK」按钮或直接返回,事件仍会触发。预期逻辑是用户点击「OK」确认支付后,再触发事件并处理支付。

核心原因

Square Cash App Pay的Web SDK中,ontokenization事件仅代表成功生成支付源(sourceId),而非用户完成支付授权。在移动端WebView场景下,跳转至Cash App后SDK会提前完成token生成,但用户是否确认支付需要通过redirectURL回调来确认。

解决方案

调整逻辑,将支付处理从ontokenization事件中剥离,改为:

  1. 在ontokenization事件中仅存储token,不立即发起支付请求
  2. 当用户从Cash App跳转回WebView时,再调用PHP SDK创建支付并验证状态

修改后的代码片段

const appId = 'MYAPPID';
const locationId = 'MY LOCATION ID';

function buildPaymentRequest(payments) {
    const paymentRequest = payments.paymentRequest({
        countryCode: 'US',
        currencyCode: 'USD',
        total: {
            amount: '10',
            label: 'Total',
        },
    });
    return paymentRequest;
}

async function initializeCashApp(payments) {
    const paymentRequest = buildPaymentRequest(payments);
    const cashAppPay = await payments.cashAppPay(paymentRequest, {
        redirectURL: window.location.href,
        referenceId: 'my-website-00000001'
    });
    const buttonOptions = {
        shape: 'semiround',
        width: 'full',
    };
    await cashAppPay.attach('#cash-app-pay', buttonOptions);
    return cashAppPay;
}

async function createPayment(token, resData) {
    const body = JSON.stringify({
        locationId,
        sourceId: token,
        temp_order_id: '123'
    });

    const paymentResponse = await fetch('/web/payment/cash-app-payment', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body,
    });

    if (paymentResponse.ok) {
        return paymentResponse.json();
    }

    const errorBody = await paymentResponse.text();
    throw new Error(errorBody);
}

async function paymentFail(tokenResponse) {
    const body = JSON.stringify({
        locationId,
        temp_order_id: '123',
        tokenResponse: tokenResponse
    });

    const paymentResponse = await fetch('/web/payment/cash-app-payment-fail', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body,
    });

    if (paymentResponse.ok) {
        return paymentResponse.json();
    }

    const errorBody = await paymentResponse.text();
    throw new Error(errorBody);
}

function redirectToPaymentResults(status, errorMessage) {
    let redirectUrl = '';
    if (status === 'SUCCESS') {
        redirectUrl = 'api/payment/close?action=success';
    } else {
        redirectUrl = `api/payment/close?action=error&message=${encodeURIComponent(errorMessage)}`;
    }
    window.location.href = redirectUrl;
}

// 检查是否从Cash App跳转回来,处理支付
async function handlePostRedirectPayment() {
    const storedToken = sessionStorage.getItem('cashAppPayToken');
    if (storedToken) {
        try {
            const paymentResults = await createPayment(storedToken);
            if (paymentResults.status === 'SUCCESS') {
                redirectToPaymentResults('SUCCESS', '');
            } else {
                redirectToPaymentResults('FAILURE', paymentResults.message || '支付失败');
            }
        } catch (error) {
            await paymentFail({ tokenResult: { status: 'ERROR', errors: [error.message] } });
            redirectToPaymentResults('FAILURE', error.message);
        } finally {
            sessionStorage.removeItem('cashAppPayToken');
        }
    }
}

document.addEventListener('DOMContentLoaded', async function () {
    if (!window.Square) {
        throw new Error('Square.js failed to load properly');
    }

    // 先处理从Cash App返回后的支付逻辑
    await handlePostRedirectPayment();

    let payments;
    try {
        payments = window.Square.payments(appId, locationId);
    } catch (e) {
        const statusContainer = document.getElementById('payment-status-container');
        statusContainer.className = 'missing-credentials';
        statusContainer.style.visibility = 'visible';
        console.log(e.message);
        return;
    }

    let cashAppPay;
    try {
        cashAppPay = await initializeCashApp(payments);
    } catch (e) {
        console.error('Initializing Cash App Pay failed', e);
        return;
    }

    cashAppPay.addEventListener('ontokenization', async function ({ detail }) {
        const tokenResult = detail.tokenResult;
        if (tokenResult.status === 'OK') {
            // 仅存储token,等待用户从Cash App返回后处理
            sessionStorage.setItem('cashAppPayToken', tokenResult.token);
        } else {
            let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
            if (tokenResult.errors) {
                errorMessage += ` and errors: ${JSON.stringify(tokenResult.errors)}`;
            }
            await paymentFail(detail);
            redirectToPaymentResults('FAILURE', errorMessage);
        }
    });
});

关键修改点

  1. 新增handlePostRedirectPayment函数:页面加载时检查是否有存储的token,若有则发起支付请求,确保只有用户从Cash App返回后才处理支付。
  2. 调整ontokenization事件逻辑:仅在token生成成功时将其存入sessionStorage,不立即调用支付接口;token生成失败时直接处理错误。
  3. 修复redirectToPaymentResults的URL编码:对错误消息进行encodeURIComponent处理,避免URL参数异常。
  4. 移除原事件中提前调用的paymentFail:该调用不符合逻辑,仅在明确失败场景下调用。

后端PHP SDK补充说明

在PHP后端处理支付时,需调用Square的CreatePayment接口,并检查返回的支付状态:

  • 若支付状态为COMPLETED,则标记订单成功
  • 若为FAILED或其他状态,则处理失败逻辑,避免提前确认订单

内容的提问来源于stack exchange,提问作者Pranav Gandhi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 14:52:49