An overview of the

An iOS in-app Purchase is an in-app Purchase from apple’s App Store, known as an IN-app Purchase (IAP). It is a system of transactions that Apple provides for the Purchase of in-app virtual goods or services. Why do we need to know the IAP process? The App Store review guide states:

If you want to unlock features or features in the app (by subscription, in-game currency, game levels, limited access to premium content, or unlocking the full version), you must use in-app purchases. Apps must not use their own mechanisms to unlock content or features, such as license keys, augmented reality markers, qr codes, etc. The App and its metadata shall not contain buttons, external links or other action numbers to direct users to purchase items other than in-App purchases.Copy the code

Payment for virtual goods or services within the APP must be made using IN-app purchases. Alipay, wechat Pay and other third-party payment methods (including Apple Pay) are not allowed, and users are not allowed to purchase virtual goods or services outside the APP in any way (including exiting the APP, prompting copywriting, etc.). If you do not comply with this rule, Apple reviewers will not release your APP!!

Prepare before internal purchase

Before integrating IAP code in APP, you need to go to ITunes Connect of the account to perform the following three steps:

1. Fill in the bank account information in the background

2. Configure product information, including product ID and price

3. Configure a sandbox account for testing IAP payment functionality.

Filling in bank account information is generally left to product management personnel, developers do not need to pay attention to, developers need to pay attention to the second and third steps.

Configure in-app purchases

IAP is a product transaction system, rather than a simple payment system. Each purchase item needs to create a corresponding product for the App in the Itunes Connect background of the developer background and submit it to Apple for approval before the purchase item takes effect. There are four types of in-app purchases:

  • Consumable items: products that can only be used once, then become invalid and must be purchased again, such as game coins and one-time virtual items
  • Non-consumable items: products that are purchased once and do not expire or decrease with use. For example: e-books
  • Auto-renew subscriptions: Products that allow users to purchase dynamic content for a fixed period of time. These subscriptions are automatically renewed unless the user opts to cancel, as with monthly subscriptions such as Apple Music (some rogue developers use this to harvest users unfamiliar with IAP products, see App Store “rogue” apps).
  • Non-renewed subscription: Allows users to purchase products with a time-limited service. The contents of items purchased in this App can be static. Such subscriptions are not automatically renewed

Note The product ID and price when configuring product information

1. The product ID is unique. It is recommended to use the Bundle Identidier prefix of the project to concatenate a custom unique product name or ID (letter or number). Once to create a new item, its product ID will always be, even if the goods have been deleted, has created the information in addition to product ID all the goods can be modified, if you remove a goods, will be unable to create a commodity of the same product ID, also means that the product ID permanent failure!!!!!!

2. When creating IAP programs, you need to set the price, the product price can only be selected from the price level provided by Apple, this price level is fixed, the same price level will correspond to the currency of each country, for example, Level 1 will correspond to 1 USD and 6 RMB, Level 2 will correspond to 2 USD and 12 RMB… The highest grade, 87, is $999.99 or 6,498 yuan. In addition, there may be some special levels to accommodate developers and users in certain currency areas, such as reserve Level A for $1 and RMB 1, reserve Level B for $1 and RMB 3 and so on. In addition, IAP items cannot be priced at RMB 9.9, which does not fit any level. See Apple’s official pricing documentation for a detailed pricing list

Apple’s pricing schedule does not normally change, but it does not rule out changing the pricing of certain currencies in the event of significant changes in exchange rates, and Apple will notify developers of the changes by email.

3. Commodity sharing

The default split between Apple and developers is 3/7 for paid apps and in-app purchases on the App Store. But in practice, transaction taxes have to be deducted before Apple can split with developers in some areas, and the developer’s actual cut may not be 70%. Since October 2015, Apple has deducted a 2% transaction tax from App Store purchases in China, giving developers between 68% and 69% of their in-app purchases for China accounts. There are also differences in transaction tax standards in different regions outside China, such as Apple’s official price-rating documents

If real income needs to be calculated strictly, this component may need to be taken into account.

The in-app purchase price and developer’s actual income for each region are detailed in Apple’s price scale.

In addition, apple introduced new rules in June 2016 for Auto-renewable Subscription type Iaps, which allow developers to receive 85% of the revenue from the second year of purchase if the Subscription is longer than 1 year. See Apple’s subscription pricing guide for details

The sandbox account

Before the launch of new in-app purchase products, testers generally need to test in-app purchase products. However, in-app purchase involves money, so Apple provides the function of sandbox test account for in-app purchase test. After the launch of Apple Pay, sandbox test account can also be used for Apple Pay payment test. The Apple ID, which can only be used for in-app purchases and Apple Pay testing, is not a real Apple ID.

Note the following when filling in sandbox test account information:

  • The email cannot be an email address that someone else has registered with an AppleID
  • An email address may not be a real email address, but it must conform to the email format
  • The selection of App Store region, the pop-up prompt box and settlement price will be based on the region selected by sandbox account. It is recommended to create several accounts of different regions for testing!!

