六、<iOS IAP>内购之产品交付

购买过程的最后一部分是应用程序等待应用商店处理支付请求,存储本次购买的信息以便将来启动,下载购买的内容,然后标记交易结束,如图4-1。


图4-1

一、等待应用商店处理交易

交易队列通过商店 Kit 框架在应用和应用商店的交流过程中起着核心作用。把应用商店需要处理的工作添加到队列,比如需要被处理的支付请求。 当交易状态改变时---比如,当支付请求成功时---商店 Kit 调用应用的交易队列观察者(observer)。 需要决定哪个类作为观察者(observer)。 在很少的应用中,可以在应用委托中处理所有的商店 Kit 逻辑,包括观察交易队列。 在大多数应用中,创建单独的类来处理该观察者逻辑和其余的应用程序商店逻辑。 观察者必须遵循 SKPaymentTransactionObserver 协议。
使用观察者意味着应用程序不会不断地查询其活动交易的状态。 除了使用交易队列来处理支付请求,应用程序还使用它来下载苹果托管的内容并找出已经更新的订阅。
当应用启动时注册交易队列,如列表4-1. 确保观察者已经随时准备好处理交易,而不只在添加一个交易到队列后处理。 举个例子,考虑用户在进入一个隧道(tunnel)之前正好在应用中购买东西。 应用不能传递被购的内容,因为没有网络连接。当应用下次启动时,商店 Kit 再次调用交易队列观察者并在那时传递被购的内容。 类似地,如果应用程序处理交易失败,每次应用启动时,商店 Kit 都会调用观察者直到交易被正确地结束。
列表 4-1 注册事务交易队列观察者

- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    /* ... */

    [[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}

在交易队列观察者中实现 paymentQueue:updatedTransactions: 方法。 交易状态改变时,商店 Kit 调用该方法--- 比如,当支付请求被处理时。 交易状态告诉应用该执行什么动作,如表 4-1 和列表 4-2. 队列中的交易可以以任何顺序改变状态。 应用需要准备好在任何时候处理任何活动交易。
表 4-1 交易状态和相应的动作

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;
        }
    }
}

为了在等待时保持用户界面最新,交易队列观察者可以实现SKPaymentTransactionObserver 协议的以下可选方法:当交易被从队列中移除时,调用paymentQueueRestoreCompletedTransactionsFinished: 方法--- 在该方法的实现中,从应用的 UI 移除相应的产品。 当商店 Kit 结束恢复交易时,根据是否有 error 发生调用paymentQueueRestoreCompletedTransactionsFinished: 或 paymentQueue:restoreCompletedTransactionsFailedWithError: 方法。 在这些方法的实现中,更新应用的 UI 来反映成功或 error。

二、保留购买记录

产品有效之后,应用需要做购买的持久购买记录。 当启动时,应用使用该持久购买记录让产品变得有效。 它还使用该记录来恢复购买,正如“Restoring Purchased Products.”中所述。 应用的持久化策略取决于出售的产品类型以及 iOS 的版本。

  • iOS 7 以及之后的版本,对于非消耗产品和自动订阅,使用应用收据作为持久记录。
  • iOS 7 之前的版本,对于非消耗产品和自动订阅,使用用户默认系统或 iCloud 来保留持久记录。
  • 对于非自动订阅,使用 iCloud 或应用服务器来保留持久记录。

对于消耗产品,应用更新它的内部状态来反映购买,但是没有必要保留持久记录因为耗材产品不能恢复或不能跨设备同步。 确保被更新状态是支持状态保留(in iOS)对象的部分,或者是手动保留整个应用启动状态的对象(in iOS 或者 OS X). 关于状态保留的信息,请看iOS App Programming Guide 中的“State Preservation and Restoration”
当使用用户默认系统(User Defaults system)或 iCloud 时,应用可以存储值,可以是数字或布尔值,或者备份交易收据。 在 OS X 中,用户可以使用 defaults 命令编辑用户默认系统。 存储收据除了防止持久记录被篡改外,还要求更多的应用逻辑。
当通过 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.从苹果服务器下载托管内容

当用户购买跟苹果托管内容相关的产品时,交易被传递给交易队列观察者同时包含SKDownload 类对象,它让下载相关的内容。
要想下载内容,通过调用SKPaymentQueue类的SKPaymentQueue: 方法,从交易的download特性中把下载对象添加到交易队列。如果 downloads 属性的值为 nil, 就表示该交易没有苹果托管内容。 不像下载应用程序,当内容超出一个特定大小时,下载内容不会自动请求一个 Wi-Fi 连接。如果没有用户的明确操作避免使用蜂窝网络来下载大文件。
在交易队列观察者里实现paymentQueue:updatedDownloads: 方法来响应下载状态的改变---比如,通过在 UI 里更新进程。 如果下载失败,把 error 特性设置为该失败信息呈现给用户。
确保应用程序能优雅地处理 errors。比如,如果设备下载时磁盘空间不足,让用户选择丢弃本次下载或者在稍后当空间充足时再次恢复下载。
当使用 progress 和 timeRemaining 属性的值进行下载时,更新应用用户界面。可以在 UI 中使用 SKPaymentQueue 类的pauseDownloads:, resumeDownloads:, 和 cancelDownloads: 方法来让用户控制下载。 使用 downloadState特性来确定下载是否完成。 不要使用 download 对象的 progress timeRemaining属性来检查它的状态---这些状态用来更新应用 UI。

注意:在交易结束前下载所有的苹果托管内容。 交易完成后,它的下载对象将不能再被使用。

在 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... */ }

在 OS X, 下载的文件由系统管理;应用不能直接移动或删除它们。 要想在下载完成后定位这些内容,使用 download 对象的contentURL 属性。 要想在后续启动中定位这些文件,使用 SKDownload类的 contentURLForProductID: 类方法。要想删除文件,使用 deleteContentForProductID:类方法。 关于从应用收据读取产品识别码的更多信息,请看 Receipt Validation Programming Guide.

3.从应用服务器中下载内容

正如应用和服务器之间的所有其它交互一样,处理从应用的服务器下载内容的细节和过程机制都是开发者责任。该通信至少包括以下步骤:

  • 应用给服务器发送收据并请求内容。
  • 服务器验证收据来证明(establish)内容已经被购买,正如Receipt Validation Programming Guide中所述。
  • 假设收据有效,服务器给应用程序提供内容。请确保应用能优雅地处理 errors。 比如,如果设备在下载时空间不足,让用户选择时丢弃已经下载的内容或者在稍后等空间充足时再次恢复下载。考虑如何托管内容和应用如何跟服务器通信的安全隐患。更多信息,请看Security Overview.
五、结束交易

结束交易就是告诉商店 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: 方法。认证所有跟交易相关的工作都已经在该方法调用之前完成,该方法在每个交易中都调用,不管交易成功与否。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容