如何在 iPhone app 内接受信用卡付款?
结论
可以在 app 内接受信用卡捐款,但不要自行采集、保存或处理卡号。较稳妥的做法是:
- 接入支付服务商的 iOS SDK,由 SDK 提供信用卡输入界面,并将卡信息令牌化。
- app 只将支付令牌或支付方式 ID 交给业务服务器。
- 服务器调用支付服务商 API 创建并确认付款。
- 同时支持 Apple Pay。对于公益捐款,这通常也是 App Store 审核的重要条件。
不要继续使用 UIWebView,它已经废弃。优先选择支付服务商的原生 SDK;如果必须使用网页收银台,可以采用 ASWebAuthenticationSession 或 SFSafariViewController。app 能否开展募捐,还要看组织资质、所在地区以及提交审核时有效的 App Review Guidelines,不能因为 app 免费就认定一定合规。
推荐架构
常见的付款流程如下:
iPhone app
│
├─ 向业务服务器请求创建捐款订单
│
▼
业务服务器
│
├─ 调用支付服务商 API 创建 PaymentIntent / Order
│
└─ 返回 client secret 或 checkout session
│
▼
iPhone app
│
├─ 使用支付服务商 SDK 展示信用卡或 Apple Pay
└─ SDK 将卡号直接发送给支付服务商
│
▼
支付服务商
│
├─ 完成 3-D Secure 等身份验证
└─ 通过 webhook 通知服务器最终付款状态
信用卡号不应经过业务服务器。这样做并不能完全免除 PCI DSS 责任,但可以大幅缩小合规范围。
实现步骤
1. 选择支持 iOS 的支付服务商
选择服务商时,至少需要确认它具备以下能力:
- 提供官方 iOS SDK。
- 支持卡信息令牌化。
- 支持 Apple Pay。
- 支持 3-D Secure 2 和必要的银行验证流程。
- 支持服务器端 webhook。
- 允许处理捐款,而不只是普通商品交易。
- 支持组织注册地和捐款人所在地区。
Stripe、Adyen、Braintree 等服务通常具备类似能力。不过,捐款政策、费率和 SDK API 都可能调整,接入时应以官方文档为准。
2. 在服务器创建付款
服务器必须验证金额,不能直接信任客户端提交的数值。下面是与具体支付服务商无关的通用伪代码:
app.post("/api/donations/payment-session", async (req, res) => {
const amount = validateDonationAmount(req.body.amount);
const currency = "usd";
const payment = await paymentProvider.createPayment({
amount,
currency,
paymentMethodTypes: ["card"],
metadata: {
purpose: "donation"
}
});
res.json({
paymentId: payment.id,
clientSecret: payment.clientSecret
});
});
生产环境还需要处理认证、限流、幂等键、金额上下限和异常日志。金额通常以最小货币单位表示,例如 10.00 USD 写成 1000,具体仍要遵循支付服务商的规则。
3. 在 app 内拉取付款会话
import Foundation
struct PaymentSession: Decodable {
let paymentId: String
let clientSecret: String
}
func createPaymentSession(
amount: Int,
completion: @escaping (Result<PaymentSession, Error>) -> Void
) {
var request = URLRequest(
url: URL(string: "https://example.com/api/donations/payment-session")!
)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: [
"amount": amount
])
URLSession.shared.dataTask(with: request) { data, _, error in
if let error {
completion(.failure(error))
return
}
guard let data else {
completion(.failure(URLError(.badServerResponse)))
return
}
do {
let session = try JSONDecoder().decode(PaymentSession.self, from: data)
completion(.success(session))
} catch {
completion(.failure(error))
}
}.resume()
}
取得 clientSecret 后,将它传给支付服务商提供的支付组件。组件的实际名称可能是 PaymentSheet、Drop-in UI 或 Hosted Fields,请按所选 SDK 的当前版本接入。
调用方式大致如下:
createPaymentSession(amount: 1000) { result in
switch result {
case .success(let session):
DispatchQueue.main.async {
paymentSDK.presentPaymentSheet(
clientSecret: session.clientSecret,
from: self
) { result in
// 此结果只用于更新界面。
// 最终付款状态仍应由服务器和 webhook 确认。
}
}
case .failure(let error):
print("Unable to create payment session: \(error)")
}
}
paymentSDK.presentPaymentSheet 只是示意 API,不是可以直接编译的系统接口。实现时需要换成支付服务商 SDK 提供的真实调用。
支持 Apple Pay
使用 Apple Pay 前,需要在 Apple Developer 后台创建 Merchant ID、配置证书,并为 target 启用 Apple Pay capability。
下面是创建支付请求的基本结构:
import PassKit
func makeApplePayRequest(amount: Decimal) -> PKPaymentRequest {
let request = PKPaymentRequest()
request.merchantIdentifier = "merchant.com.example.donations"
request.countryCode = "US"
request.currencyCode = "USD"
request.supportedNetworks = [.visa, .masterCard, .amex, .discover]
request.merchantCapabilities = [.capability3DS]
request.paymentSummaryItems = [
PKPaymentSummaryItem(
label: "Donation",
amount: NSDecimalNumber(decimal: amount)
),
PKPaymentSummaryItem(
label: "Example Foundation",
amount: NSDecimalNumber(decimal: amount)
)
]
return request
}
然后通过 PKPaymentAuthorizationController 展示支付界面:
let request = makeApplePayRequest(amount: Decimal(string: "10.00")!)
guard PKPaymentAuthorizationController.canMakePayments(
usingNetworks: request.supportedNetworks
) else {
return
}
let controller = PKPaymentAuthorizationController(paymentRequest: request)
controller.delegate = self
controller.present { presented in
if !presented {
print("Unable to present Apple Pay")
}
}
用户授权后,应用会取得 PKPayment。其中的支付数据应交给支付服务商 SDK 或服务器处理,不能将它视为款项已经到账的证明。
付款结果必须由 webhook 确认
客户端可能断网或被终止,用户也可能在银行验证结束前退出,因此不能仅凭 app 内的回调将捐款记为成功。
服务器应验证支付服务商的 webhook 签名,再根据事件更新订单:
app.post("/webhooks/payment", rawBodyMiddleware, async (req, res) => {
const event = paymentProvider.verifyWebhook(
req.body,
req.headers["payment-signature"]
);
if (event.type === "payment.succeeded") {
await markDonationAsPaid(event.paymentId);
}
if (event.type === "payment.failed") {
await markDonationAsFailed(event.paymentId);
}
res.sendStatus(200);
});
还要使用支付服务商提供的事件 ID 或付款 ID 进行幂等处理,避免 webhook 重试导致重复记账。
如果只能使用网页收银台
服务器可以生成一次性的托管收银台 URL,再由 app 打开。不要将长期密钥、金额计算逻辑或信用卡处理代码放在网页脚本中。
涉及 3-D Secure、银行跳转和 Apple Pay 时,WKWebView 容易受到 Cookie、弹窗、回跳和支付能力等限制。更合适的方式是:
import SafariServices
func presentCheckout(url: URL, from viewController: UIViewController) {
let safari = SFSafariViewController(url: url)
viewController.present(safari, animated: true)
}
付款完成后,可以通过 Universal Links 或自定义 URL Scheme 返回 app。返回后仍要向服务器查询付款状态,不能直接相信回跳 URL 中的 success=true。
App Store 审核注意事项
捐款和普通数字内容购买并不相同,但这不代表捐款可以无条件绕过 In-App Purchase。
提交审核前,需要重点确认:
- 收款主体是否属于当地法律认可的公益或非营利组织。
- 当前 App Review Guidelines 是否允许该主体在 app 内直接募捐。
- 是否必须提供 Apple Pay。
- 是否需要说明资金用途、受益方和退款政策。
- 是否需要向捐款人提供收据或税务凭证。
- 款项是否直接进入合规组织,而不是开发者个人账户。
- 捐款是否会解锁 app 中的数字功能、内容、会员权益或虚拟物品。
如果付款会解锁数字内容或 app 功能,即使界面将它称为”捐款”,审核时仍可能被认定为数字商品交易,并被要求使用 StoreKit。只有不提供对价的捐赠才适合按募捐规则处理。
安全与合规注意事项
- 不要记录卡号、CVV、Apple Pay token 或完整支付请求。
- 不要在 app 中保存支付服务商的 secret key。
- 所有业务接口必须使用 HTTPS。
- 金额、币种和收款对象必须由服务器校验。
- 创建付款时使用幂等键,避免用户重复点击造成重复扣款。
- 不要将客户端显示的”成功”直接记为到账。
- 清楚展示捐款金额、币种、收款组织以及是否自动续费。
- 如果是定期捐款,需要单独取得授权,并提供清晰的取消方式。
- 上线前完成支付服务商要求的 PCI DSS 自评和组织身份验证。
实际可行的方案不是在 UIWebView 中自行编写信用卡表单,而是使用支付服务商的 iOS SDK 或托管收银台,再由服务器创建付款、验证 webhook 并记录最终结果。
备注:内容仅供参考。
