PAYATHON 2026

如何在 Titanium Module 中集成 Square Android Native SDK?

支付老李

结论

可以编写自定义 Android Titanium Module,把 Square 的原生 Android SDK 封装起来,再通过 Titanium JavaScript 调用。

基本流程如下:

  1. 在 Android Module 中引入 Square SDK。
  2. 使用 Java 初始化 Square SDK。
  3. 通过 Module 打开 Square 的银行卡录入界面。
  4. 在 onActivityResult() 中接收 Square 返回的支付凭证。
  5. 通过 Titanium 事件或回调将凭证传给 JavaScript。
  6. Titanium 把凭证发送到自己的后端,由后端调用 Square Payments API 完成扣款。

旧版 Square In-App Payments SDK 通常将客户端生成的凭证称为 nonce,部分新版产品或文档则称为 payment token。具体的 API 名称和 Gradle 版本要以项目实际使用的 Square SDK 文档为准。

Web Payments SDK 用于浏览器环境,不建议将它直接放进 Titanium WebView,作为原生集成的替代方案。Android 和 iOS 应分别开发对应的原生 Titanium Module。

创建 Android Titanium Module

使用 Titanium CLI 创建模块:

ti create --type module \
  --platform android \
  --name SquarePayments \
  --id com.example.squarepayments

Titanium SDK 版本不同,生成的目录结构可能会略有差异。核心代码位于模块的 Android 工程中。

引入 Square SDK

在模块的 Gradle 配置中添加 Square Maven 仓库和依赖。下面使用的是占位版本,不能将其视为当前最新版本:

repositories {
    google()
    mavenCentral()

    maven {
        url "https://sdk.squareup.com/public/android"
    }
}

dependencies {
    implementation "com.squareup.sdk.in-app-payments:card-entry:SQUARE_SDK_VERSION"
}

例如:

def squareSdkVersion = "YOUR_CONFIRMED_VERSION"

dependencies {
    implementation "com.squareup.sdk.in-app-payments:card-entry:$squareSdkVersion"
}

Square 的仓库地址、artifact 名称和 Android 最低版本要求可能会随 SDK 发布而变化。如果当前 Square 文档使用 Mobile Payments SDK,而不是旧版 In-App Payments SDK,就需要按照新 SDK 的依赖和支付流程调整,不能只更换版本号。

实现 Titanium 与 Square 的桥接

以下示例通过 Titanium 事件返回结果:

  • cardNonce:成功取得凭证
  • cardEntryError:录卡或 token 生成失败
  • cardEntryCanceled:用户取消
package com.example.squarepayments;

import android.app.Activity;
import android.content.Intent;

import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.annotations.Kroll;
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.TiLifecycle;
import org.appcelerator.titanium.TiBaseActivity;
import org.appcelerator.titanium.util.TiConvert;

import com.squareup.sdk.inapppayments.InAppPaymentsSdk;
import com.squareup.sdk.inapppayments.cardentry.CardDetails;
import com.squareup.sdk.inapppayments.cardentry.CardEntry;
import com.squareup.sdk.inapppayments.cardentry.CardEntryActivityResult;
import com.squareup.sdk.inapppayments.core.Callback;

@Kroll.module(
    name = "SquarePayments",
    id = "com.example.squarepayments"
)
public class SquarePaymentsModule extends org.appcelerator.kroll.KrollModule
        implements TiLifecycle.OnActivityResultEvent {

    private TiBaseActivity activeActivity;
    private boolean initialized;

    @Kroll.method
    public void initialize(String applicationId) {
        if (applicationId == null || applicationId.trim().isEmpty()) {
            throw new IllegalArgumentException(
                "Square applicationId must not be empty"
            );
        }

        if (!initialized) {
            InAppPaymentsSdk.INSTANCE.initialize(
                TiApplication.getInstance(),
                applicationId
            );
            initialized = true;
        }
    }

    @Kroll.method
    public void startCardEntry() {
        if (!initialized) {
            fireModuleError(
                "NOT_INITIALIZED",
                "Call initialize(applicationId) before startCardEntry()."
            );
            return;
        }

        Activity currentActivity = TiApplication.getAppCurrentActivity();

        if (!(currentActivity instanceof TiBaseActivity)) {
            fireModuleError(
                "NO_ACTIVITY",
                "No active Titanium activity is available."
            );
            return;
        }

        activeActivity = (TiBaseActivity) currentActivity;
        activeActivity.addOnActivityResultListener(this);

        CardEntry.startCardEntryActivity(activeActivity);
    }

    @Override
    public void onActivityResult(
            Activity activity,
            int requestCode,
            int resultCode,
            Intent data
    ) {
        if (requestCode != CardEntry.DEFAULT_CARD_ENTRY_REQUEST_CODE) {
            return;
        }

        removeActivityResultListener();

        if (resultCode == Activity.RESULT_CANCELED && data == null) {
            fireEvent("cardEntryCanceled", new KrollDict());
            return;
        }

        CardEntry.handleActivityResult(
            data,
            new Callback<CardEntryActivityResult>() {
                @Override
                public void onResult(CardEntryActivityResult result) {
                    if (result.isSuccess()) {
                        CardDetails cardDetails = result.getSuccessValue();

                        KrollDict payload = new KrollDict();
                        payload.put("nonce", cardDetails.getNonce());

                        fireEvent("cardNonce", payload);
                        return;
                    }

                    KrollDict payload = new KrollDict();
                    payload.put(
                        "message",
                        String.valueOf(result.getErrorValue())
                    );

                    fireEvent("cardEntryError", payload);
                }
            }
        );
    }

    private void fireModuleError(String code, String message) {
        KrollDict payload = new KrollDict();
        payload.put("code", code);
        payload.put("message", message);
        fireEvent("cardEntryError", payload);
    }

    private void removeActivityResultListener() {
        if (activeActivity != null) {
            activeActivity.removeOnActivityResultListener(this);
            activeActivity = null;
        }
    }
}

