如何在 Titanium Module 中集成 Square Android Native SDK?
结论
可以编写自定义 Android Titanium Module,把 Square 的原生 Android SDK 封装起来,再通过 Titanium JavaScript 调用。
基本流程如下:
- 在 Android Module 中引入 Square SDK。
- 使用 Java 初始化 Square SDK。
- 通过 Module 打开 Square 的银行卡录入界面。
- 在
onActivityResult()中接收 Square 返回的支付凭证。 - 通过 Titanium 事件或回调将凭证传给 JavaScript。
- 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。
备注:内容仅供参考。