iOS app 如何销售服务 tokens 并避免 30% 抽成?
结论
可以在 iOS App 内完成付款,通常不需要使用 Apple 的 In-App Purchase,也不会产生 Apple 针对数字内容收取的 30% 佣金。
前提是用户购买的内容必须在线下美容院实际消费,而不是 App 内的数字内容、虚拟功能或数字权益。美甲、按摩、理发,以及支付给美容师的小费,都属于线下服务,通常应通过 Apple Pay、银行卡或其他合规方式付款,无须使用 StoreKit。
付款产品不宜设计成可以自由储值、转让或兑换的通用 “tokens”。更稳妥的方案是让用户直接购买明确的美容服务,或者把余额限定为只能用于平台内线下美容服务的预付余额。App 最终能否通过审核,仍取决于实际功能、商品说明,以及审核人员对整个交易流程的判断。
为什么不需要使用 In-App Purchase
Apple 判断应当使用哪种支付方式时,关注的是用户最终买到了什么,而不是商品被称为 “token”、“credit” 还是”积分”。
如果购买内容在 App 内消费,例如:
- 游戏金币、钻石或道具
- 会员解锁功能
- 视频、课程或电子书
- AI 调用次数
- App 内的增值功能
这类内容一般需要使用 In-App Purchase。
如果付款购买的是 App 外发生的实体商品或线下服务,例如:
- 美甲
- 按摩
- 理发
- 打车
- 外卖
- 酒店住宿
- 线下服务人员的小费
这类交易通常应使用 Apple Pay 或银行卡,而不是 In-App Purchase。
因此,用户购买余额后只能到线下合作美容院消费,与购买游戏代币并不是同一类交易。但如果 tokens 还能购买 App 内的数字功能、兑换现金、转让给其他用户,或者在多个无关场景中流通,App 审核和金融合规风险都会明显增加。
推荐的产品设计
最稳妥的方案是取消抽象的 token 定价,直接使用法定货币展示和销售具体服务:
- Salon A 美甲:¥200
- Salon B 按摩:¥400
- Salon C 理发:¥300
- 小费:¥10、¥20 或用户自定义金额
用户选择美容院和服务后,直接在 App 内通过 Apple Pay 或银行卡付款。平台后端记录订单,服务完成后再与美容院结算。
这种结构更容易向审核人员解释清楚:App 是一个线下美容服务交易平台,用户付款不会解锁任何数字内容。
如果业务确实需要预付余额,应当限制其用途:
- 余额只能支付平台合作美容院提供的线下服务
- 不得用于购买 App 内的数字内容或功能
- 用户之间不能转账
- 不得宣传为投资品、虚拟货币或可交易资产
- 不能兑换现金,退款应按照明确的规则处理
- 余额、充值记录和实际服务订单应能一一对应
- 商品页应明确说明余额的用途和限制
即使采用这些限制,预付余额仍可能受到当地储值卡、预付资金、退款、资金托管和反洗钱规定的约束。是否需要许可证,不能只根据 App Store 规则判断,应请熟悉经营地区法律的专业人士确认。
支付流程
建议使用以下交易流程:
- 用户在 App 内选择美容院和具体服务。
- App 从后端获取实时价格、币种和订单编号。
- App 展示服务内容、预约时间、取消政策和退款条件。
- 用户使用 Apple Pay 或银行卡付款。
- 支付服务商在后端确认付款结果。
- 后端将订单标记为已支付。
- 美容院完成服务后,平台再进行结算。
- 小费作为线下服务订单的一部分单独记录。
不要只根据客户端返回的”支付成功”结果发放余额或确认订单。后端应向支付服务商验证最终支付结果,最好同时处理带签名的 webhook 通知。
Apple Pay 示例
下面的代码只负责在 iOS 端发起 Apple Pay。支付凭据应发送到后端,再由后端交给支付服务商处理。
import PassKit
final class BeautyPaymentService: NSObject, PKPaymentAuthorizationControllerDelegate {
private let merchantIdentifier = "merchant.com.example.beauty"
func startPayment(
salonName: String,
serviceName: String,
amount: Decimal,
tip: Decimal
) {
guard PKPaymentAuthorizationController.canMakePayments() else {
return
}
let request = PKPaymentRequest()
request.merchantIdentifier = merchantIdentifier
request.countryCode = "CN"
request.currencyCode = "CNY"
request.merchantCapabilities = [.capability3DS]
request.supportedNetworks = [.visa, .masterCard, .chinaUnionPay]
let serviceItem = PKPaymentSummaryItem(
label: "\(salonName) - \(serviceName)",
amount: NSDecimalNumber(decimal: amount)
)
let tipItem = PKPaymentSummaryItem(
label: "小费",
amount: NSDecimalNumber(decimal: tip)
)
let total = amount + tip
let totalItem = PKPaymentSummaryItem(
label: "Beauty Platform",
amount: NSDecimalNumber(decimal: total)
)
request.paymentSummaryItems = [
serviceItem,
tipItem,
totalItem
]
let controller = PKPaymentAuthorizationController(
paymentRequest: request
)
controller.delegate = self
controller.present()
}
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didAuthorizePayment payment: PKPayment
) async -> PKPaymentAuthorizationResult {
do {
try await sendPaymentToBackend(payment)
return PKPaymentAuthorizationResult(status: .success, errors: nil)
} catch {
return PKPaymentAuthorizationResult(
status: .failure,
errors: [error]
)
}
}
func paymentAuthorizationControllerDidFinish(
_ controller: PKPaymentAuthorizationController
) {
controller.dismiss()
}
private func sendPaymentToBackend(_ payment: PKPayment) async throws {
let paymentData = payment.token.paymentData
var request = URLRequest(
url: URL(string: "https://api.example.com/payments/apple-pay")!
)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"orderId": "server-generated-order-id",
"paymentToken": paymentData.base64EncodedString()
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
throw PaymentError.authorizationFailed
}
}
}
enum PaymentError: Error {
case authorizationFailed
}
在生产环境中,orderId、金额、币种、服务内容和收款方信息都应由后端生成或验证。不能把客户端传来的金额作为最终扣款依据,否则金额可能被篡改。
Apple Pay 本身不是支付处理机构。还需要接入支持 Apple Pay 的支付服务商或收单机构,并完成 Merchant ID、证书、域名和支付处理配置。虽然这类交易不需要支付 Apple 的数字内容佣金,但仍会产生支付服务商手续费。
如果必须保留 tokens
可以在后端账户中记录 tokens 余额,但要明确限定交易用途。例如:
{
"balanceId": "bal_123",
"currency": "CNY",
"availableAmount": 60000,
"usageScope": "offline_beauty_services_only",
"transferable": false,
"cashRedeemable": false
}
金额应按最小货币单位保存,例如人民币分,避免使用浮点数:
struct ServiceOrder: Codable {
let orderId: String
let salonId: String
let serviceId: String
let amountInFen: Int
let tipInFen: Int
}
如果界面仍然显示 “1 token”,必须明确说明它对应的金额、有效期、退款方式和使用范围。不要通过调整 token 与法定货币的兑换比例来掩盖实际价格,也不要把 token 描述成可能升值的资产。
一种折中方式是保留套餐概念,同时在结算页显示真实的货币金额。例如:“美甲套餐 2 次,¥398”。付款记录和发票仍以法定货币计价。
App Store 审核说明
提交审核时,可以在 App Review Notes 中直接说明业务模式,例如:
The app facilitates payments for beauty services performed in physical salons. Payments do not unlock digital content or in-app functionality. Purchased balance can only be redeemed for in-person services such as haircuts, manicures, and massages, and cannot be transferred or exchanged for cash.
还应提供:
- 审核人员可使用的测试账号
- 完整的预约和付款操作路径
- 合作美容院及线下履约说明
- 余额使用限制
- 退款和取消政策
- 对应的服务订单示例
不要把线下服务包装成数字商品来规避审核,也不要在同一个余额中混入 App 内会员、数字课程或其他数字权益。只要该余额能够购买数字内容,Apple 就可能要求相关部分使用 In-App Purchase,甚至要求重新设计整个支付流程。
需要特别注意的事项
使用 Apple Pay 或银行卡,只解决 App Store 支付规则方面的问题,并不代表业务自动满足金融、税务和消费者保护要求。预收用户资金可能涉及:
- 储值卡或预付账户监管
- 未消费余额的退款义务
- 资金托管和商户结算
- 发票及税务处理
- 退款、拒付和欺诈风险
- 小费的归属与税务记录
- 美容院停业后的余额处理
Apple 的审核规则也可能调整。上线前应以提交审核时有效的最新版 App Review Guidelines 为准,重点核对线下商品和服务的支付条款。
优先采用”选择具体线下服务,再用 Apple Pay 或银行卡付款”的模式。只有当业务确实依赖预付余额时,才保留用途受限的 tokens,并提前准备审核说明,处理当地储值资金的合规问题。
备注:内容仅供参考。
