news 2026/2/27 4:55:00

iOS In-App Purchase 自动续订订阅完整实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iOS In-App Purchase 自动续订订阅完整实现指南

前言

自动续订订阅(Auto-Renewable Subscriptions)是 iOS 应用最常见的变现模式之一,适用于流媒体服务、云存储、会员权益等场景。相比一次性购买,订阅模式能够为开发者提供稳定的现金流,同时也为用户提供持续更新的服务体验。

本文将从零开始,全面讲解自动续订订阅的实现,涵盖 App Store Connect 配置、客户端代码实现、服务端验证、状态管理等核心环节。


自动续订订阅基础概念

1. 订阅类型对比

类型特点适用场景
自动续订订阅自动扣费续订,直到用户取消视频会员、音乐服务、云存储
非续订订阅固定时长,到期不自动续订赛季通行证、限时服务
消耗型使用后消失,可重复购买游戏金币、虚拟道具
非消耗型一次购买,永久拥有去广告、功能解锁

2. 订阅生命周期

┌─────────────────────────────────────────────────────────────────────┐ │ 订阅生命周期 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 首次订阅 ──► 免费试用期 ──► 付费周期 ──► 自动续订 ──► ... │ │ │ │ │ │ │ │ │ │ │ ├──► 续订成功 ──► 继续 │ │ │ │ │ │ │ │ │ │ │ ├──► 续订失败 ──► 宽限期 │ │ │ │ │ │ │ │ │ │ │ │ │ └──► 计费重试期 │ │ │ │ │ │ │ │ │ │ │ │ │ └──► 过期 │ │ │ │ │ │ │ │ │ │ │ └──► 用户取消 ──► 到期过期 │ │ │ │ │ │ │ └───────────┴────────────┴──► 退款 ──► 立即失效 │ │ │ └─────────────────────────────────────────────────────────────────────┘

3. 关键术语解释

  • 订阅组(Subscription Group):同一组内的订阅互斥,用户只能订阅其中一个
  • 服务等级(Service Level):组内订阅的优先级,决定升降级行为
  • 宽限期(Grace Period):续订失败后,仍保留服务的宽限时间(最长16天)
  • 计费重试期(Billing Retry):Apple 尝试重新扣费的时间段(最长60天)
  • Original Transaction ID:订阅链的唯一标识,首次购买时生成

App Store Connect 配置

1. 创建订阅组

  1. 登录 App Store Connect
  2. 选择您的 App →订阅订阅组
  3. 点击+创建新的订阅组
订阅组结构示例: Premium 会员订阅组 ├── 年度会员 (com.yourapp.premium.yearly) - Level 1 ├── 季度会员 (com.yourapp.premium.quarterly) - Level 2 └── 月度会员 (com.yourapp.premium.monthly) - Level 3

2. 配置订阅产品

对于每个订阅产品,需要配置:

配置项说明示例
产品 ID唯一标识符com.yourapp.premium.monthly
订阅时长1周到1年1个月
价格选择价格等级等级6(¥18)
推介促销优惠首次订阅优惠首月免费试用
促销优惠挽留/获客优惠3个月5折
优惠代码自定义优惠码WELCOME2024

3. 设置服务器通知(Server-to-Server Notifications)

App Store Connect → 应用 → App 信息 → App Store Server Notifications

配置 V2 通知端点:

生产环境 URL: https://api.yourapp.com/apple/notifications 沙盒环境 URL: https://api-sandbox.yourapp.com/apple/notifications

4. 获取共享密钥

App Store Connect → 用户和访问 → 共享密钥

共享密钥用于验证收据,请妥善保管! 示例:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

客户端实现

1. 项目配置

启用 In-App Purchase 能力
Xcode → Project → Targets → Signing & Capabilities → + Capability → In-App Purchase
StoreKit 配置文件(用于本地测试)
  1. File → New → File → StoreKit Configuration File
  2. 添加订阅产品配置
  3. Scheme → Edit Scheme → Run → Options → StoreKit Configuration

