iOS In-App Purchase 从0到1【iOS内购 Object-c版本】

最近在做一款产品,用到了In-App Purchase,对比Apple的文档研究了两天,也找了一些文章,但是没有一篇是非常详细的,因为任何一点细节不注意,里面都会有坑。所以决定写一篇详细的技术文章,希望能帮到初次做这个功能的技术朋友。这里以【订阅】作为例子介绍。

前期准备:

iTunesConnect平台配置

1、配置银行账户信息

【协议、税务和银行业务】

这部分按照页面指示填写即可,第一步是收款的银行卡信息,第二步是Tax信息,主要填写美国报税表,这里资料很多,不做赘述,可参考:https://blog.csdn.net/joinclear/article/details/107641680

2,创建内购

image

3,在内购群组中创建内购项目

image
image
image

以上,内购产品就差不多了。

接下来,创建沙盒测试账号。

4,创建沙盒测试账号,在Debug版本上,测试内购购买。

image
image

注意,单选编辑某个账号,可以设置订阅自续费的时间周期,测试期间,一般按照分钟设置。

在真机上测试时,请一定要记住,先把iPhone上appstore的账号退出登录。

至此,我们就进入代码阶段。

In-App Purchase的代码实现细节

1,引入StoreKit.framework

注意,app上的内购逻辑,不能强制用户登录后内购,否则会被审核Reject。

2,内购界面

内购界面一定要做一些具体的说明,一定要带【恢复购买】的按钮。

以我自己的app举例:

image

3,逻辑代码

1)第一步:首先启动StoreKit的回调监听

我是将内购封装到了一个单实例中,而且在

- (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions 

在这个启动函数中,第一时间进行监听,原因就是如果Apple自动扣费后,用户启动app后,会收到续费的消息通知,需要准确接收,来处理用户的到期时间 。

if ([SKPaymentQueue canMakePayments]) {      

  [[SKPaymentQueue defaultQueue] addTransactionObserver:self];

    }

2)发起购买请求

产品的id,就是在iTunesConnect中创建内购时设置的id字符串。

//发起产品查询请求

NSSet*productIds = [[NSSet alloc] initWithArray:@[MONTH_PRODUCTID]];  

SKProductsRequest* productReq = [[SKProductsRequest alloc]  initWithProductIdentifiers:productIds];

productReq.delegate=self;

[productReq start];

//产品查询回调,因为我就加了一个产品,所以我直接取第一个了。

#pragma mark SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    
    [response.products enumerateObjectsUsingBlock:^(SKProduct * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
    }];
    
    [response.invalidProductIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
    }];
    
    if (response.products.count > 0) {
        
        SKProduct *product = response.products.firstObject;
            SKMutablePayment *payment = [[SKMutablePayment alloc] init];
            payment.applicationUsername = @"xxxx可以是用户ID";
            payment.productIdentifier    = product.productIdentifier;
            payment.quantity             = 1;
            
            [[SKPaymentQueue defaultQueue] addPayment:payment];
        
    }
    else
    {
        [SVProgressHUD dismiss];
    }
}

3)支付回调

#pragma mark **SKPaymentTransactionObserver**

// 13.监听购买结果

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction {
    
    for (SKPaymentTransaction *tran in transaction){
        
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:
               // NSLog(@"交易完成");
                // 订阅特殊处理
                if (tran.originalTransaction) {
                    // 如果是自动续费的订单,originalTransaction会有内容
                    NSLog(@"自动续费的订单");
                } else {
                    // 普通购买,以及第一次购买自动订阅
                    NSLog(@"普通购买,以及第一次购买自动订阅");
                }
                
                [self completeTransaction:tran];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];// 销毁本次操作,由本地数据库进行记录和恢复
                
                //[SVProgressHUD dismiss];
                break;
            case SKPaymentTransactionStatePurchasing:
                //NSLog(@"商品添加进列表");
                break;
            case SKPaymentTransactionStateRestored:
               // NSLog(@"已经购买过商品");
                
                [self completeTransaction:tran];
                
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                
                //[SVProgressHUD dismiss];
                break;
            case SKPaymentTransactionStateFailed:
               //NSLog(@"交易失败");
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                
                [SVProgressHUD dismiss];
                break;
            default:
                [SVProgressHUD dismiss];
                break;
        }
    }
}
// 交易结束,当交易结束后还要去appstore上验证支付信息是否都正确
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    
    [self verifyFinishedTransaction:transaction];
    
}

4) 验证支付信息是否都正确
两种方式:
1,客户端自行校验
2,服务端校验
原理都是一样的,我这里用客户端校验来举例。
我们来看一下apple的文档怎么说:

image.png

注意Important的描述:
我们在用Debug版本测试也好,apple review团队在审核也好,都是采用sandbox环境,所以,apple的建议是,首先去product环境发送校验【正式的用户都会走这一步】,返回结果中的status如果等于21007【测试用户走这一步】,说明我们应该去sandbox去校验,再重新发送一次去sandbox url去校验。

上代码
代码中passwrod 就是上面在iTunesConnect上面管理的共享密钥,对于自续费的内购,密钥必须设置,否则会返回错误

#define ITMS_SANDBOX_VERIFY_RECEIPT_URL     @"https://sandbox.itunes.apple.com/verifyReceipt"
#define ITMS_PRODUCT_VERIFY_RECEIPT_URL    @"https://buy.itunes.apple.com/verifyReceipt"


