Apple内购开发流程

这里介绍如何给Apple交税(30%),主要涉及财务流程和开发测试流程。

现在这里涉及的内购业务主要是对非消耗项目的单个支付流程,并且这里不涉及项目服务器端的支付流程。

线上流程

Apple ID财务流程

这一步在这里就不讲了(开发也不关心,这里公司账号已经完成)。

内购整体流程说明

内购开发官方文档

内购项目添加

以下信息要对应到具体的项目业务里面的内购内容,这里主要介绍整个开发流程,以下信息就随意填了一些。

  • 从App Store Connect后台创建内购项目。

  • 填写内购项目的具体信息,这些包括项目id,名称,价格,介绍等信息。

开发测试流程

StoreKit支付接入

根据开发文档上的支付流程,我们需要通过StoreKit发起几个支付请求(同时支付流程中的UI控制是系统来实现的)。

首先在程序部分我们采用状态结果闭包回调的方式来告诉外部运行结果:

1
2
3
4
5
enum PaymentResult<T> {
case Success(T)
case Failure(NSError)
}
typealias PaymentCompletionBlock = (_ result : PaymentResult<Any>) -> Void

接下来整个内购代码流程(不涉及自己服务器)主要实现以下几个部分:

1.根据商品ID获取商品信息
这一步过程中需要根据我们在后台配置的内购商品ID发起信息请求,并监听请求回调。

1
2
3
4
5
fileprivate func requestProducts(_ productsId : [String]) -> Void {
let request = SKProductsRequest.init(productIdentifiers: NSSet.init(array: productsId) as! Set<String>)
request.delegate = self
request.start()
}

2.根据商品信息请求支付并监听支付状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// SKProductsRequest Delegate
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if response.products.count == 0 {
print("请求的商品不存在")
if self.comletionBlock != nil {
self.comletionBlock!(PaymentResult.Failure(NSError.init(domain: "请求的商品不存在", code: PaymentErrorCode.ProductNotExist.rawValue, userInfo: nil)))
}
self.isPaying = false
return
}

print("----------- 收到内购商品反馈信息 --------------");
let products = response.products
if response.invalidProductIdentifiers.count > 0 {
print("无效的商品ID:", response.invalidProductIdentifiers)
}

var requestProduct : SKProduct? = nil
for product in products {
print("--- 请求的商品信息 ---")
print("商品标题:", product.localizedTitle)
print("商品描述信息:", product.localizedDescription)
print("价格:", product.price)
print("商品ID:", product.productIdentifier)
if self.payingProductId == product.productIdentifier {
requestProduct = product
}
}

if requestProduct == nil {
if self.comletionBlock != nil {
self.comletionBlock!(PaymentResult.Failure(NSError.init(domain: "请求的商品不存在", code: PaymentErrorCode.ProductNotExist.rawValue, userInfo: nil)))
}
self.isPaying = false
return
}

// 支付
SKPaymentQueue.default().add(SKPayment.init(product: requestProduct!))
self.payingProductId = nil;
}

3.监听支付状态,交易完成获取交易凭证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// SKPaymentTransactionObserver监听
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case SKPaymentTransactionState.purchased:
self.completeTransaction(transaction, completion: self.comletionBlock)
self.comletionBlock = nil
self.isPaying = false
break

case SKPaymentTransactionState.purchasing:
break

case SKPaymentTransactionState.restored:
self.restoreTransaction(transaction, completion: self.comletionBlock)
self.comletionBlock = nil
self.isPaying = false
break

case SKPaymentTransactionState.failed:
self.failedTransaction(transaction, completion: self.comletionBlock)
self.comletionBlock = nil
self.isPaying = false
break

default:
break
}
}
}

