iOS-自动订阅
自动订阅做完,对此做一个记录,关于开发者中心的一系列信息,此处不做详细解释,填的东西确实比较多,可以参考集成帮助.
记得添加共享密钥,否则购买会验证失败,如下图:
共享密钥.png
针对无登录用户的App,我是将他buy成功的信息保存在了UserDefaul里,如果将应用删除,下次登录需要点击恢复才可以继续使用会员服务。这种方法还是会有漏洞,无奈现在没什么好办法,有想到的麻烦告知一下。
下面直接上代码
BuyVipPublic.h
//
// BuyVipPublic.h
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef enum {
kIAPPurchSuccess = 0, // 成功
kIAPPurchFailed = 1, // 失败
kIAPPurchCancle = 2, // 购买
KIAPPurchVerFailed = 3, // 校验失败
KIAPPurchVerSuccess = 4, // 校验成功
kIAPPurchNotArrow = 5, // 不允许
}IAPPurchType;
typedef void (^IAPCompletionHandle)(IAPPurchType type,NSData *data);
@interface BuyVipPublic : NSObject
// buy
- (void)startPurchWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle;
// 恢复
- (void)restore;
@end
NS_ASSUME_NONNULL_END
BuyVipPublic.m
//
// BuyVipPublic.m
//
#import <StoreKit/StoreKit.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <AdSupport/AdSupport.h>
#import "BuyVipPublic.h"
@interface BuyVipPublic()<SKPaymentTransactionObserver,SKProductsRequestDelegate>
@property (nonatomic,copy) NSString *purchID;
@property (nonnull,strong) IAPCompletionHandle handle;
// 恢复
@property (nonatomic,strong) NSMutableArray *resumeId;
@end
@implementation BuyVipPublic
#pragma mark - system lifecycle
- (instancetype)init{
self = [super init];
if (self) {
// 监听写在程序入口,程序挂起时移除监听,这样如果有未完成的订单将会自动执行并回调 paymentQueue:updatedTransactions:方法
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
#pragma mark - Public Method
- (void)startPurchWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle{
if (purchID) {
if ([SKPaymentQueue canMakePayments]) {
// buy
self.purchID = purchID;
self.handle = handle;
NSSet *nsset = [NSSet setWithArray:@[purchID]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}else{
[self handleActionWithType:kIAPPurchNotArrow data:nil];
}
}
}
#pragma mark - Private Method
- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
switch (type) {
case kIAPPurchSuccess:
SSLog(@"成功");
break;
case kIAPPurchFailed:
SSLog(@"失败");
break;
case kIAPPurchCancle:
SSLog(@"用户取消");
break;
case KIAPPurchVerFailed:
SSLog(@"订单校验失败");
break;
case KIAPPurchVerSuccess:
SSLog(@"校验成功");
break;
case kIAPPurchNotArrow:
SSLog(@"不允许程序内buy");
break;
default:
break;
}
if(self.handle){
self.handle(type,data);
}
}
// 结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
[self verifyPurchaseWithPaymentTransaction:transaction isTestServer:NO];
}
// 失败
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
if (transaction.error.code != SKErrorPaymentCancelled) {
[self handleActionWithType:kIAPPurchFailed data:nil];
}else{
[self handleActionWithType:kIAPPurchCancle data:nil];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction isTestServer:(BOOL)flag{
// 验证凭据,获取返回的交易凭据
NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
if(!receipt){
// 凭证为空验证失败
[self handleActionWithType:KIAPPurchVerFailed data:nil];
return;
}
// buy成功将交易凭证发送给服务端进行再次校验
// [self handleActionWithType:kIAPPurchSuccess data:receipt];
NSError *error;
NSDictionary *requestContents = @{ @"receipt-data": [receipt base64EncodedStringWithOptions:0] ,@"password":@"这里填写的是开发者中心上创建的共享密钥"};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
if (!requestData) { // 交易凭证为空验证失败
[self handleActionWithType:KIAPPurchVerFailed data:nil];
return;
}
//In the test environment, use https://sandbox.itunes.apple.com/verifyReceipt
//In the real environment, use https://buy.itunes.apple.com/verifyReceipt
NSString *serverString = @"https://buy.itunes.apple.com/verifyReceipt";
if (flag) {
serverString = @"https://sandbox.itunes.apple.com/verifyReceipt";
}
NSURL *storeURL = [NSURL URLWithString:serverString];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
// 无法连接服务器,校验失败
[self handleActionWithType:KIAPPurchVerFailed data:nil];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
SSLog(@"jsonResponse = %@",jsonResponse);
if (!jsonResponse) {
// 苹果服务器校验数据返回为空校验失败
[self handleActionWithType:KIAPPurchVerFailed data:nil];
}
//先验证正式服务器,如果正式服务器返回21007再去苹果测试服务器验证,沙盒测试环境苹果用的是测试服务器
NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];
if (status && [status isEqualToString:@"21007"]) {
[self verifyPurchaseWithPaymentTransaction:transaction isTestServer:YES];
}else if(status && [status isEqualToString:@"0"]){
NSData *data = [NSJSONSerialization dataWithJSONObject:jsonResponse options:NSJSONWritingPrettyPrinted error:nil];
[self handleActionWithType:KIAPPurchVerSuccess data:data];
}
SSLog(@"----验证结果 %@",jsonResponse);
}
}];
// 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] <= 0){
SSLog(@"--------------没有商品------------------");
return;
}
SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:self.purchID]){
p = pro;
break;
}
}
SSLog(@"productID:%@", response.invalidProductIdentifiers);
SSLog(@"产品数量:%lu",(unsigned long)[product count]);
SSLog(@"%@",[p description]);
SSLog(@"%@",[p localizedTitle]);
SSLog(@"%@",[p localizedDescription]);
SSLog(@"%@",[p price]);
SSLog(@"%@",[p productIdentifier]);
SSLog(@"发送请求");
SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
SSLog(@"------------------错误-----------------:%@", error);
[SVPShow showFailureWithMessage:[error localizedDescription]];
}
- (void)requestDidFinish:(SKRequest *)request{
SSLog(@"------------反馈信息结束-----------------");
}
- (void) PurchasedTransaction: (SKPaymentTransaction *)transaction
{
SSLog(@"-----PurchasedTransaction----");
NSArray *transactions =[[NSArray alloc] initWithObjects:transaction, nil];
[self paymentQueue:[SKPaymentQueue defaultQueue] updatedTransactions:transactions];
}
#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
for (SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
SSLog(@"transactionIdentifier = %@",tran.transactionIdentifier);
if (tran.originalTransaction) {
// 如果是自动续费的订单,originalTransaction会有内容
SSLog(@"自动续费的订单,originalTransaction = %@",tran.originalTransaction);
} else {
// 普通购买,以及第一次购买自动订阅
SSLog(@"普通购买,以及第一次购买自动订阅");
}
[self completeTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
SSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:
SSLog(@"已经购买过商品");
// 消耗型不支持恢复购买
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
//存入数组同步并发送通知
[self userDefaultSave:@"YES"];
[SVPShow disMiss];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:tran];
break;
default:
break;
}
}
}
// 恢复购买
- (void)restore
{
[SVPShow show];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
// 恢复失败
SSLog(@"恢复失败");
[SVPShow showFailureWithMessage:@"Restore Purchases fail"];
}
-(void)userDefaultSave:(NSString *)saveData
{
//存入数组并同步
[[NSUserDefaults standardUserDefaults] setObject:saveData forKey:@"buyResult"];
[[NSUserDefaults standardUserDefaults] synchronize];
// 发送通知
[KNotification postNotificationName:@"ConnectViewController" object:nil userInfo:nil];
}
@end
具体使用
#define QH_LAZY(object, assignment) (object = object ?: assignment)
@property (nonatomic,strong) BuyVipPublic *iapManager;
///初始化
-(BuyVipPublic *)iapManager{
return QH_LAZY(_iapManager, ({
BuyVipPublic *iap = [[BuyVipPublic alloc]init];
iap;
}));
}
#pragma mark -- 订阅、内购
- (IBAction)subsrcribeAction:(UIButton *)sender {
SSLog(@"订阅");
// 这里的StoreID是开发者中心申请下来的商品id
self.productID = StoreID;
[SVPShow show];
if ([SKPaymentQueue canMakePayments]) {
// 如果允许应用内付费购买
[self byAction:self.productID];
} else {
// 用户不允许应用内购买,弹出提示
[self showMessage];
[SVPShow disMiss];
}
}
-(void)showMessage
{
NSString *message = @"In app purchase is not allowed ,Please open Settings - > screen usage time - > content and privacy access restrictions - > purchased items in iTunes Store and App Store - > purchased items in App - > allow";
UIAlertController *alt = [UIAlertController alertControllerWithTitle:@"Remind" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleDefault handler:nil];
[alt addAction:okAction];
[self presentViewController:alt animated:YES completion:nil];
}
// 购买
-(void)byAction:(NSString *)plyID
{
///使用
//点击购买按钮后,先从后台获取订单号(order_id),然后再发起内购.
//plyID 为 产品ID,之前在创建的时候自定义的
[self.iapManager startPurchWithID:plyID completeHandle:^(IAPPurchType type,NSData *data) {
SSLog(@"data --- %@",data);
if (type == kIAPPurchSuccess) { // 购买成功
if (data) {
//返回数据
SSLog(@"购买成功,正在验证凭证...");
[SVPShow showSuccessWithMessage:@"Purchase succeeded, verifying credentials..."];
}
}else if(type == kIAPPurchCancle){
[SVPShow showInfoWithMessage:@"Cancel purchase"];
[self userDefaultSave:@"YES"];
}else if(type == kIAPPurchNotArrow){
[SVPShow showFailureWithMessage:@"In app purchase is not allowed"];
}else if(type == kIAPPurchFailed){
[SVPShow showFailureWithMessage:@"Purchase failed"];
[self userDefaultSave:@"NO"];
}
if (type == KIAPPurchVerSuccess) {
SSLog(@"//订单第二次校验成功");
///购买凭证
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
SSLog(@"%@",jsonDict);
NSArray *latestArray = jsonDict[@"latest_receipt_info"];
if (latestArray.count > 0)
{
// 过期时间
NSInteger dateIn = [SafeString(latestArray[0][@"purchase_date_ms"]) integerValue];
NSDate *date = [[NSDate alloc]initWithTimeIntervalSince1970:dateIn/1000];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyy-MM-dd";
NSString *dateStr = [dateFormatter stringFromDate:date];
[KeychainTool addKeychainData:dateStr forKey:@"ExpirationTime"];
}
// 将购买结果存到本地
[self userDefaultSave:@"YES"];
[SVPShow disMiss];
}
}];
}
-(void)userDefaultSave:(NSString *)saveData
{
//存入数组并同步
[[NSUserDefaults standardUserDefaults] setObject:saveData forKey:@"buyResult"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
// 恢复订阅
- (IBAction)restoreAction:(UIButton *)sender {
[self.iapManager restore];
SSLog(@"恢复订阅");
}
- 审核碰到的问题
- 大概意思是说app的名称是free,里面却有订阅,需要改一下名称
Guideline 2.3.7 - Performance - Accurate Metadata
Your app name to be displayed on the App Store include references to the price of your app or the service it provides, which is not considered a part of these metadata items
Next Steps
To resolve this issue, please remove any references to pricing from your app’s name, including any references to your app or service being free or discounted. If you would like to advertise changes to your app’s price, consider including this information in the app description. Changes to your app’s price can be made in the Pricing and Availability section of App Store Connect.
Resources
For information on how to revise your app name, please review Renaming a Project or App.
For resources on metadata best practices, you may want to review the App Store Product Page information available on the Apple Developer website.
For information on scheduling price tier changes, please review the Schedule price changes section of App Store Connect Developer Help.
- 大概意思是说协议相关的,需要在描述信息中也添加上相关协议
Guideline 3.1.2 - Business - Payments - Subscriptions
We noticed that your app did not meet all the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.
We were unable to find the following required item(s) in your app's metadata:
– A functional link to the Terms of Use (EULA)
Next Steps
To resolve this issue, please add this missing information. If the above information is present, please reply to this message in Resolution Center to provide details on where to locate it.
If you are using the standard Apple Terms of Use (EULA), you will need to include a link to the Terms of Use in your App Description. If you are using a custom EULA, add it in App Store Connect.
Resources
- Learn more about offering auto-renewable subscriptions on the App Store.
- Review the Paid Applications agreement (App Store Connect login required).
目前就这些,有问题请指出。