一般来说,开发人员刚接触内购,都会遇到流程不清楚、千头万绪。
如何一次性搞定内购问题?
一、掌握内购流程:
1、完成前期准备工作
1)、接手内购,一定要阅读苹果的《APP内购买项目》文档
2)、iTunesconnect后台配置内购项目
a、未配置过内购的新项目,签署 Paid Applications agreement(《付费应用程序协议》)
b、配置内购项目,“App -->功能-->APP内购买项目”
参考:创建及发布说明
3)、Xcode工程配置
开启此选项App Store中APP的介绍界面显示内购的相关项目,关闭则不显示
4)、开发实现流程:
2、iTunesconnect创建产品
苹果的内购分以下四类商品:
1、消耗型项目
只可使用一次的产品,使用之后即失效,必须再次购买。
示例:钓鱼 App 中的鱼食。
2、非消耗型项目
只需购买一次,不会过期或随着使用而减少的产品。
示例:游戏 App 的赛道。
3、自动续期订阅
允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。
示例:每月订阅提供流媒体服务的 App。
4、非续期订阅
允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。
示例:为期一年的已归档文章目录订阅。
首先,在“我的APP”——“功能”——“App内购买项目”添加适合自己的商品:
二、开发实现:
每个开发人员帐户可在该帐户的所有 App 中创建最多 10,000 个 App 内购买项目产品。App 内购买项目共有四种类型:消耗型、非消耗型、自动续期订阅和非续期订阅。
开源库(XYIAPKit),使用此开源库直接可省去“开发实现”这一步骤。
实现步骤主要包括三步:
1、首先在项目工程中加入StoreKit.framework
2、加入头文件#import <StoreKit/StoreKit.h>
3、遵守代理SKPaymentTransactionObserver,SKProductsRequestDelegate
步骤一:App Store请求内购项
注意:此步骤建议在开始创建购买订单前完成,这样可以减少购买时查询订单的时间
1)、判断用户是否具备支付权限
if ([SKPaymentQueue canMakePayments]) {
//允许应用内付费购买
}else {
//用户禁止应用内付费购买.
}
Indicates whether the user is allowed to make payments.
An iPhone can be restricted from accessing the Apple App Store. For example, parents can restrict their children’s ability to purchase additional content. Your application should confirm that the user is allowed to authorize payments before adding a payment to the queue. Your application may also want to alter its behavior or appearance when the user is not allowed to authorize payments.
2、创建一个商品查询的请求,productIdentifiers
指需要查询的“产品ID”的数组
NSSet * set = [NSSet setWithArray:productIdentifiers];
SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
request.delegate = self;
[request start];
查询的结果将通过SKProductsRequestDelegate
得到查询的结果
获取商品的查询结果
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE_IOS(3_0);
在此回调中我们可以拿到所查询产品的信息,保存所查询的信息,以供构建购买时使用。
SKProductsResponse
// Array of SKProduct instances.
@property(nonatomic, readonly) NSArray<SKProduct *> *products NS_AVAILABLE_IOS(3_0);
// Array of invalid product identifiers.
@property(nonatomic, readonly) NSArray<NSString *> *invalidProductIdentifiers NS_AVAILABLE_IOS(3_0);
获取请求完成和失败的结果
- (void)requestDidFinish:(SKRequest *)request NS_AVAILABLE_IOS(3_0);
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error NS_AVAILABLE_IOS(3_0);
步骤二:开始构建购买请求
1)、判断请求的产品ID是否已获取到它的产品信息SKProduct
,且是否可用。
若商品可用创建支付;
若商品不可用提示用户;
若商品尚未获取它的产品信息,进行“步骤一”操作
2)、创建支付
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
3)、添加支付交易的Observer
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
注意在适当的时候移除Observer
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
可以通过遵循SKPaymentTransactionObserver
协议来监听整个交易的过程
交易状态发生改变时,包括状态的改变,交易的结束
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions NS_AVAILABLE_IOS(3_0);
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased://交易完成
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed://交易失败
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored://已经购买过该商品
[self restoreTransaction:transaction];
break;
case SKPaymentTransactionStatePurchasing: //商品添加进列表
NSLog(@"商品添加进列表");
break;
default:
break;
}
4)、校验票据
票据的校验是保证内购安全完成的非常关键的一步,一般有三种方式:
1、服务器验证,获取票据信息后上传至信任的服务器,由服务器完成与App Store的验证(提倡使用此方法,比较安全)
2、本地票据校验
3、本地App Store请求验证
a)、本地票据校验
本地票据校验的一般步骤
要验证收据,请按顺序执行以下测试:
1.找到收据。
如果没有收据,则验证失败。
2.验证收据是否由Apple 正确签署
如果收据不是由 Apple 签署,则验证失败。
3.验证收据中的Bundle Identifier(数据包标识符)与在Info.plist文件中含有您要的CFBundleIdentifier值的硬编码常量相匹配。
如果两者不匹配,则验证失败。
4.验证收据中的版本标识符字符串与在Info.plist文件中含有您要的CFBundleShortVersionString值(macOS)或
CFBundleVersion 值(iOS)的硬编码常量相匹配。
如果两者不匹配,则验证失败。
5.按照“计算 GUID 的哈希(Hash)(第 8 页)”所述计算GUID 的哈希(Hash)。
如果结果与收据中的哈希(Hash)不匹配,则验证失败。
如果通过所有测试,则验证成功。
注意: Bundle Identifier(数据包标识符)和版本标识符字符串是UTF-8 字符串,而不仅仅是一系列字节。确保您相应地编写比较逻辑代码。
如果您的 App 支持“批量购买计划”,请检查收据的有效日期。
b)、App Store验证:
1)、获取票据
- (NSString *)iapReceipt
{
NSString *receiptString = nil;
NSURL *rereceiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
if ([[NSFileManager defaultManager] fileExistsAtPath:[rereceiptURL path]]) {
NSData *receiptData = [NSData dataWithContentsOfURL:rereceiptURL];
receiptString = [receiptData base64EncodedStringWithOptions:0];
}
return receiptString;
}
如果此时未获取到票据的信息,使用SKReceiptRefreshRequest
来刷新票据结果。
SKReceiptRefreshRequest *refreshReceiptRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:@{}];
refreshReceiptRequest.delegate = self;
[refreshReceiptRequest start];
- (void)requestDidFinish:(SKRequest *)request {
}
2)、请求验证
获取到票据以后我们通过App Store来验证票据是否真实
沙盒状态下使用:https://sandbox.itunes.apple.com/verifyReceipt来验证
生产环境下使用:https://buy.itunes.apple.com/verifyReceipt
常见的验证状态代码:
由于审核时,审核人员为沙盒状态,注意状态码“21007”
- (void)verifyRequestData:(NSString *)base64Data
url:(NSString *)url
transaction:(SKPaymentTransaction *)transaction
success:(void (^)(void))successBlock
failure:(void (^)(NSError *error))failureBlock
{
NSMutableDictionary *params = [NSMutableDictionary dictionary];
[params setValue:base64Data forKey:@"receipt-data"];
[params setValue:self.sharedSecretKey forKey:@"password"];
NSError *jsonError;
NSData *josonData = [NSJSONSerialization dataWithJSONObject:params
options:NSJSONWritingPrettyPrinted
error:&jsonError];
if (jsonError) {
NSLog(@"verifyRequestData failed: error = %@", jsonError);
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
request.HTTPBody = josonData;
static NSString *requestMethod = @"POST";
request.HTTPMethod = requestMethod;
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
if (!data) {
NSError *wrapperError = [weakSelf unableVerifyReceiptError:error];
if (failureBlock != nil) failureBlock(wrapperError);
return;
}
NSError *jsonError;
NSDictionary *responseJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (!responseJSON) {
NSLog(@"Failed To Parse Server Response");
if (failureBlock != nil) failureBlock(jsonError);
}
static NSString *statusKey = @"status";
NSInteger statusCode = [responseJSON[statusKey] integerValue];
static NSInteger successCode = 0;
static NSInteger sandboxCode = 21007;
if (statusCode == successCode) {
[weakSelf saveVerifiedReceipts:transaction response:responseJSON];
if (successBlock != nil) successBlock();
} else if (statusCode == sandboxCode) {
[weakSelf sandboxVerify:base64Data
transaction:transaction
success:successBlock
failure:failureBlock];
} else {
NSLog(@"Verification Failed With Code %ld", (long)statusCode);
NSError *serverError = [NSError errorWithDomain:XYStoreErrorDomain code:statusCode userInfo:nil];
if (failureBlock != nil) failureBlock(serverError);
}
});
});
}
3)、核实票据信息
a、非订阅产品,若返回结果正确,验证通过
b、订阅产品需要验证订阅是否过期
- (BOOL)checkIsSubscribed:(XYiTunesResponse *)iTunesResponse productId:(NSString *)productId
{
NSDate *expires_date;
for (XYInAppReceipt *appReceipt in iTunesResponse.latest_receipt_info) {
if ([appReceipt.product_id isEqualToString:productId] == NO) {
continue;
}
if (expires_date) {
expires_date = [expires_date laterDate:appReceipt.expires_date];
}else {
expires_date = appReceipt.expires_date;
}
}
// 不包含过期信息表示无自动续期交易
if (!expires_date) {
return NO;
}
if (([expires_date timeIntervalSinceDate:iTunesResponse.receipt.request_date] > 0) && ([expires_date timeIntervalSinceDate:[NSDate date]] > 0)) {
// 1、对比请求时间
// 针对SKPaymentTransactionObserver的监听,当交易信息发生更新时,苹果会自动推送当前的交易状态,
// 缓存票据更新时的请求时间,通过与过期时间对比来确定用户的订阅是否过期
// 此方式可以避免用户修改系统时间造成的问题,也能保证及时的更新用户订阅状况
// 也可使用获取当前外部服务器的时间,当然需要异步操作,时间成本比较高
// 2、对比系统时间
// 防止订阅后,用户强制断网
return YES;
}
return NO;
}
@interface XYInAppReceipt : NSObject
/**
The default value is 1, the minimum value is 1, and the maximum value is 10.
*/
@property (nonatomic, assign) NSInteger quantity;
@property (nonatomic, copy) NSString *product_id;
@property (nonatomic, copy) NSString *transaction_id;
@property (nonatomic, copy) NSString *original_transaction_id;
@property (nonatomic, strong) NSDate *purchase_date;
@property (nonatomic, strong) NSDate *original_purchase_date;
/**
仅用于,自动续费订阅
true,表示处于 免费试用 时期
如果已有票据中含有is_trial_period或者is_in_intro_offer_period为true,用户不再具备有此项资格
For a subscription, whether or not it is in the free trial period.
This key is only present for auto-renewable subscription receipts. The value for this key is "true" if the customer’s subscription is currently in the free trial period, or "false" if not.
Note: If a previous subscription period in the receipt has the value “true” for either the is_trial_period or the is_in_intro_offer_period key, the user is not eligible for a free trial or introductory price within that subscription group.
*/
@property (nonatomic, assign) BOOL is_trial_period;
//**********************以上为四种内购类型公共字段,下面字段为自动续期订阅独有字段*********************
/**
This key is only present for auto-renewable subscription receipts.
Use this value to identify the date when the subscription will renew or expire, to determine if a customer should have access to content or service.
After validating the latest receipt, if the subscription expiration date for the latest renewal transaction is a past date, it is safe to assume that the subscription has expired.
*/
@property (nonatomic, strong) NSDate *expires_date;
/**
“1” - Customer canceled their subscription.
“2” - Billing error; for example customer’s payment information was no longer valid.
“3” - Customer did not agree to a recent price increase.
“4” - Product was not available for purchase at the time of renewal.
“5” - Unknown error
*/
@property (nonatomic, assign) NSInteger expiration_intent;
/**
对于订阅过期的自动续费产品,苹果是否会尝试自动续费
For an expired subscription, whether or not Apple is still attempting to automatically renew the subscription.
“1” - App Store is still attempting to renew the subscription.
“0” - App Store has stopped attempting to renew the subscription.
This key is only present for auto-renewable subscription receipts. If the customer’s subscription failed to renew because the App Store was unable to complete the transaction, this value will reflect whether or not the App Store is still trying to renew the subscription.
*/
@property (nonatomic, assign) BOOL is_in_billing_retry_period;
/**
仅用于,自动续费订阅
true,表示处于 引导价格 时期
如果已有票据中含有is_trial_period或者is_in_intro_offer_period为true,用户不再具备有此项资格
For an auto-renewable subscription, whether or not it is in the introductory price period.
This key is only present for auto-renewable subscription receipts. The value for this key is "true" if the customer’s subscription is currently in an introductory price period, or "false" if not.
Note: If a previous subscription period in the receipt has the value “true” for either the is_trial_period or the is_in_intro_offer_period key, the user is not eligible for a free trial or introductory price within that subscription group.
*/
@property (nonatomic, assign) BOOL is_in_intro_offer_period;
/**
退款操作时间
For a transaction that was canceled by Apple customer support, the time and date of the cancellation. For an auto-renewable subscription plan that was upgraded, the time and date of the upgrade transaction.
Treat a canceled receipt the same as if no purchase had ever been made.
A canceled in-app purchase remains in the receipt indefinitely. Only applicable if the refund was for a non-consumable product, an auto-renewable subscription, a non-renewing subscription, or for a free subscription.
*/
@property (nonatomic, strong) NSDate *cancellation_date;
/**
内购取消的原因
“1” - Customer canceled their transaction due to an actual or perceived issue within your app.
“0” - Transaction was canceled for another reason, for example, if the customer made the purchase accidentally.
Use this value along with the cancellation date to identify possible issues in your app that may lead customers to contact Apple customer support.
*/
@property (nonatomic, copy) NSString *cancellation_reason;
/**
APP唯一标识符
*/
@property (nonatomic, copy) NSString *app_item_id;
/**
This key is not present for receipts created in the test environment. Use this value to identify the version of the app that the customer bought
*/
@property (nonatomic, copy) NSString *version_external_identifier;
/**
This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
*/
@property (nonatomic, copy) NSString *web_order_line_item_id;
/**
The current renewal status for the auto-renewable subscription.
“1” - Subscription will renew at the end of the current subscription period.
“0” - Customer has turned off automatic renewal for their subscription.
This key is only present for auto-renewable subscription receipts, for active or expired subscriptions. The value for this key should not be interpreted as the customer’s subscription status. You can use this value to display an alternative subscription product in your app, for example, a lower level subscription plan that the customer can downgrade to from their current plan.
*/
@property (nonatomic, assign) NSInteger auto_renew_status;
/**
The current renewal preference for the auto-renewable subscription.
This key is only present for auto-renewable subscription receipts. The value for this key corresponds to the productIdentifier property of the product that the customer’s subscription renews. You can use this value to present an alternative service level to the customer before the current subscription period ends.
*/
@property (nonatomic, copy) NSString *auto_renew_product_id;
/**
The current price consent status for a subscription price increase.
“1” - Customer has agreed to the price increase. Subscription will renew at the higher price.
“0” - Customer has not taken action regarding the increased price. Subscription expires if the customer takes no action before the renewal date.
This key is only present for auto-renewable subscription receipts if the subscription price was increased without keeping the existing price for active subscribers. You can use this value to track customer adoption of the new price and take appropriate action.
*/
@property (nonatomic, assign) NSInteger price_consent_status;
*自动续期订阅
1)、自动续费
购买流程上,自动续费订阅与普通购买没有区别;
主要的区别在于:除了第一次购买行为是用户主动触发的。后续续费都是Apple自动完成的,一般在要过期的前24小时开始,苹果会尝试扣费,扣费成功的话 会在APP下次启动的时候主动推送给APP。
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
添加上面的监听就是非常必要的了。
如何处理首次订阅以及续费订阅?
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased://交易完成
// 订阅特殊处理
if(transaction.originalTransaction){
// 如果是自动续费的订单originalTransaction会有内容
}else{
// 普通购买,以及 第一次购买 自动订阅
}
break;
case SKPaymentTransactionStateFailed: //交易失败
break;
case SKPaymentTransactionStateRestored: //已经购买过该商品
break;
case SKPaymentTransactionStatePurchasing: //商品添加进列表
break;
default:
break;
}
}
}
关键点: 上面代码可以看到,通过transaction.originalTransaction
来判断是否为续费订单,通过这个知识,对之后的支付统计及付费跟踪非常关键。
2)、推介促销价(目前比较流行的套路)
主要分:随用随付、随用随付、免费试用
a、随用随付
如果您选择“随用随付”,则顾客将为选定时限的每个结算周期支付推介促销价(例如,订阅的标准价格为 9.99 美元,推介促销价为前 3 个月每月 1.99 美元)。可设定以下时限:
1 周订阅,1 至 12 周
1 个月订阅,1 至 12 个月
2 个月订阅,2、4、6、8、10 和 12 个月
3 个月订阅,3、6、9 和 12 个月
6 个月订阅,6 和 12 个月
1 年订阅,1 年
b、提前支付
如果您选择“提前支付”,顾客将一次性支付选定时限的推介促销价(例如,订阅的标准价格为 9.99 美元,推介促销价为前 2 个月 1.99 美元)。可设定以下时限:1 个月、2 个月、3 个月、6 个月或 1 年。
c、免费试用
如果您选择“免费试用”,则顾客在选定的时限内免费访问订阅。时限可以是 3 天、1 周、2 周、1 个月、2 个月、3 个月、6 个月或 1 年。
推介促销价是为回馈用户的一个促销方式,Apple推出后很受APP开发的欢迎。
当然也是非常流行的套路,我们了解一下这个地方套路的核心问题:
拿免费试用来说,用户一旦点击了免费试用后,免费试用结束后,苹果将自动进行扣款;
那么如果试用后不想购买而想退款的话,找到取消的地方就需要靠智商了,取消免费试用的路径隐藏的非常深。
四、内购的测试问题
1)、沙盒测试:非正式环境下测试需要添加沙盒测试员
您可以使用沙箱技术测试您的 App 和 App 内购买项目,而无需创建财务交易。沙箱技术是一个使用 App Store 基础架构但不处理实际付款的测试环境。它会返回交易,付款被视为已成功处理。
添加方式:
自动续费测试时需注意:
意思就是,沙箱环境 自动续费时间缩短了,一周 对应 三分钟,一月 对应 五分钟。。。
购买完一个一周 类型订阅,就不要在APP不退出的情况等待了,必须3分钟 或是 10分钟后重新登录,Apple才会主动告知你结果,也就是第一点提到的。
沙箱环境自动续费是一定会自动续费的吗?
不一定的,有时候会,有时候不会。所以要多测测,多建几个测试账号。
2)、若发现测试的商品无法进行购买,或者无法调起付费,可以检验商品是否存在,或者是否正确等问题。
五、审核问题
添加自动订阅,必须在订阅路径内需包含以下信息:
1)、隐私协议及关于App的内容
2)、暴露自动续费的信息
– Information about the auto-renewable nature of the subscription
– Links to the privacy policy and terms of use