这段代码主要用于说明桥接结构。Square 和 Titanium 的版本不同,下列细节也可能不同:

  • CardEntry.startCardEntryActivity() 的参数形式。
  • 默认 request code 常量的位置。
  • CardEntryActivityResult 的错误对象接口。
  • Titanium Activity Result Listener 的注册方式。
  • AndroidX、Gradle Plugin 和最低 Android SDK 要求。

如果出现编译错误,需要根据实际安装的 Titanium SDK 和 Square SDK 调整相应签名,不要混用不同版本文档中的代码。

在 Titanium 应用中调用

编译并安装模块后,在 tiapp.xml 中启用它:

<modules>
    <module platform="android">
        com.example.squarepayments
    </module>
</modules>

然后在 Titanium JavaScript 中完成初始化并监听结果:

const SquarePayments = require("com.example.squarepayments");

SquarePayments.addEventListener("cardNonce", event => {
    sendPaymentTokenToBackend(event.nonce);
});

SquarePayments.addEventListener("cardEntryError", event => {
    console.error(
        "Square card entry failed:",
        event.code || "",
        event.message || "Unknown error"
    );
});

SquarePayments.addEventListener("cardEntryCanceled", () => {
    console.log("The customer canceled card entry.");
});

SquarePayments.initialize("YOUR_SQUARE_APPLICATION_ID");

function openPaymentForm() {
    SquarePayments.startCardEntry();
}

按钮调用示例:

const payButton = Ti.UI.createButton({
    title: "支付"
});

payButton.addEventListener("click", () => {
    openPaymentForm();
});

$.window.add(payButton);

将支付凭证发送到后端

客户端只负责采集银行卡信息并获取一次性支付凭证。Square access token 不能保存在 Titanium 应用中,也不能由客户端直接发起需要服务端认证的付款请求。

function sendPaymentTokenToBackend(paymentToken) {
    const client = Ti.Network.createHTTPClient({
        timeout: 15000,

        onload() {
            const response = JSON.parse(this.responseText);

            if (response.success) {
                console.log("Payment completed");
            } else {
                console.error(response.message || "Payment failed");
            }
        },

        onerror(error) {
            console.error("Payment request failed:", error.error);
        }
    });

    client.open("POST", "https://api.example.com/payments");

    client.setRequestHeader(
        "Content-Type",
        "application/json"
    );

    client.send(JSON.stringify({
        paymentToken,
        amount: 2599,
        currency: "USD",
        orderId: "ORDER_12345"
    }));
}

不要完全信任客户端提交的金额。后端应根据订单重新计算应付金额,并验证订单归属、状态和币种。

后端再通过 Square 服务端 SDK 或 Payments API 创建付款。概念性示例如下:

const request = {
    sourceId: paymentToken,
    idempotencyKey: crypto.randomUUID(),
    amountMoney: {
        amount: 2599,
        currency: "USD"
    }
};

const response = await squareClient.payments.create(request);

实际方法名取决于后端使用的 Square SDK 语言和版本。idempotencyKey 应由后端生成并持久化,以免网络重试造成重复扣款。

使用回调的处理方式

如果希望每次调用时传入回调,可以让 Module 接收 KrollFunction:

private org.appcelerator.kroll.KrollFunction pendingCallback;
private org.appcelerator.kroll.KrollObject callbackContext;

@Kroll.method
public void startCardEntry(
        org.appcelerator.kroll.KrollFunction callback
) {
    pendingCallback = callback;
    callbackContext = getKrollObject();

    // 注册 Activity Result Listener,然后启动 CardEntry。
}

成功后调用:

KrollDict result = new KrollDict();
result.put("success", true);
result.put("nonce", cardDetails.getNonce());

pendingCallback.call(callbackContext, result);
pendingCallback = null;
callbackContext = null;

JavaScript 侧调用:

SquarePayments.startCardEntry(result => {
    if (result.success) {
        sendPaymentTokenToBackend(result.nonce);
        return;
    }

    console.error(result.message);
});

如果应用可能同时发起多个支付操作,回调模式更便于将结果与当前请求对应起来。模块仍需防止重复打开录卡页面,并在 Activity 销毁、用户取消或发生异常时清理待处理回调。

必须注意的安全问题

  • Square access token 只能保存在后端,不能放进 Titanium 应用或 Android Module。
  • Square application ID 通常属于客户端配置,与服务端 access token 不同。
  • 不要记录银行卡号、CVV、支付 nonce 或 payment token。
  • 不要让银行卡信息经过 Titanium JavaScript、自己的服务器或普通输入框。
  • 后端必须重新验证客户端提交的金额、订单号和商品信息。
  • 后端创建付款时应使用幂等键。
  • Sandbox 与生产环境的 application ID、location 和 access token 必须成套切换。
  • 是否支付成功,应以后端 Square API 返回结果或 webhook 验证结果为准,不能只根据客户端页面关闭来判断。
  • Android 模块只能用于 Android。iOS 需要使用 Objective-C 或 Swift 单独开发 Titanium iOS Module。

如果项目需要长期维护,建议先确认 Square 当前是否仍允许新项目使用 In-App Payments SDK。如果 Square 已要求使用 Mobile Payments SDK,桥接思路仍然相同,但初始化方式、UI 启动方式和结果对象都必须改用该 SDK 当前公开的 API。

备注:内容仅供参考。