2. StoreKit 1 完整实现

importStoreKit// MARK: - 订阅产品标识符structSubscriptionProducts{staticletmonthlyID="com.yourapp.premium.monthly"staticletquarterlyID="com.yourapp.premium.quarterly"staticletyearlyID="com.yourapp.premium.yearly"staticletallProductIDs:Set<String>=[monthlyID,quarterlyID,yearlyID]}// MARK: - 订阅管理器classSubscriptionManager:NSObject,ObservableObject{// MARK: - 单例staticletshared=SubscriptionManager()// MARK: - 发布属性@Publishedvarproducts:[SKProduct]=[]@PublishedvarpurchasedProductIDs:Set<String>=[]@PublishedvarisSubscribed:Bool=false@PublishedvarisLoading:Bool=false@PublishedvarerrorMessage:String?// MARK: - 私有属性privatevarproductsRequest:SKProductsRequest?privatevarpurchaseCompletionHandler:((Result<SKPaymentTransaction,Error>)->Void)?privatevarrestoreCompletionHandler:((Result<[SKPaymentTransaction],Error>)->Void)?// MARK: - 初始化privateoverrideinit(){super.init()startObservingPaymentQueue()}deinit{stopObservingPaymentQueue()}// MARK: - 支付队列观察funcstartObservingPaymentQueue(){SKPaymentQueue.default().add(self)}funcstopObservingPaymentQueue(){SKPaymentQueue.default().remove(self)}// MARK: - 请求产品信息funcfetchProducts(){guard!isLoadingelse{return}isLoading=trueerrorMessage=nilletrequest=SKProductsRequest(productIdentifiers:SubscriptionProducts.allProductIDs)request.delegate=selfrequest.start()productsRequest=requestprint("🛒 开始请求产品信息...")}// MARK: - 购买订阅funcpurchase(_product:SKProduct,completion:@escaping(Result<SKPaymentTransaction,Error>)->Void){guardSKPaymentQueue.canMakePayments()else{completion(.failure(SubscriptionError.paymentsNotAllowed))return}purchaseCompletionHandler=completion isLoading=trueletpayment=SKPayment(product:product)SKPaymentQueue.default().add(payment)print("💳 发起购买:\(product.productIdentifier)")}// MARK: - 恢复购买funcrestorePurchases(completion:@escaping(Result<[SKPaymentTransaction],Error>)->Void){restoreCompletionHandler=completion isLoading=trueSKPaymentQueue.default().restoreCompletedTransactions()print("🔄 开始恢复购买...")}// MARK: - 验证收据funcvalidateReceipt(completion:@escaping(Result<ReceiptValidationResponse,Error>)->Void){guardletreceiptURL=Bundle.main.appStoreReceiptURL,FileManager.default.fileExists(atPath:receiptURL.path),letreceiptData=try?Data(contentsOf:receiptURL)else{completion(.failure(SubscriptionError.noReceiptFound))return}letreceiptString=receiptData.base64EncodedString()// 发送到您的服务器进行验证ReceiptValidator.validate(receipt:receiptString){resultinDispatchQueue.main.async{switchresult{case.success(letresponse):self.processValidationResponse(response)completion(.success(response))case.failure(leterror):completion(.failure(error))}}}}// MARK: - 处理验证响应privatefuncprocessValidationResponse(_response:ReceiptValidationResponse){guardletlatestReceipt=response.latestReceiptInfo?.firstelse{isSubscribed=falsereturn}// 检查订阅是否有效ifletexpiresDateMs=latestReceipt.expiresDateMs,letexpiresDate=Double(expiresDateMs){letexpiration=Date(timeIntervalSince1970:expiresDate/1000)isSubscribed=expiration>Date()ifisSubscribed{purchasedProductIDs.insert(latestReceipt.productId)}}}// MARK: - 获取格式化价格funcformattedPrice(forproduct:SKProduct)->String{letformatter=NumberFormatter()formatter.numberStyle=.currency formatter.locale=product.priceLocalereturnformatter.string(from:product.price)??"\(product.price)"}// MARK: - 获取订阅周期描述funcsubscriptionPeriodDescription(forproduct:SKProduct)->String{guardletperiod=product.subscriptionPeriodelse{return""}letunit:Stringswitchperiod.unit{case.day:unit=period.numberOfUnits==1?"天":"\(period.numberOfUnits)天"case.week:unit=period.numberOfUnits==1?"周":"\(period.numberOfUnits)周"case.month:unit=period.numberOfUnits==1?"月":"\(period.numberOfUnits)个月"case.year:unit=period.numberOfUnits==1?"年":"\(period.numberOfUnits)年"@unknowndefault:unit=""}returnunit}// MARK: - 获取免费试用描述funcfreeTrialDescription(forproduct:SKProduct)->String?{guardletintroPrice=product.introductoryPrice,introPrice.paymentMode==.freeTrialelse{returnnil}letperiod=introPrice.subscriptionPeriodletunit:Stringswitchperiod.unit{case.day:unit="\(period.numberOfUnits)天"case.week:unit="\(period.numberOfUnits)周"case.month:unit="\(period.numberOfUnits)个月"case.year:unit="\(period.numberOfUnits)年"@unknowndefault:returnnil}return"免费试用\(unit)"}}// MARK: - SKProductsRequestDelegateextensionSubscriptionManager:SKProductsRequestDelegate{funcproductsRequest(_request:SKProductsRequest,didReceive response:SKProductsResponse){DispatchQueue.main.async{self.isLoading=falseself.products=response.products.sorted{$0.price.compare($1.price)==.orderedAscending}print("✅ 获取到\(response.products.count)个产品")if!response.invalidProductIdentifiers.isEmpty{print("⚠️ 无效产品ID:\(response.invalidProductIdentifiers)")}}}funcrequest(_request:SKRequest,didFailWithError error:Error){DispatchQueue.main.async{self.isLoading=falseself.errorMessage=error.localizedDescriptionprint("❌ 请求产品失败:\(error.localizedDescription)")}}}// MARK: - SKPaymentTransactionObserverextensionSubscriptionManager:SKPaymentTransactionObserver{funcpaymentQueue(_queue:SKPaymentQueue,updatedTransactions transactions:[SKPaymentTransaction]){fortransactionintransactions{switchtransaction.transactionState{case.purchasing:print("🔄 购买中:\(transaction.payment.productIdentifier)")case.purchased:print("✅ 购买成功:\(transaction.payment.productIdentifier)")handlePurchased(transaction)case.failed:print("❌ 购买失败:\(transaction.error?.localizedDescription??"未知错误")")handleFailed(transaction)case.restored:print("🔄 恢复成功:\(transaction.payment.productIdentifier)")handleRestored(transaction)case.deferred:print("⏸ 购买延迟(等待审批):\(transaction.payment.productIdentifier)")handleDeferred(transaction)@unknowndefault:print("⚠️ 未知交易状态")}}}funcpaymentQueueRestoreCompletedTransactionsFinished(_queue:SKPaymentQueue){DispatchQueue.main.async{self.isLoading=falseprint("✅ 恢复购买完成")letrestoredTransactions=queue.transactions.filter{$0.transactionState==.restored}self.restoreCompletionHandler?(.success(restoredTransactions))self.restoreCompletionHandler=nil}}funcpaymentQueue(_queue:SKPaymentQueue,restoreCompletedTransactionsFailedWithError error:Error){DispatchQueue.main.async{self.isLoading=falseself.errorMessage=error.localizedDescriptionprint("❌ 恢复购买失败:\(error.localizedDescription)")self.restoreCompletionHandler?(.failure(error))self.restoreCompletionHandler=nil}}// MARK: - 处理购买成功privatefunchandlePurchased(_transaction:SKPaymentTransaction){// 验证收据validateReceipt{[weakself]resultinswitchresult{case.success:self?.purchaseCompletionHandler?(.success(transaction))case.failure(leterror):self?.purchaseCompletionHandler?(.failure(error))}self?.purchaseCompletionHandler=nilself?.isLoading=false}// 完成交易SKPaymentQueue.default().finishTransaction(transaction)}// MARK: - 处理购买失败privatefunchandleFailed(_transaction:SKPaymentTransaction){DispatchQueue.main.async{self.isLoading=falseifleterror=transaction.erroras?SKError{switcherror.code{case.paymentCancelled:self.purchaseCompletionHandler?(.failure(SubscriptionError.paymentCancelled))default:self.purchaseCompletionHandler?(.failure(error))}}self.purchaseCompletionHandler=nil}SKPaymentQueue.default().finishTransaction(transaction)}// MARK: - 处理恢复privatefunchandleRestored(_transaction:SKPaymentTransaction){purchasedProductIDs.insert(transaction.payment.productIdentifier)SKPaymentQueue.default().finishTransaction(transaction)}// MARK: - 处理延迟privatefunchandleDeferred(_transaction:SKPaymentTransaction){DispatchQueue.main.async{self.isLoading=falseself.purchaseCompletionHandler?(.failure(SubscriptionError.paymentDeferred))self.purchaseCompletionHandler=nil}}}// MARK: - 错误定义enumSubscriptionError:LocalizedError{casepaymentsNotAllowedcasepaymentCancelledcasepaymentDeferredcasenoReceiptFoundcaseinvalidReceiptcaseserverErrorvarerrorDescription:String?{switchself{case.paymentsNotAllowed:return"当前设备不允许应用内购买"case.paymentCancelled:return"购买已取消"case.paymentDeferred:return"购买需要授权,请等待审批"case.noReceiptFound:return"未找到购买凭证"case.invalidReceipt:return"购买凭证无效"case.serverError:return"服务器验证失败"}}}// MARK: - 收据验证响应模型structReceiptValidationResponse:Codable{letstatus:IntletlatestReceiptInfo:[LatestReceiptInfo]?letpendingRenewalInfo:[PendingRenewalInfo]?enumCodingKeys:String,CodingKey{casestatuscaselatestReceiptInfo="latest_receipt_info"casependingRenewalInfo="pending_renewal_info"}}structLatestReceiptInfo:Codable{letproductId:StringlettransactionId:StringletoriginalTransactionId:StringletpurchaseDateMs:StringletexpiresDateMs:String?letisTrialPeriod:String?letisInIntroOfferPeriod:String?enumCodingKeys:String,CodingKey{caseproductId="product_id"casetransactionId="transaction_id"caseoriginalTransactionId="original_transaction_id"casepurchaseDateMs="purchase_date_ms"caseexpiresDateMs="expires_date_ms"caseisTrialPeriod="is_trial_period"caseisInIntroOfferPeriod="is_in_intro_offer_period"}}structPendingRenewalInfo:Codable{letautoRenewProductId:StringletautoRenewStatus:StringletexpirationIntent:String?letgracePeriodExpiresDateMs:String?enumCodingKeys:String,CodingKey{caseautoRenewProductId="auto_renew_product_id"caseautoRenewStatus="auto_renew_status"caseexpirationIntent="expiration_intent"casegracePeriodExpiresDateMs="grace_period_expires_date_ms"}}

