iOS内购IAP(八) —— 编程指南之提供产品(一)

版本记录

版本号 时间
V1.0 2018.07.28

前言

大家都知道,ios虚拟商品如宝石、金币等都需要走内购,和苹果三七分成,如果这类商品不走内购那么上不去架或者上架以后被发现而被下架。最近有一个项目需要增加内购支付功能,所以最近又重新集成并整理了下,希望对大家有所帮助。感兴趣的可以参考上面几篇。
1. iOS内购IAP(一) —— 基础配置篇(一)
2. iOS内购IAP(二) —— 工程实践(一)
3. iOS内购IAP(三) —— 编程指南之关于内购(一)
4. iOS内购IAP(四) —— 编程指南之设计您的应用程序的产品(一)
5. iOS内购IAP(五) —— 编程指南之检索产品信息(一)
6. iOS内购IAP(六) —— 编程指南之请求支付(一)
7. iOS内购IAP(七) —— 编程指南之促进应用内购买(一)

Delivering Products - 提供产品

在购买过程的最后部分,您的应用程序等待App Store处理付款请求,存储有关购买的信息以供将来启动,下载购买的内容,然后将交易标记为已完成,如图5-1所示。

Figure 5-1 Stages of the purchase process—delivering products

Waiting for the App Store to Process Transactions - 等待App Store处理交易

transaction队列在让您的应用程序通过StoreKit框架与App Store进行通信方面发挥着核心作用。您将工作添加到App Store需要处理的队列中,例如需要处理的付款请求。当transaction的状态发生变化时 - 例如,当付款请求成功时 - StoreKit会调用应用程序的transaction队列观察器。你决定哪个类作为观察者。在非常小的应用程序中,您可以处理应用程序委托中的所有StoreKit逻辑,包括观察transaction队列。在大多数apps中,您创建一个单独的类来处理此观察者逻辑以及应用程序的其余存储逻辑。观察者必须遵循SKPaymentTransactionObserver协议。

使用观察者意味着您的应用程序不会不断轮询其活动transaction的状态。除了将transaction队列用于付款请求之外,您的应用还使用transaction队列下载Apple-hosted的内容,并查明订阅已续订。

在启动应用程序时注册transaction队列观察器,如Listing 5-1所示。确保观察者随时准备好处理transaction,而不仅仅是在将transaction添加到队列之后。例如,考虑用户在进入tunnel之前在您的应用中购买商品的情况。您的应用无法提供已购买的内容,因为没有网络连接。下次启动应用程序时,StoreKit会再次调用您的transaction队列观察器并在此时传递所购买的内容。同样,如果您的应用未能将交易标记为已完成,StoreKit会在您的应用每次启动时调用观察者,直到交易正确完成。

Listing 5-1  Registering the transaction queue observer

- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    /* ... */
 
    [[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}

transaction队列观察器上实现paymentQueue:updatedTransactions:方法。 当transaction状态发生变化时,StoreKit会调用此方法 - 例如,处理付款请求时。 transaction状态告诉您应用程序需要执行的操作,如表5-1和Listing 5-2所示。 队列中的transaction可以按任何顺序更改状态。 您的应用需要随时准备好处理任何活动的交易。

Table 5-1 Transaction statuses and corresponding actions

Listing 5-2  Responding to transaction statuses

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

为了在等待时保持用户界面是最新的,transaction队列观察器可以实现SKPaymentTransactionObserver协议中的可选方法,如下所示。 在从队列中删除transaction时调用paymentQueue:removedTransactions:方法 - 在此方法的实现中,从应用程序的UI中删除相应的项目。 paymentQueueRestoreCompletedTransactionsFinished:paymentQueue:restoreCompletedTransactionsFailedWithError:方法在StoreKit完成恢复transaction时调用,具体取决于是否存在错误。 在实施这些方法时,请更新应用的UI以反映成功或错误。


Persisting the Purchase - 持续购买

在使产品可用后,您的应用需要持续记录购买情况。您的应用在启动时使用该持久性记录继续使产品可用。它还使用该记录恢复购买,如 Restoring Purchased Products中所述。您应用的持久性策略取决于您销售的产品类型和iOS版本。

  • 对于iOS 7及更高版本中的非消耗品和自动续订订阅,请使用应用收据作为持久记录。
  • 对于早于iOS 7的iOS版本中的非消耗性产品和自动续订订阅,请使用User Defaults系统或iCloud来保留持久记录。
  • 对于非续订订阅,请使用iCloud或您自己的服务器来保留持久记录。
  • 对于消耗品,您的应用程序会更新其内部状态以反映购买情况,但不需要保留持久记录,因为消耗品不会在设备之间恢复或同步。确保更新后的状态是支持状态保留的对象(在iOS中)的一部分,或者是您在应用程序启动时(在iOS或macOS中)手动保留状态的一部分。有关状态保护的信息,请参阅State Preservation and Restoration

使用User Defaults系统或iCloud时,您的应用可以存储值,例如数字或布尔值,或者交易收据的副本。在macOS中,用户可以使用defaults命令编辑User Defaults系统。存储收据需要更多的应用程序逻辑,但可以防止持久性记录被篡改。

通过iCloud保持持久性时,请注意您的应用程序的持久记录在设备之间同步,但您的应用程序负责下载其他设备上的任何关联内容。

1. Persisting Using the App Receipt - 坚持使用应用程序收据

应用程序收据包含用户购买的记录,由Apple加密签名。 有关更多信息,请参阅Receipt Validation Programming Guide

有关消耗品的信息在付款时会添加到收据中,并保留在收据中,直到您完成交易。 完成交易后,下次更新收据时将删除此信息 - 例如,下次用户进行购买时。

有关所有其他类型购买的信息将在付款时添加到收据中,并无限期地保留在收据中。

2. Persisting a Value in User Defaults or iCloud - 在User Defaults或iCloud中保留值

要在User DefaultsiCloud中存储信息,请设置该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];

3. Persisting Using Your Own Server - 坚持使用自己的服务器

将收据副本连同某种凭据或标识符一起发送到您的服务器,以便您可以跟踪哪些收据属于特定用户。 例如,让用户使用电子邮件或用户名以及密码向服务器标识自己。 不要使用UIDeviceidentifierForVendor属性 - 您不能使用它来识别和恢复同一用户在其他设备上进行的购买,因为不同的设备对此属性具有不同的值。


Unlocking App Functionality - 解锁应用程序功能

如果产品启用了应用程序功能,请设置布尔值以启用代码路径并根据需要更新用户界面。 要确定要解锁的功能,请查阅应用在事务发生时所做的持久性记录。 只要购买完成并在应用启动时,您的应用就需要更新此布尔值。

例如,使用应用收据,您的代码可能如下所示:

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"];

或者,使用User Defaults系统:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL rocketCarEnabled = [defaults boolForKey:@"enable_rocket_car"];

然后使用该信息在您的应用中启用相应的代码路径。

if (rocketCarEnabled) {
    // Use the rocket car.
} else {
    // Use the regular car.
}

Delivering Associated Content - 提供相关内容

如果产品具有关联的内容,则您的应用需要将该内容提供给用户。例如,购买游戏中的关卡需要提供定义该关卡的文件,并且在音乐应用中购买额外的音乐需要提供让用户播放这些音乐所需的声音文件。

您可以将该内容嵌入应用程序包中,也可以根据需要下载 - 每种方法都有其优点和缺点。如果您的应用包中包含的内容太少,则用户必须等待下载甚至小额购买。如果您的应用程序包中包含太多内容,则应用程序的初始下载时间过长,并且不会购买相应产品的用户浪费空间。此外,如果您的应用太大,用户将无法通过蜂窝网络下载它。

  • Embed smaller files (up to a few megabytes) in your app, especially if you expect most users to buy that product - 在您的应用中嵌入较小的文件(最多几兆字节),特别是如果您希望大多数用户购买该产品。您的应用包中的内容可以在用户购买时立即使用。但是,要在应用包中添加或更新内容,您必须提交应用的更新版本。

  • Download larger files when needed - 需要时下载更大的文件。从应用包中分离内容会使应用的初始下载量减少。例如,游戏可以在其应用程序包中包含第一级别,并允许用户在购买时下载其余级别。假设您的应用从服务器获取产品标识符列表,而不是在应用包中进行硬编码,则无需重新提交应用以添加或更新应用下载的内容。