Use of sandbox account test:

  • First, sandbox test accounts must be tested in a real environment and be an adhoc or develop certificate signed installation package. Sandbox accounts do not support installation packages downloaded directly from the App Store
  • Log out of the real Apple ID account in the App Store of the real phone. After logging out, you do not need to log in to the sandbox test account in the App Store
  • Then go to the App to test the purchase of goods, the login box will pop up, selectUse your existing Apple IDAnd then log in to the sandbox test account. After successful login, the purchase prompt box will pop up. ClickbuyAnd a prompt box will pop up to complete the purchase.

The purchase process

The IAP payment process is divided into client and server. The client works as follows:

  • Get the list of purchased products (read from App or read from your own server) and show the list to the user
  • After the user selects a purchased product, the user first requests the localized information list of available purchased products, this time calling the code of Apple’s StoreKit library
  • After obtaining the localization information of the purchased product, the purchased product can be obtained according to the ID of the purchased product selected by the user
  • Initiate IAP purchase request based on in-app purchase product and receive callback of purchase completion
  • At the end of the purchase process, a request is made to the server to verify the credentials and payment results
  • Its own server returns the payment result information to the front end and issues virtual products

The front-end payment flow chart is as follows:

------------------------------ IAPManager.h -----------------------------
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef enum {
    IAPPurchSuccess = 0,       // 购买成功
    IAPPurchFailed = 1,        // 购买失败
    IAPPurchCancel = 2,        // 取消购买
    IAPPurchVerFailed = 3,     // 订单校验失败
    IAPPurchVerSuccess = 4,    // 订单校验成功
    IAPPurchNotArrow = 5,      // 不允许内购
}IAPPurchType;

typedef void (^IAPCompletionHandle)(IAPPurchType type,NSData *data);

@interface IAPManager : NSObject
+ (instancetype)shareIAPManager;
- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle;
@end

NS_ASSUME_NONNULL_END



------------------------------ IAPManager.m -----------------------------

#import "IAPManager.h"
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

@interface IAPManager()<SKPaymentTransactionObserver,SKProductsRequestDelegate>{
   NSString           *_currentPurchasedID;
   IAPCompletionHandle _iAPCompletionHandle;
}
@end

@implementation IAPManager
 
+ (instancetype)shareIAPManager{
     
    static IAPManager *iAPManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
        iAPManager = [[IAPManager alloc] init];
    });
    return iAPManager;
}
- (instancetype)init{
    self = [super init];
    if (self) {
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}
 
- (void)dealloc{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
 
 
- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle{
    if (purchID) {
        if ([SKPaymentQueue canMakePayments]) {
            _currentPurchasedID = purchID;
            _iAPCompletionHandle = handle;
            
            //从App Store中检索关于指定产品列表的本地化信息
            NSSet *nsset = [NSSet setWithArray:@[purchID]];
            SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
            request.delegate = self;
            [request start];
        }else{
            [self handleActionWithType:IAPPurchNotArrow data:nil];
        }
    }
}

- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
#if DEBUG
    switch (type) {
        case IAPPurchSuccess:
            NSLog(@"购买成功");
            break;
        case IAPPurchFailed:
            NSLog(@"购买失败");
            break;
        case IAPPurchCancel:
            NSLog(@"用户取消购买");
            break;
        case IAPPurchVerFailed:
            NSLog(@"订单校验失败");
            break;
        case IAPPurchVerSuccess:
            NSLog(@"订单校验成功");
            break;
        case IAPPurchNotArrow:
            NSLog(@"不允许程序内付费");
            break;
        default:
            break;
    }
#endif
    if(_iAPCompletionHandle){
        _iAPCompletionHandle(type,data);
    }
}
 
- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
    //交易验证
    NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
     
    if(!receipt){
        // 交易凭证为空验证失败
        [self handleActionWithType:IAPPurchVerFailed data:nil];
        return;
    }
    // 购买成功将交易凭证发送给服务端进行再次校验
    [self handleActionWithType:IAPPurchSuccess data:receipt];
     
    NSError *error;
    NSDictionary *requestContents = @{
                                      @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                      };
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];
     
    if (!requestData) { // 交易凭证为空验证失败
        [self handleActionWithType:IAPPurchVerFailed data:nil];
        return;
    }
     
    NSString *serverString = @"https:xxxx";
    NSURL *storeURL = [NSURL URLWithString:serverString];
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];
     
    [[NSURLSession sharedSession] dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            // 无法连接服务器,购买校验失败
            [self handleActionWithType:IAPPurchVerFailed data:nil];
        } else {
            NSError *error;
            NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
            if (!jsonResponse) {
                // 服务器校验数据返回为空校验失败
                [self handleActionWithType:IAPPurchVerFailed data:nil];
            }
             
            NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];
            if(status && [status isEqualToString:@"0"]){
                [self handleActionWithType:IAPPurchVerSuccess data:nil];
            } else {
                [self handleActionWithType:IAPPurchVerFailed data:nil];
            }
#if DEBUG
            NSLog(@"----验证结果 %@",jsonResponse);
