2026/4/18 16:29:35
网站建设
项目流程
个人网站建设推广策划书,手机销售网站制作,网站开发 开源,母婴用品商城网站建设前言
自动续订订阅(Auto-Renewable Subscriptions)是 iOS 应用最常见的变现模式之一,适用于流媒体服务、云存储、会员权益等场景。相比一次性购买,订阅模式能够为开发者提供稳定的现金流,同时也为用户提供持续更新的服务体验。
本文将从零开始,全面讲解自动续订订阅的实…前言自动续订订阅(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. 创建订阅组登录 App Store Connect选择您的 App →订阅→订阅组点击+创建新的订阅组订阅组结构示例: Premium 会员订阅组 ├── 年度会员 (com.yourapp.premium.yearly) - Level 1 ├── 季度会员 (com.yourapp.premium.quarterly) - Level 2 └── 月度会员 (com.yourapp.premium.monthly) - Level 32. 配置订阅产品对于每个订阅产品,需要配置:配置项说明示例产品 ID唯一标识符com.yourapp.premium.monthly订阅时长1周到1年1个月价格选择价格等级等级6(¥18)推介促销优惠首次订阅优惠首月免费试用促销优惠挽留/获客优惠3个月5折优惠代码自定义优惠码WELCOME20243. 设置服务器通知(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/notifications4. 获取共享密钥App Store Connect → 用户和访问 → 共享密钥共享密钥用于验证收据,请妥善保管! 示例:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6客户端实现1. 项目配置启用 In-App Purchase 能力Xcode → Project → Targets → Signing Capabilities → + Capability → In-App PurchaseStoreKit 配置文件(用于本地测试)File → New → File → StoreKit Configuration File添加订阅产品配置Scheme → Edit Scheme → Run → Options → StoreKit Configuration2. StoreKit 1 完整实现importStoreKit// MARK: - 订阅产品标识符structSubscriptionProducts{staticletmonthlyID="com.yourapp.premium.monthly"staticletquarterlyID="com.yourapp.premium.quarterly"staticletyearlyID="com.yourapp.premium.yearly"staticletallProductIDs:SetString=[monthlyID,quarterlyID,yearlyID]}// MARK: - 订阅管理器classSubscriptionManager:NSObject,ObservableObject{// MARK: - 单例staticletshared=SubscriptionManager()// MARK: - 发布属性@Publishedvarproducts:[SKProduct]=[]@PublishedvarpurchasedProductIDs:SetString=[]@PublishedvarisSubscribed:Bool=false@PublishedvarisLoading:Bool=false@PublishedvarerrorMessage:String?// MARK: - 私有属性privatevarproductsRequest:SKProductsRequest?privatevarpurchaseCompletionHandler:((ResultSKPaymentTransaction,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(ResultSKPaymentTransaction,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(ResultReceiptValidationResponse,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=expirationDate()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:{