术语 in-app purchaseIAP

大致流程

选自 iOS快速上手应用内购(IAP)附Demo - 简书

  1. 应用发送请求到服务器,获取所有的可用Products Id集合,(当然你也可以把Products Id硬编码到程序中,这样会导致不能动态配置商品).
  2. 根据服务器返回的 Products Id 以及信息设置购买界面UI.(这里跟图一有些不同)
  3. 用户点击购买商品.
  4. 客户端根据相应的 Product Id 向 App Store 请求产品信息,并发起购买 payment.(使用StoreKit)
  5. App Store处理该 payment,并返回完成的 transaction
  6. 客户端从 transaction 中获取 receipt 凭证数据,并将其发送给服务器,等待返回.
  7. 服务器验证 receipt 凭证数据是否使用过,保存然后发给App Store验证是否合法
  8. 服务器得到 App Store 验证结果,返回给客户端相应购买成功或失败信息
  9. 客户端提示用户购买结果以及处理相应UI.

另一个流程

注意事项

  • 需要提前在 iTunes Connect 中创建项目(entry)
  • 只能为虚拟物品提供内购服务
  • 测试 IAP 需要创建“沙盒用户”(Sandbox User)

购买项目种类

  • Consumable 消费品,可重复购买
  • Non-Consumable 购买一次,永久有效
  • Non-Renewing Subscription 有有效期的订阅
  • Auto-Renewing Subscription 可重复的订阅

物品属性

  • Reference Name 在 iTunes Connect 中显示的物品昵称,该昵称不会出现在 app 中。
  • Product ID 一个唯一的字符串,用来区别 IAP 的物品。通常是由 Bundle ID + name 来定义。
  • Cleared for Sale 开启/关闭 该物品的购买服务
  • Price Tier 定价
  • Display Name 展示名称
  • Description 物品描述

创建沙盒用户

添加方式

iTunes Connect -> Users and Roles -> Sandbox Testers -> “+”

注意事项

  • 所创建的邮箱,不能与 Apple ID 相关联
  • 如果要测试 Non-Consumable 类型的购买,每次都需要创建一个新的沙盒用户,否则下次购买时,会被判定为恢复购买(restoring)
  • 测试 Consumable 时,需要重新安装应用
  • The first time you submit your app for review, you also need to submit in-app products to be reviewed at the same time. After the first submission, you can submit updates to your app and products for review independently of each other.

Tips

  • 避免重复注册邮箱:如果有gmail,可以使用“别名”(alias)来注册。比如,原有地址 abc@gmail.com,可以写成 abc+iap1@gmail.com。

配置Xcode

  1. 确认 Team 及 Bundle ID 和线上配置一样
  2. 在 Capabilities 中,打开 In-App Purchase

Show me the code

列出商品列表

注意:你无法从苹果服务器获得商品列表,必须自己维护商品标识列表(product identifiers)

通过商品的标识(product identifier)来获取商品信息(描述、标题、价格等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)fetchProducts {
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]];
// Keep a strong reference to the request.
self.request = productsRequest;
productsRequest.delegate = self;
[productsRequest start];
}
#pragma mark - Products Request Delegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
//保留商品信息
self.products = response.products;
//已失效的商品
for (NSString *invalidIdentifier in response.invalidProductIdentifiers) {
NSLog(@"无效的产品标识:%@", invalidIdentifier);
}
}

查找已购买的商品

iOS 7.0+可以使用 SKReceiptRefreshRequest

1
2
3
request = [[SKReceiptRefreshRequest alloc] init];
request.delegate = self;
[request start];

购买商品

创建一个购买请求

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
- (SKPayment *)makeAPayment:(SKProduct *)product {
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = 2;
///提供加密后的平台用户账号名,用于给苹果检查可疑的支付行为
payment.applicationUsername = [self hashedValueForAccountName:@"UserID"];
return payment;
}
//官方的hash方法实例,用于加密用户在app平台上的账号名
- (NSString *)hashedValueForAccountName:(NSString*)userAccountName
{
const int HASH_SIZE = 32;
unsigned char hashedChars[HASH_SIZE];
const char *accountName = [userAccountName UTF8String];
size_t accountNameLen = strlen(accountName);
// Confirm that the length of the user name is small enough
// to be recast when calling the hash function.
if (accountNameLen > UINT32_MAX) {
NSLog(@"Account name too long to hash: %@", userAccountName);
return nil;
}
CC_SHA256(accountName, (CC_LONG)accountNameLen, hashedChars);
// Convert the array of bytes into a string showing its hex representation.
NSMutableString *userAccountHash = [[NSMutableString alloc] init];
for (int i = 0; i < HASH_SIZE; i++) {
// Add a dash every four bytes, for readability.
if (i != 0 && i%4 == 0) {
[userAccountHash appendString:@"-"];
}
[userAccountHash appendFormat:@"%02x", hashedChars[i]];
}
return userAccountHash;
}
//将上述方法返回的payment放入支付队列
[[SKPaymentQueue defaultQueue] addPayment:payment];

等待苹果返回支付结果

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
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//增加一个支付流程观察者
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}
//观察者实现该方法
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
// Call the appropriate custom method for the transaction state.
case SKPaymentTransactionStatePurchasing:
[self showTransactionAsInProgress:transaction deferred:NO];
break;
case SKPaymentTransactionStateDeferred:
[self showTransactionAsInProgress:transaction deferred:YES];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
break;
default:
// For debugging
NSLog(@"Unexpected transaction state %@", @(transaction.transactionState));
break;
}
}
}

持久化

客户端需考虑将 receipt 凭证数据本地持久化,并加入请求失败重发机制

通过沙盒用户购买商品

使用沙盒用户购买商品时,需要退出当前登录的 iTunes & App Store 账号

服务器端验证 receipt

  1. 接收ios端发过来的购买凭证。
  2. 判断凭证是否已经存在或验证过,然后存储该凭证。
  3. 将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
  4. 如果需要,修改用户相应的会员权限。

如何验证

将该购买凭证用Base64编码,然后POST给苹果的验证服务器,苹果将验证结果以JSON形式返回。

验证地址

Always verify your receipt first with the production URL; proceed to verify with the sandbox URL if you receive a 21007 status code. Following this approach ensures that you do not have to switch between URLs while your application is being tested or reviewed in the sandbox or is live in the App Store.

虽然在审核时,我们会将地址切换到生产环境。但在验证支付收据环节上,苹果的审核人员使用的是 sandbox 环境。所以,在进行验证时,都需要遵循以下步骤:

  1. 通过生产环境URL验证
  2. 判断返回的状态码,若为 21007 ,则通过测试环境进行验证

check receipt but recommend to use in server side instead of using this function

在一段源码中,我看到了这样的提示。建议将验证收据的过程放到服务器端进行。我觉得很有道理,因为我们无法保证用户的网络环境。