首页 iOS IAP内购全流程解析
文章
取消

iOS IAP内购全流程解析

1. iOS IAP时序图

IAP时序图

2. 实际操作

 1. 用户点击”Add License“

 2. iOS app调用自己服务器 ../productIds接口去获取界面上多个订阅项

 3. 自己服务器返回productIds

 4. 根据服务器返回展示单个年订阅、月订阅和无限制订阅列表给用户

 5. 用户选择一个订阅计划,点击”Subscribe Now“

 6. 通过productId调用SKProductsRequest到苹果服务器获取SKProduct

1
2
_productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:@[productId]];
_productRequest.delegate = self;

 7. 苹果服务器在代理中返回包含折扣的SKProduct

1
2
3
4
5
6
7
8
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    //response.products
    //response.invalidProductIdentifiers
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    
}

 8. 如果productId在invalidProductIdentifiers中,则跳转失败页。

 9. 调用验证折扣接口去自己服务器验证折扣是否被允许。

 10. 服务器返回验证结果。

 11. 如果服务器验证失败,则跳转失败页。

 12. 使用SKProduct和折扣创建SKMutablePayment, 添加到SKPaymentQueue中。

1
2
3
4
5
6
7
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];

SKPaymentDiscount *discount = [[SKPaymentDiscount alloc] initWithIdentifier:identifier keyIdentifier:keyIdentifier nonce:nonce signature:signature timestamp:timestamp];

payment.applicationUsername = userId;
payment.paymentDiscount = discount;
[[SKPaymentQueue defaultQueue] addPayment:payment];

 13. app将会展示系统订阅窗口,向用户展示订阅信息。

 14. 用户点击”Subscribe“去开始支付流程。

 15. 苹果在代理回调中返回支付结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    for(SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                
                break;
            case SKPaymentTransactionStateDeferred:
                break;
            case SKPaymentTransactionStatePurchased:
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            default: break;
        }
    }
}

 16. 如果苹果返回支付失败,则跳转失败页。

 17. 从沙盒得到所有收据列表数据做base64String

 18. 发送base64String到自己服务器。

 19. 自己服务器调用apple.com/verifyReceipt接口验证收据。

 20. 苹果服务器返回所有收据列表json到我们服务器。

 21. 遍历所有收据,找出originalTransitionId跟TransitionId相同的项,这些是新订阅的,然后再找到其中所有没处理过的收据,下发所有没处理过收据对应的license给用户账号。续订是苹果服务器直接跟自己服务器去交互的,客户端不需要管。

 22. 如果最后一个收据是成功的,返回成功给app,否则返回error给app。

 23. 如果返回的error,则跳转失败页。

 24. 展示成功UI给用户,用户可以看到云端下发的license。

3. 优化点

 1. 在addPayment之前可以做这样一个处理,使用iCloudToken或IDFV存Keychain来生成一个设备ID,用这个来判断用户在这个iCloud账号下或这个设备下购买过几次license了,比如用户在A账号购买了一个月度订阅,又在B账号要购买一个月度订阅,这时候同一个appleid是不支持这个操作的,就需要创建多个productId来根据购买次数返回对应的productId,这样来避免流失的用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    id token = [NSFileManager defaultManager].ubiquityIdentityToken;
    
    if (token) {
        NSData *tokenData = [NSKeyedArchiver archivedDataWithRootObject: token];
        if (tokenData && tokenData.length != 0) {
            return [tokenData base64EncodedString];
        }
    }
    
    NSString *keychainDeviceID = [KeychainManager objectForKey:@"deviceID"];
    if (keychainDeviceID) {
        return keychainDeviceID;
    }
    
    NSUUID *deviceID = [[UIDevice currentDevice] identifierForVendor];
    NSString *deviceIDString = [deviceID UUIDString];
    [KeychainManager setObject:deviceID forKey:@"deviceID"];
    return deviceIDString;

 2. 判断如果是卸载重装的app在获得网络权限后通过SKReceiptRefreshRequest来获取未下载到本地的receipt数据,然后上报给服务器,让服务器确保收到所有收据,没有漏单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@objc public func refreshReceipt(){
    let request = SKReceiptRefreshRequest(receiptProperties: nil)
    request.delegate = self
    request.start()
}

public func requestDidFinish(_ request: SKRequest) {
  // call refresh subscriptions method again with same blocks
    if request is SKReceiptRefreshRequest {
        self.sendReceiptToServer()
    }
}

public func request(_ request: SKRequest, didFailWithError error: Error){
    if request is SKReceiptRefreshRequest {
        
    }
}

 3. 支付时的验票请求需要服务器lastReceipt验票通过给客户端返回成功或失败,客户端可以据此展示成功或失败页面给用户。非支付时的验票请求是为了确保服务器一定收到了收据,防止漏单的,所以服务端不需要判断lastReceipt给客户端返回成功还是失败,只要整体收据是有效的,个人感觉一律返回成功即可。

 4.启动app后在appDelegate didFinishLaunching中就要开启支付结果监听,因为上一次如果支付后无网再次启动有监听的话也会再次调用updatedTransactions代理。

1
2
3
[[SKPaymentQueue defaultQueue] addTransactionObserver:_transactionObserver];

//第2小节第15点中需要调用finishTransaction去完成交易。

 5. didFinishLaunching需要做一下兜底上报收据逻辑,从沙盒获取收据,上报给服务器,防止之前上报失败导致的漏单现象。

1
2
3
4
5
guard let receiptURL = Bundle.main.appStoreReceiptURL else { return }
guard let receiptData = try? Data.init(contentsOf: receiptURL) else { return }
let base64String = receiptData.base64EncodedString()

//base64String上报给服务器。

 6. 对于 AppStore (iOS),有必要安装一个 Web 服务器来响应订阅状态更改的 HTTP POST 请求。

  Apple 开发者文档

 7. is-retryable字段需要服务器做一下处理,避免类似苹果支付收据还没同步给苹果验票服务器导致验票不成功的问题,此字段在21100-21199 status时将会返回true,服务器可以加个延时5秒再次请求苹果服务器进行验票,以便尽快下发订阅给用户。

  Receipt Response

4. 知识点

 1. in_app, latest_receipt_info存在返回为空的收据,苹果app分为免费app和付费app,即使是免费app下载时也会生成收据,这时候还没有应用内购买,所以in_app, latest_receipt_info会返回空,receipt中仅仅包含app购买的收据。

5. 参考文档

Apple 开发者文档

6. 有任何问题欢迎评论区留言进行探讨。

本文由作者按照 CC BY 4.0 进行授权

iOS网络层设计二-缓存方案深入

iOS StoreKit2新特性