3. 订阅界面实现(SwiftUI)

importSwiftUIstructSubscriptionView:View{@StateObjectprivatevarsubscriptionManager=SubscriptionManager.shared @StateprivatevarselectedProduct:SKProduct?@StateprivatevarshowAlert=false@StateprivatevaralertMessage=""@Environment(\.dismiss)privatevardismissvarbody:someView{NavigationView{ScrollView{VStack(spacing:24){// 头部headerSection// 功能特性featuresSection// 订阅选项subscriptionOptionsSection// 订阅按钮subscribeButton// 恢复购买restoreButton// 法律条款legalSection}.padding()}.navigationTitle("升级会员").navigationBarTitleDisplayMode(.inline).toolbar{ToolbarItem(placement:.navigationBarTrailing){Button("关闭"){dismiss()}}}}.onAppear{subscriptionManager.fetchProducts()}.alert("提示",isPresented:$showAlert){Button("确定",role:.cancel){}}message:{
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/26 23:20:46

【医疗信息合规导出】:基于PHP的PDF与XML加密导出技术揭秘

第一章&#xff1a;医疗数据PHP导出格式概述在医疗信息系统开发中&#xff0c;数据导出功能是实现信息共享、统计分析和合规上报的关键环节。PHP作为广泛应用的服务器端脚本语言&#xff0c;常被用于构建医疗数据管理平台的后端服务。导出的数据格式需满足可读性、兼容性和结构…

作者头像 李华
网站建设 2026/2/27 7:16:12

你还在为Rust-PHP扩展报错崩溃?:3种高效解决方案立即上手

第一章&#xff1a;Rust-PHP 扩展的版本适配在构建基于 Rust 编写的 PHP 扩展时&#xff0c;版本兼容性是确保扩展稳定运行的关键因素。PHP 的内部 API 随版本迭代频繁变化&#xff0c;而 Rust 通过 php-rs 或 ext-php-rs 等绑定库与 Zend 引擎交互&#xff0c;因此必须精确匹配…

作者头像 李华
网站建设 2026/2/26 12:41:26

仅限高级开发者:PHP 8.6扩展开发文档未公开的7个核心结构体

第一章&#xff1a;PHP 8.6 扩展开发概览 PHP 8.6 作为 PHP 语言持续演进的重要版本&#xff0c;进一步优化了扩展开发的接口稳定性与性能表现。该版本在延续 Zend 引擎高效特性的基础上&#xff0c;引入了更清晰的扩展注册机制和增强的类型支持&#xff0c;使 C 语言编写的原生…

作者头像 李华
网站建设 2026/2/7 0:23:33

多传感器信息融合,卡尔曼滤波算法的轨迹跟踪与估计 AEKF——自适应扩展卡尔曼滤波算法

多传感器信息融合&#xff0c;卡尔曼滤波算法的轨迹跟踪与估计AEKF——自适应扩展卡尔曼滤波算法 AUKF——自适应无迹卡尔曼滤波算法 UKF——无迹卡尔曼滤波算法 三种不同的算法实现轨迹跟踪轨迹跟踪这活儿听起来高端&#xff0c;实际干起来全是坑。传感器数据像一群不听话的…

作者头像 李华
网站建设 2026/2/26 19:54:55

【NGS数据质控黄金法则】:10个R语言关键步骤确保分析可靠性

第一章&#xff1a;NGS数据质控的核心意义与R语言优势高通量测序&#xff08;NGS&#xff09;技术的迅猛发展为基因组学研究提供了前所未有的数据规模&#xff0c;但原始测序数据中常包含接头污染、低质量碱基和PCR重复等问题&#xff0c;直接影响后续分析的准确性。因此&#…

作者头像 李华
网站建设 2026/2/26 9:18:07

boost获取dll导出函数调用(C++源码)

1、概述 boost获取dll导出函数并调用,4个步骤。 1、包含头文件 2、加载dll 3、获取函数地址 4、调用函数 与windows 的GetProcessAdress方式相比,感觉boost更麻烦一点,于是用ai搜索了下区别,我觉得其中一个好处就是支持跨平台吧。 由于boost::dll::shared_library::get&…

作者头像 李华