fileprivate func completeTransaction(_ transaction : SKPaymentTransaction?, completion : PaymentCompletionBlock?) {
self.verifyReceipt { (data, response, error) in
if transaction != nil {
SKPaymentQueue.default().finishTransaction(transaction!)
}

if error != nil {
print("校验发生错误,错误信息:", error!.localizedDescription)
if completion != nil {
completion!(PaymentResult.Failure(error! as NSError))
}
return
}

let resultDict = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! NSDictionary
print("内购校验结果:", resultDict!)

/**
* Status
* 21000 App Store无法读取你提供的JSON数据
* 21002 收据数据不符合格式
* 21003 收据无法被验证
* 21004 你提供的共享密钥和账户的共享密钥不一致
* 21005 收据服务器当前不可用
* 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
* 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
* 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
*/
if resultDict!["status"] as! NSNumber == 0 {
print("支付成功")
if completion != nil {
completion!(PaymentResult.Success(resultDict!))
}
return
}

// 切换环境再校验一遍
if resultDict!["status"] as! NSNumber == 21007 && self.verifyReceiptAddr != self.sandBoxVerifyReceiptAddr {
self.verifyReceiptAddr = self.sandBoxVerifyReceiptAddr
self.completeTransaction(nil, completion: completion)
return
}

if completion != nil {
print("支付失败")
completion!(PaymentResult.Failure(NSError.init(domain: "支付凭证校验失败", code: PaymentErrorCode.VerifyReceiptFail.rawValue, userInfo: nil)))
}
}
}

fileprivate func failedTransaction(_ transaction : SKPaymentTransaction, completion : PaymentCompletionBlock?) {
SKPaymentQueue.default().finishTransaction(transaction)
if completion != nil {
completion!(PaymentResult.Failure(transaction.error! as NSError))
}
}

4.校验交易凭证是否有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fileprivate func verifyReceipt(_ completion : @escaping (Data?, URLResponse?, Error?) -> Void) {
// 验证购买,避免越狱软件模拟苹果请求达到非法购买问题
let receiptData = NSData.init(contentsOf: Bundle.main.appStoreReceiptURL!)
let verifyReceiptUrl = NSURL.init(string: self.verifyReceiptAddr!)
let request = NSMutableURLRequest.init(url: verifyReceiptUrl! as URL, cachePolicy: NSURLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 10.0)
request.httpMethod = "POST"

let encodeStr = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithLineFeed)
let payloadData = String.init(format: "{\"receipt-data\" : \"%@\"}", encodeStr!).data(using: String.Encoding.utf8)
request.httpBody = payloadData

let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: completion)
task.resume()
}

5.对于非消耗品的恢复
这里通过监听Restore的回调函数来记录哪些非消耗品是已购的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func restoreProducts(_ completion : PaymentCompletionBlock?) -> Void {
self.comletionBlock = completion
SKPaymentQueue.default().restoreCompletedTransactions()
}

func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("恢复内购数量:", queue.transactions.count)
let purchaseIds: NSMutableArray = NSMutableArray.init()
for transaction in queue.transactions {
purchaseIds.add(transaction.original?.payment.productIdentifier as Any)
}

if self.comletionBlock != nil {
self.comletionBlock!(PaymentResult.Success(NSDictionary.init(object: purchaseIds, forKey: "restoredProdcutId" as NSCopying)))
}
self.comletionBlock = nil
}

func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
print("恢复内购失败")
if self.comletionBlock != nil {
self.comletionBlock!(PaymentResult.Failure(error as NSError))
}
self.comletionBlock = nil
}

沙盒测试

到这一步支付程序流程也开发完毕了,开始进入测试阶段,在测试之前我们需要确认几个信息。

  • 由于Apple内购算是APP Store的服务,所以我们要确认后台的APP ID和我们工程中的Bundle ID是否一致(com.xxxx.xxxx)。

  • 记得把工程中内购打开,不然StoreKit会出问题。

  • 添加沙箱测试

    这个必须是新建一个Apple ID才能加入沙箱测试(意味着其他Apple ID是没办法直接邀请进来的,创建成功后最好不要去删除了)

  • 真机调试并登入测试Apple ID
    这部分就按正常流程操作就可以了。