在iOS 6及更高版本中,大多数应用程序应使用Apple-hosted的内容来下载文件。您使用Xcode中的应用内购买内容目标创建Apple-hosted的内容包,并将其提交到App Store Connect。当您在Apple的服务器上托管内容时,您不需要提供任何服务器 - Apple的应用程序内容由使用支持其他大型操作的相同基础架构(如App Store)存储。此外,即使您的应用未运行,Apple托管的内容也会在后台自动下载。

如果您已经拥有服务器基础架构,如果需要支持旧版本的iOS,或者如果您跨多个平台共享服务器基础架构,则可以选择托管自己的内容。

注意:您无法修补应用程序二进制文件或下载可执行代码。 您的应用必须包含在提交时支持其所有功能所需的所有可执行代码。 如果新产品需要更改代码,请提交应用的更新版本。

1. Loading Local Content - 加载本地内容

使用NSBundle类加载本地内容,就像从应用程序包中加载其他资源一样。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"rocketCar"
                                     withExtension:@"plist"];
[self loadVehicleAtURL:url];

2. Downloading Hosted Content from Apple’s Server - 从Apple的服务器下载托管内容

当用户购买具有关联Apple托管内容的产品时,传递给您的transaction队列观察器的事务还包括一个SKDownload实例,允许您下载相关内容。

要下载内容,请通过调用SKPaymentQueuestartDownloads:方法将事务的下载属性中的下载对象添加到事务队列。如果downloads属性的值为nil,则该事务没有Apple托管的内容。与下载应用程序不同,下载内容不会自动为大于特定大小的内容提供Wi-Fi连接。如果没有用户的明确操作,请避免使用蜂窝网络下载大型文件。

在事务队列观察器上实现paymentQueue:updatedDownloads:方法,以响应下载状态中的更改 - 例如,通过更新UI中的进度。如果下载失败,请使用其error属性中的信息向用户显示错误。

确保您的应用友好的处理错误。例如,如果设备在下载过程中磁盘空间不足,请为用户提供丢弃部分下载的选项,或者在空间可用时稍后继续下载。

使用progresstimeRemaining属性的值下载内容时更新用户界面。您可以从UI使用pauseDownloads:, resumeDownloads:,resumeDownloads:和cancelDownloads:SKPaymentQueue的方法,让用户控制正在进行的下载。使用downloadState属性确定下载是否已完成。不要使用下载对象的progresstimeRemaining属性来检查其状态 - 这些属性用于更新UI。

注意:在完成交易之前下载所有Apple托管的内容。 事务完成后,将无法再使用其下载对象。

在iOS中,您的应用可以管理下载的文件。 StoreKit框架在Caches目录中为您保存文件,并且未设置备份标志。 下载完成后,您的应用程序负责将其移至适当的位置。 对于在设备磁盘空间不足(以及稍后由应用程序重新下载)时可以删除的内容,请将文件保留在Caches目录中。 否则,将文件移动到Documents文件夹并设置标志以将其从用户备份中排除。

// Listing 5-3  Excluding downloaded content from backups

NSError *error;
BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]
                              forKey:NSURLIsExcludedFromBackupKey
                               error:&error];
if (!success) { /* Handle error... */ }

在macOS中,下载的文件由系统管理; 您的应用无法直接移动或删除它们。 要在下载后找到内容,请使用下载对象的contentURL属性。 要在后续启动时找到该文件,请使用SKDownloadcontentURLForProductID:方法。 要删除文件,请使用deleteContentForProductID:方法。 有关从应用程序收据中读取产品标识符的信息,请参阅Receipt Validation Programming Guide

3. Downloading Content from Your Own Server - 从您自己的服务器下载内容