#pragma mark - VerifyFinishedTransaction
-(void)verifyFinishedTransaction:(SKPaymentTransaction *)transaction{
    
    NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
    NSString* receipt = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
                    
    //NSLog(@"购买结果票据:%@",receipt);
    
    [[GoGoDB sharedDBInstance] insertNewReceipt:receipt];
    
    if(transaction.transactionState == SKPaymentTransactionStatePurchased ||
       transaction.transactionState ==  SKPaymentTransactionStateRestored){
        
        // 在app上做验证, 仅用于测试
       //passwrod 就是在iTunesConnect上面管理的共享密钥
        NSDictionary *body = @{@"receipt-data":receipt,
                               @"password":@""
        };
        
        NSError *error = nil;
        NSData *payloadData = [NSJSONSerialization dataWithJSONObject:body
                                                           options:NSJSONWritingPrettyPrinted
                                                             error: &error];
        
        if(payloadData)
        {
            //我这里用的是我自己封装的HTTP Request,请替换为你自己的HTTP request
            if(verify_request == nil)
            {
                self.verify_request = [[WebClient alloc] initWithDelegate:self];
            }
            
            verify_request._httpMethod = @"POST";
            
            //请求参数
            NSMutableDictionary *param = [NSMutableDictionary dictionary];
            [param setValue:ITMS_PRODUCT_VERIFY_RECEIPT_URL forKey:@"baseUrl"];
            [param setValue:payloadData forKey:@"Body"];
            
            verify_request._requestParam = param;
            
            IMP_BLOCK_SELF(JCVideoPlayer);
            
            [SVProgressHUD showInfoWithStatus:@"即将完成购买"];
            
            [verify_request requestWithJSONBodySusessBlock:^(id lParam, id rParam) {
                
                NSString *response = lParam;
                
                [block_self processAppleReqRes:response payload:payloadData];
                
            } FailBlock:^(id lParam, id rParam) {
                
                [block_self processAppleReqRes:nil payload:nil];
            }];
        }
        
    }
    else
    {
        [SVProgressHUD dismiss];
    }
}

- (void)processAppleReqRes:(NSString*)response payload:(NSData*)payload{
    
    [SVProgressHUD dismiss];
    
    if(response)
    {
        NSError *jsonParsingError = nil;
        NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonParsingError];
       // NSLog(@"%@", dict);
        
        int status = [[dict objectForKey:@"status"] intValue];
        //NSString *environment = [dict objectForKey:@"environment"];
        
        if(status == 0)
        {
           
            User *u = [User sharedUserData:nil];
            
            NSArray *latest_receipt_info = [dict objectForKey:@"latest_receipt_info"];
            if(latest_receipt_info && [latest_receipt_info count])
            {
                NSDictionary *latest = [latest_receipt_info firstObject];
                long long ms = [[latest objectForKey:@"expires_date_ms"] longLongValue];
                int expires_date_s = (int)(ms/1000);
                NSString *product_id = [latest objectForKey:@"product_id"];
                
                if(expires_date_s && [product_id isEqualToString:MONTH_PRODUCTID])
                {
                    u._expiretime = [self getLocalDateFormateUTCDate:expires_date_s];
                }
            }
            
            NSArray *pending_renewal_info = [dict objectForKey:@"pending_renewal_info"];
            if(pending_renewal_info && [pending_renewal_info count])
            {
                NSDictionary *latest = [latest_receipt_info firstObject];
                int  auto_renew_status = [[latest objectForKey:@"auto_renew_status"] intValue];
                NSString *product_id = [latest objectForKey:@"auto_renew_product_id"];
                
                if([product_id isEqualToString:MONTH_PRODUCTID])
                {
                    u.auto_renew_status = auto_renew_status;
                }
            }
            
            [UserDefaultsKV saveUser:u];
            
            [self refreshAfterPurchase];
        }
        else if(status == 21007)
        {
            [self verifySandboxTransaction:payload];
        }
    
        //21007
        
        
    }
}

- (void) verifySandboxTransaction:(NSData*)payloadData{
    
    if(payloadData)
    {
        self.verify_request = [[WebClient alloc] initWithDelegate:self];
        
        verify_request._httpMethod = @"POST";
        NSMutableDictionary *param = [NSMutableDictionary dictionary];
        [param setValue:ITMS_SANDBOX_VERIFY_RECEIPT_URL forKey:@"baseUrl"];
        [param setValue:payloadData forKey:@"Body"];
        
        verify_request._requestParam = param;
        
       // [SVProgressHUD show];
        IMP_BLOCK_SELF(JCVideoPlayer);
        
        [SVProgressHUD showInfoWithStatus:@"即将完成购买"];
        
        [verify_request requestWithJSONBodySusessBlock:^(id lParam, id rParam) {
            
            NSString *response = lParam;
            
            [block_self processAppleReqRes:response payload:nil];
            
        } FailBlock:^(id lParam, id rParam) {
            
            [block_self processAppleReqRes:nil payload:nil];
        }];
    }
}

- (NSString *)getLocalDateFormateUTCDate:(int )expires_date_s {
    
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:expires_date_s];
    
    NSDateFormatter *format = [[NSDateFormatter alloc] init];
    format.dateFormat = @"yyyy-MM-dd HH:mm:ss";
    format.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
    format.timeZone = [NSTimeZone localTimeZone];
    
    NSString *dateString = [format stringFromDate:date];
    return dateString;
}

对上面代码做一些说明:


image.png

因为是自动续期,所以有多个收据,
latest_receipt_info这个数组第一个就是最新的收据,
他的expires_date就是最新的过期时间,和这个判断就可以知道会员是否过期。对于验证用户是否取消订阅,时间是GMT时间,要进行转换。
pending_renewal_info , 该字段是续订状态的说明.
auto_renew_status 为0, 说明已经关闭订阅。对于退款,解析latest_receipt_info中的交易,
退款后会出现cancellation_date和cancellation_reason字段, 未退款则没有这两个字段。

至此,应该就基本能理解透彻了。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容