自动续订订阅可让您持续访问应用程序中的内容、服务或高级功能。它们会在持续时间结束时自动续订,直到用户选择取消。订阅适用于 iOS、iPadOS、macOS、watchOS 和 tvOS。
出色的订阅应用程序通过提供持续价值和不断创新应用程序体验来证明定期付款是合理的。如果您正在考虑实施订阅模式,请计划定期更新您的应用程序以增强功能或扩展内容。
要提供订阅,您需要在 App Store Connect 中配置它们并在您的应用中使用 StoreKit API。您还需要将每个订阅分配给一个订阅组(一组具有不同访问级别、价格和持续时间的订阅,人们可以从中进行选择),然后添加名称、价格和描述等详细信息。此信息显示在 App Store 上您的应用程序产品页面的“应用程序内购买”部分。确保订阅适用于您的应用程序支持的所有设备类型。考虑允许订阅者在您的应用程序中查看他们的订阅状态,以及升级、交叉升级和降级选项,以及一种轻松管理或关闭他们的自动续订订阅的方法。请务必遵循我们的设计和审核指南。
准备好:
- 观看 应用内购买和订阅视频。
- 请参阅 应用内购买 StoreKit API 文档。
- 在App Store Connect 帮助中了解如何配置您的订阅 。
- 使用 App Store Server API 并启用 App Store Server Notifications 以获得订阅状态的实时变化。
在创建任何订阅之前,请确保您已完成所有这些步骤并且所有步骤都显示为活动状态。
去这里https://appstoreconnect.apple.com/agreements/#
创建订阅
转到应用程序详细信息页面,然后单击功能下的应用程序内购买链接。
选择自动续订订阅并单击创建按钮。
SwiftUI 视图模型
//
// AppStoreManager.swift
// ARSubscription
//
// Created by Smin Rana on 2/1/22.
//
import SwiftUI
import StoreKit
import Combine
class AppStoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
@Published var products = [SKProduct]()
override init() {
super.init()
SKPaymentQueue.default().add(self)
}
func getProdcut(indetifiers: [String]) {
print("Start requesting products ...")
let request = SKProductsRequest(productIdentifiers: Set(indetifiers))
request.delegate = self
request.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Did receive response")
if !response.products.isEmpty {
for fetchedProduct in response.products {
DispatchQueue.main.async {
self.products.append(fetchedProduct)
}
}
}
for invalidIdentifier in response.invalidProductIdentifiers {
print("Invalid identifiers found: \(invalidIdentifier)")
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Request did fail: \(error)")
}
// Transaction
@Published var transactionState: SKPaymentTransactionState?
func purchaseProduct(product: SKProduct) {
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
} else {
print("User can't make payment.")
}
}
struct PaymentReceiptResponseModel: Codable {
var status: Int
var email: String?
var password: String?
var message: String?
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
self.transactionState = .purchasing
case .purchased:
print("===============Purchased================")
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
// TODO: Send your receiptString to the server and verify with Apple
// receiptString should be sent to server as JSON
// {
// "receipt" : receiptString
// }
self.transactionState = .purchased // only if server sends successful response
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
case .restored:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
print("==================RESTORED State=============")
self.transactionState = .restored
case .failed, .deferred:
print("Payment Queue Error: \(String(describing: transaction.error))")
queue.finishTransaction(transaction)
self.transactionState = .failed
default:
print(">>>> something else")
queue.finishTransaction(transaction)
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("===============Restored================")
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
// TODO: Send your receiptString to the server and verify with Apple
// receiptString should be sent to server as JSON
// {
// "receipt" : receiptString
// }
self.transactionState = .purchased // only if server sends successful response
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
}
func restorePurchase() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
在模拟器上使用 .storekit 文件进行测试:
使用 PHP 和 Laravel 的服务器端:
创建特定于应用程序的共享密钥
PHP 和 Laravel
<?php
// Laravel and GuzzleHTTP\Client
function verifyReceiptWithApple() {
$input = file_get_contents('php://input');
$request = json_decode($input);
$d = $request->receipt;
$secret = 'your_app_purchase_secret';
//$url = 'https://sandbox.itunes.apple.com/verifyReceipt';
$url = 'https://buy.itunes.apple.com/verifyReceipt';
// Replace with curl if you are not using Laravel
$client = new Client([
'headers' => [ 'Content-Type' => 'application/json' ]
]);
$response = $client->post($url,
['body' => json_encode(
[
'receipt-data' => $d,
'password' => $secret,
'exclude-old-transactions' => false
]
)]
);
$json = json_decode($response->getBody()->getContents());
if ($json->status == 0) {
$email = "";
// Get original transaction id
$receipts = $json->receipt->in_app;
if (!empty($receipts) && count($receipts) > 0) {
$first_receipt = $receipts[0];
if ($first_receipt->in_app_ownership_type == "PURCHASED") {
$original_transaction_id = $first_receipt->original_transaction_id;
// Create email address with transaction id
// Create new user if not exists
$email = $original_transaction_id.'@domain.com';
$have_user = "check_with_your_database";
if (!$have_user) {
// New purchase -> user not found
} else {
// Restore purchase -> user found
}
}
}
return response()->json(["status" => 1, "message" => "Receipt is verified"]);
} else {
return response()->json(["status" => 0, "message" => "Invalid receipt"]);
}
}
包含应用内购买的内容
- 价值主张
- 呼吁采取行动
- 明确条款
- 报名
- 多层
- 登录
- 恢复
- 条款和条件
阅读更多
应用商店分发和营销:https://developer.apple.com/videos/app-store-distribution-marketing
订阅架构:https://developer.apple.com/videos/play/wwdc2020/10671/