#endif
        }
    }];
    
    // 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
 
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    NSArray *product = response.products;
    if([product count] <= 0){
#if DEBUG
        NSLog(@"--------------没有商品------------------");
#endif
        return;
    }
     
    SKProduct *p = nil;
    for(SKProduct *pro in product){
        if([pro.productIdentifier isEqualToString:_currentPurchasedID]){
            p = pro;
            break;
        }
    }
     
#if DEBUG
    NSLog(@"productID:%@", response.invalidProductIdentifiers);
    NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
    NSLog(@"产品描述:%@",[p description]);
    NSLog(@"产品标题%@",[p localizedTitle]);
    NSLog(@"产品本地化描述%@",[p localizedDescription]);
    NSLog(@"产品价格:%@",[p price]);
    NSLog(@"产品productIdentifier:%@",[p productIdentifier]);
#endif
     
    SKPayment *payment = [SKPayment paymentWithProduct:p];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}
 
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
#if DEBUG
    NSLog(@"------------------从App Store中检索关于指定产品列表的本地化信息错误-----------------:%@", error);
#endif
}
 
- (void)requestDidFinish:(SKRequest *)request{
#if DEBUG
    NSLog(@"------------requestDidFinish-----------------");
#endif
}
 
#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    for (SKPaymentTransaction *tran in transactions) {
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:
                [self verifyPurchaseWithPaymentTransaction:tran];
                break;
            case SKPaymentTransactionStatePurchasing:
#if DEBUG
                NSLog(@"商品添加进列表");
#endif
                break;
            case SKPaymentTransactionStateRestored:
#if DEBUG
                NSLog(@"已经购买过商品");
#endif
                // 消耗型不支持恢复购买
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:tran];
                break;
            default:
                break;
        }
    }
}

// 交易失败
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
    if (transaction.error.code != SKErrorPaymentCancelled) {
        [self handleActionWithType:IAPPurchFailed data:nil];
    }else{
        [self handleActionWithType:IAPPurchCancel data:nil];
    }
     
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
@end


/* 调用支付方法
 - (void)purchaseWithProductID:(NSString *)productID{
      
     [[IAPManager shareIAPManager] startPurchaseWithID:productID completeHandle:^(IAPPurchType type,NSData *data) {
          
     }];
 }
 */

Copy the code

Server work:

  • Receives the purchase voucher from the iOS terminal, checks whether the voucher exists or has been verified, and stores the voucher. The credential is sent to Apple’s server for validation and the result is returned to the client.

Return to buy

There are 4 kinds of in-app purchases: consumable items, non-consumable items, auto-renewed subscriptions and non-renewed subscriptions. The “non-consumable” and “auto-renew subscriptions” need to provide the ability to resume purchases, such as creating a resume button, otherwise the audit is likely to be rejected.

/ / callback within apple purchase and restore the interface [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];Copy the code

“Consumable items” and “non-renewed subscription” apple does not provide the interface to restore, do not call the above method to restore, otherwise it may be rejected!!

“Non-renewed subscription” is also synchronized across devices, so in principle, it also needs to provide the function of resuming purchases, but it needs to rely on the account system built by the APP itself to recover, and cannot use the interface provided by Apple above.

The buy off single

Drop single is the user pay to buy goods, money deduction, the goods did not arrive in the account. When a lost order occurs, users often come to customer service angry. Customer service then has to ask developers to manually add the product to the user. Obviously, hurting a user’s experience, especially a paying user’s experience, is a pretty bad thing.

How does a drop order come about? This starts with the technical flow of IAP payments.

IAP payment process:

1. Initiate payment

2. Fee deduction is successful

3. Receipt (Payment Voucher)

4, go to the background to verify the credentials to obtain the commodity transaction status

5. Return data and refresh data in front of verification

  • Missing order Situation 1:

    The problems in link 2 and 3 belong to Apple and have not been dealt with at present.

  • Missing order Situation 2:

Something goes wrong between three and four, like a broken Internet connection. At this point, the front end will store the payment credentials persistently. If the user uninstalls the APP during this period, the order will be really missed in the front end. If there is no assistance, the next time you open the APP and enter the purchase page, you will first judge whether there is any unsuccessful payment, and then prompt the user. This step depends on how the product needs to be done, allowing the user to choose whether to resume unsuccessful payments or to resume them silently.

  • Case 3 of missing order:

Something goes wrong between 4 and 5. At this point in fact, the background has been successful, but the front end did not get the data, when the single miss processing, the next time to enter the first refresh data can be.

Notes for internal purchase

  • Receipt will be sentenced heavily

Generally speaking, verify whether the payment voucher is valid and put it in the background. If the background does not re-evaluate, the same voucher can be verified countless times, because Apple does not re-evaluate, so the front end can go to the background to do verification !!!! for countless times with a payment voucher obtained by the front end , the background will issue countless goods to the front end, but the user only paid once, so the safe way is to make a record of the payment credentials verified by the background, each time to determine whether the new credentials have been used, to prevent multiple delivery of goods.

reference

Summary of iOS In-app Purchase