PAYATHON 2026

如何在 iPhone app 内接受信用卡付款?

支付小周

结论

可以在 app 内接受信用卡捐款,但不要自行采集、保存或处理卡号。较稳妥的做法是:

  1. 接入支付服务商的 iOS SDK,由 SDK 提供信用卡输入界面,并将卡信息令牌化。
  2. app 只将支付令牌或支付方式 ID 交给业务服务器。
  3. 服务器调用支付服务商 API 创建并确认付款。
  4. 同时支持 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 并记录最终结果。

备注:内容仅供参考。