购买过程的最后一部分是应用程序等待应用商店处理支付请求,存储本次购买的信息以便将来启动,下载购买的内容,然后标记交易结束,如图4-1。
图4-1
一、等待应用商店处理交易
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
/* ... */
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}
Snip20170517_15.png
列表 4-2 相应交易状态
-(void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
// Call the appropriate custom method.
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
default:
break;
}
}
}
二、保留购买记录
- iOS 7 以及之后的版本,对于非消耗产品和自动订阅,使用应用收据作为持久记录。
- iOS 7 之前的版本,对于非消耗产品和自动订阅,使用用户默认系统或 iCloud 来保留持久记录。
- 对于非自动订阅,使用 iCloud 或应用服务器来保留持久记录。
1、使用应用收据来保留记录
应用记录包括了用户购买的记录,它由苹果公司加密签名。更多详情,请看 Receipt Validation Programming Guide.
关于消耗产品和无需更新订阅的产品信息在它们被支付后加入收据,并保留该信息直到结束这个交易。 当结束交易后,该信息将被删除,下次的收据被更新---比如,下次用户新的购买。
所有其它类型的购买信息在它们被支付时加入收据,并且收据被永久保留。
2.在用户默认系统或 iCloud 中保留数值
要想在用户默认系统或 iCloud 中保留信息,把该值设置为关键字(key)。
#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif
[storage setBool:YES forKey:@"enable_rocket_car"];
[storage setObject:@15 forKey:@"highest_unlocked_level"];
[storage synchronize];
#######A、在用户默认系统或 iCloud 中保留一个收据
要想在用户默认系统或 iCloud 中存储一个交易收据,把值设置为关键字(key)赋值给收据。
#if USE_ICLOUD_STORAGE
NSUbiquitousKeyValueStore *storage = [NSUbiquitousKeyValueStore defaultStore];
#else
NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
#endif
NSData *newReceipt = transaction.transactionReceipt;
NSArray *savedReceipts = [storage arrayForKey:@"receipts"];
if (!receipts) {
// Storing the first receipt
[storage setObject:@[newReceipt] forKey:@"receipts"];
} else {
// Adding another receipt
NSArray *updatedReceipts = [savedReceipts arrayByAddingObject:newReceipt];
[storage setObject:updatedReceipts forKey:@"receipts"];
}
[storage synchronize];
#######B、用自己的服务器保留
把收据的副本和某些凭据和识别码发送到应用的服务器,这样可以随时查看某个用户的收据。 比如,让用户使用 email 或用户名密码登陆。不要使用 UIDevice 类的 identifierForVendor 特性---不能用它来认证和恢复不同设备上同一个用户的购买记录,因为不同的设备的该特性有不同的值。
三、解锁应用功能
如果产品设计开启应用功能,给开启代码设置布尔值并根据需要更新应用界面。为了确认解锁什么功能,当交易发生时咨询应用程序做的持久记录。需要在购买完成以及应用启动时更新该布尔值。
举例子,使用应用收据,代码应该类似以下代码:
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
// Custom method to work with receipts
BOOL rocketCarEnabled = [self receipt:receiptData
includesProductID:@"com.example.rocketCar"];
或者,使用用户默认系统:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL rocketCarEnabled = [defaults boolForKey:@"enable_rocket_car"];
然后使用该信息来开启应用程序中的相应代码路径
if (rocketCarEnabled) {
// Use the rocket car.
} else {
// Use the regular car.
}
四、传递相关内容
如果产品有相关内容,应用程序需要传递该内容给用户。 比如,购买游戏中的关卡需要传递定义了该关卡的文件。在音乐应用中,购买额外的乐器需要传递那些乐器需要的声音文件。
可以把这些内容整合到应用程序 bundle 中或者根据需要下载它--每种方法都有它的优势和劣势。 如果应用 bundle 中包含了太少的内容,即使用户购买再少的内容也必须等待下载。 如果你的应用 bundle 中包含了太多的内容,应用程序的初始下载太耗时,对于那些不想购买相应产品的用户来说太浪费内存了。此外,若应用程序太大,用户将无法通过蜂窝网络(cellular networks)下载它。
在应用中嵌入少量的文件(最多几兆),特别是如果期望大多数用户可以购买该产品时。 应用 bundle 中的内容在用户购买时可以立即提供。然而,要想添加或更新应用 bundle 中的内容,必须提交到苹果商店应用程序更新的版本。
需要时下载大量的文件。把内容从应用 bundle 中分离可以让应用在初次下载时,应用消耗时间少。比如,游戏可以在应用 bundle 包含第一个关卡,并让用户在购买时下载剩余的关卡。 假设应用程序从应用服务器获取它的产品识别码列表,而不是硬性编码在应用 bundle 中,就不需要重复提交应用程序来添加或更新应用程序需要下载的内容。
在 iOS 6 和以上版本中,大多数应用程序都会使用苹果托管的内容作为下载文件。 在 Xcode 中的 In-App Purchase Content target(内置购买内容目标)来创建苹果托管的内容 Budle,并把它递交到 iTunes Connect 中。当把内容托管到苹果的服务器后,不需要在提供任何服务区---应用内容由苹果来存储,它使用相同的支持其他大型经营相同的基础设施(infrastructure),比如苹果商店。 另外,苹果托管的内容即使应用没有在运行也能自动在后台下载。
若有服务器基础设施, 需要支持 iOS 老版本,或者是跨平台共享服务器基础设施,请选择应用服务器来托管内容。
注意:不能修补应用的二进制或下载可执行代码。 当递交时,应用必须包含支持其所有功能所需的可执行代码。 如果新产品要求的代码发生了改变,请递交更新版本的应用程序。
1.加载本地内容
使用 NSBundle 类加载本地内容,就像从应用 Bundle 中加载其它资源一样。
NSURL *url = [[NSBundle mainBundle] URLForResource:@"rocketCar"
withExtension:@"plist"];
[self loadVehicleAtURL:url];
2.从苹果服务器下载托管内容
注意:在交易结束前下载所有的苹果托管内容。 交易完成后,它的下载对象将不能再被使用。
在 iOS 中,应用程序可以管理下载的文件。 文件通过商店 Kit 框架被存储在 Caches 文件夹中,它们都没有设置备份标记。 下载完成之后,应用程序负责把它们移动到恰当的位置。 对于那些可以被删除的内容,比如设备内存不足(并且稍后会由应用程序重新下载)的内容,则被留在 Caches 文件夹中。否则,把文件移动到 Documents 文件夹并给它们设置标记以防止它们从用户的备份中丢失。
列表 4-3 Excluding downloaded content from backups
NSError *error;
BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]
forKey:NSURLIsExcludedFromBackupKey
error:&error];
if (!success) { /* Handle error... */ }
3.从应用服务器中下载内容
正如应用和服务器之间的所有其它交互一样,处理从应用的服务器下载内容的细节和过程机制都是开发者责任。该通信至少包括以下步骤:
- 应用给服务器发送收据并请求内容。
- 服务器验证收据来证明(establish)内容已经被购买,正如中所述。
- 假设收据有效,服务器给应用程序提供内容。请确保应用能优雅地处理 errors。 比如,如果设备在下载时空间不足,让用户选择时丢弃已经下载的内容或者在稍后等空间充足时再次恢复下载。考虑如何托管内容和应用如何跟服务器通信的安全隐患。更多信息,请看.
五、结束交易
结束交易就是告诉商店 Kit 已经完成购买所需内容。 没有结束的交易一直保留在队列中直到它们结束,并且应用程序每次启动时调用交易队列观察者,这样应用就可以结束交易。 应用需要结束每笔交易,不管交易成功与否。
在结束交易之前完成所有以下操作:
- 保存购买记录。
- 下载相关内容。
- 更新应用程序的 UI 来让用户访问产品。
- 要想结束交易,在支付队列中调用 finishTransaction: 方法。
SKPaymentTransaction *transaction = <# The current payment #>;
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
结束了交易后,不要对那个交易做任何操作或者不要再做任何工作来传递产品。 如果有任何工作没完成,则表示应用程序还没准备好结束该交易。
注意:不要在交易真正完成之前,尝试调用 finishTransaction: 方法,在应用中尝试使用一些其它机制来跟踪未结束交易。商店 Kit 不是这么用的。这么做的话会阻止下载苹果托管内容并可能导致其它问题。
六、建议测试步骤
测试代码的每个部分来认证已经正确地实现内购。
1、 测试一个支付请求
创建一个SKPayment实例使用有效的产品标识符来测试。设置一个断点来检查(inspect)支付请求。 把支付请求添加到交易队列,并设置一个断点来确认(comfirm)观察者已经调用了 paymentQueue:updatedTransactions: 方法。
测试过程中,可以立即结束交易而不需要提供内容。 然而,即使是在测试过程中,结束交易失败也可能导致问题:未结束的交易将一直留在队列中,它可能影响以后的测试。
2、认证交易观察者代码
检查交易观察者的 SKPaymentTransactionObserver 协议的实现。 认证它可以处理交易,即使目前没有显示你的应用程序的商店 UI,即使没有在近期没有购买。
在代码中定位 SKPaymentQueue 类的 addTransactionObserver:方法的调用。 认证应用程序在应用启动时调用了该方法。
3、测试成功地交易
用测试用户账号登陆应用商店,在应用中做购买。 在交易队列观察者的 paymentQueue:updatedTransactions: 方法实现中设置一个断点,并检查交易来认证它的状态是 SKPaymentTransactionStatePurchased.
在保留购买记录代码中设置断点,并确保该代码在响应成功地购买时调用。 检查用户默认系统或者 iCloud 键值存储,并确认已经记录了正确的信息。
4、测试中断的交易
在交易队列观察者的 paymentQueue:updatedTransactions: 方法中设置一个断点,就可以控制它是否传递了产品。 然后在测试环境中像平时一样购买,用断点来暂时忽视该交易----比如,通过使用 LLDB 中的 thread return 命令从方法内立即返回。
终止和重新启动应用。商店 Kit 在启动后不久再次调用 paymentQueue:updatedTransactions: 方法;这次,让应用程序正常的响应。认证应用正确地传递了产品并完成交易。
5、认证交易已经结束
定位应用程序在哪调用了 finishTransaction: 方法。认证所有跟交易相关的工作都已经在该方法调用之前完成,该方法在每个交易中都调用,不管交易成功与否。