与应用程序和服务器之间的所有其他交互一样,您自己负责从自己的服务器下载内容的过程的详细信息和机制。 通信至少包括以下步骤:

  • 您的应用会将收据发送到您的服务器并请求内容。
  • Receipt Validation Programming Guide中所述,您的服务器会验证收据以确定已购买内容。
  • 假设收据有效,您的服务器会使用内容响应您的应用。

确保您的应用正常处理错误。 例如,如果设备在下载过程中磁盘空间不足,请为用户提供丢弃部分下载的选项,或者在空间可用时稍后继续下载。

请考虑托管内容的方式以及应用程序与服务器通信的安全隐患。 有关更多信息,请参阅Security Overview


Finishing the Transaction - 完成交易

完成交易告诉StoreKit您已完成购买所需的一切。 未完成的交易在完成之前保留在队列中,并且每次启动应用程序时都会调用交易队列观察器,以便您的应用程序可以完成交易。 无论交易成功还是失败,您的应用都需要完成每笔交易。

完成交易之前,请完成以下所有操作:

  • 坚持购买。
  • 下载相关内容。
  • 更新应用的用户界面,让用户获取该产品。

要完成交易,请在支付队列上调用finishTransaction:方法。

SKPaymentTransaction *transaction = <# The current payment #>;
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

完成交易后,请勿对该交易采取任何操作或执行任何发送产品的工作。 如果仍有任何工作,您的应用尚未准备好完成交易。

注意:在交易实际完成之前,不要尝试调用finishTransaction:方法,尝试使用应用程序中的某些其他机制来跟踪未完成的交易。 StoreKit不是以这种方式使用的。 这样做可以防止您的应用下载Apple托管的内容,并可能导致其他问题。


Suggested Testing Steps - 建议的测试步骤

测试代码的每个部分以验证您是否已正确实现它。

1. Test a Payment Request - 测试付款请求

使用您已测试的有效产品标识符创建SKPayment实例。 设置断点并检查付款请求。 将付款请求添加到交易队列,并设置断点以确认调用观察者的paymentQueue:updatedTransactions:方法。

在测试期间,可以在不提供内容的情况下立即完成交易。 但是,即使在测试期间,未能完成事务也可能导致问题:未完成的交易将无限期地保留在队列中,这可能会影响以后的测试。

2. Verify Your Observer Code - 验证您的观察员代码

查看交易观察者对SKPaymentTransactionObserver协议的实现。 即使您当前没有显示应用的商店用户界面,即使您最近没有开始购买,也要验证它是否可以处理交易。

在代码中找到对SKPaymentQueueaddTransactionObserver:方法的调用。 确认您的应用在应用启动时调用此方法。

3. Test a Successful Transaction - 测试成功的交易

使用测试用户帐户登录App Store,然后在您的应用中进行购买。 在事务队列观察器的paymentQueue:updatedTransactions:方法的实现中设置断点,并检查交易以验证其状态是SKPaymentTransactionStatePurchased

在代码中持续购买的点处设置断点,并确认响应成功购买而调用此代码。 检查User DefaultsiCloud键值存储,并确认已记录正确的信息。

4. Test an Interrupted Transaction - 测试中断的交易

在交易队列观察器的paymentQueue:updatedTransactions:方法中设置断点,以便您可以控制它是否提供产品。 然后在测试环境中照常购买,并使用断点暂时忽略该交易 - 例如,通过在LLDB中使用thread return命令立即返回该方法。

终止并重新启动您的应用。 StoreKit在启动后不久再次调用paymentQueue:updatedTransactions:方法; 这一次,让你的应用程序正常响应。 验证您的应用是否正确交付产品并完成交易。

5. Verify That Transactions Are Finished - 验证交易是否完成

找到您的应用调用finishTransaction:方法的位置。 在调用方法之前验证与交易相关的所有工作是否已完成,并且为每个交易调用该方法,无论是成功还是失败。

后记

本篇主要讲述了提供产品,感兴趣的给个赞或者关注~~~~

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

推荐阅读更多精彩内容