Better practice of Apple IAP receipt inspection

Posted by HAVOCWIZARD on Thu, 24 Feb 2022 17:07:35 +0100

preface

IAP pays too many pits. Here are some advanced pits.

1, Request goods

The following is the code of the requested item:

- (void)validateProductIdentifier:(NSArray *)productIdentifier {
    SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifier]];
    self.request = productRequest;
    productRequest.delegate = self;
    [productRequest start];
}

#pragma mark - SKProductsRequestDelegate Protocol
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    
    self.products = response.products;
    [response.products enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        SKProduct *product = (SKProduct *)obj;
        NSLog(@"valid identifier: %@", product.productIdentifier);   
    }];
    
    for (NSString *invalidIdentifier in response.invalidProductIdentifiers) {
        // invalid identifier
        NSLog(@"invalid identifier: %@", invalidIdentifier);
    }
    
    if (![SKPaymentQueue canMakePayments]) {
        // display error UI ...
    }
    // display store UI ...
}

Requesting product information from Apple server is to display the store UI. The requested SKProduct contains the title, description, price, currency symbol and other information of the product. In China, generally, the server interface provides commodity information, the client directly displays the store UI, and the payment is initiated only when the user clicks to buy. Therefore, in this case, it is not necessary to request product information from the apple server. Because when requesting product information, the apple server is overseas, and the domestic delay is large, about six or seven seconds slow. It may even fail to jump into the proxy method of SKProductsRequest, resulting in payment failure.

terms of settlement:

Directly omit the creation and initiation of the request SKProductsRequest. When paying, use paymentWithProductIdentifier to directly generate SKPayment.

SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];

2, Receive validation

Get receive

    NSData *receipt;
    if (IOS7_OR_LATER) {
    // iOS 7 style app receipts
        NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
        receipt = [NSData dataWithContentsOfURL:receiptURL];
    }else {
    // iOS 6 style transaction receipt
        receipt = transaction.transactionReceipt;
    }

Verify receipt

Receipt Validation Programming Guide
The above address is the verification method of receipt. For security reasons, APP receive needs a third-party server to verify with apple server. After verification, the returned value is json dictionary.

The description of the field status is as follows:

For iOS 6 style transaction receipts, the status code reflects the status of the specific transaction's receipt.

For iOS 7 style app receipts, the status code is reflects the status of the app receipt as a whole. For example, if you send a valid app receipt that contains an expired subscription, the response is 0 because the receipt as a whole is valid.

You can see that the iOS 6 style receipt contains the receipt of the transaction.

The iOS 7 style receipt contains a list of information, which contains a lot of transaction information. If status=0 is returned, it only means that the receipt of the whole App has passed the verification.

The app side needs to send a receipt to the server. The server verifies the receipt to the apple server and then returns status. be careful! The iOS 7 style receipt contains all transaction credentials of the entire application. Therefore, when status=0, the goods of all transactions in the receipt should be distributed. Apple's verification results only tell us whether the receipt is valid or invalid. We don't know which transactions have distributed goods. Therefore, the server needs to query, arrange and record from the database, and verify whether the transaction is a refunded order to avoid re distribution of goods.

Misunderstanding:

Use [[NSBundle mainBundle] appStoreReceiptURL] to get the receipt, but the server tries to find the last transaction information.

Correct posture:

The goods of all transaction s in the receipt should be distributed (except those reused and refunded)

Receive JSON return result

iOS 7 style

{
    environment = Sandbox;
    receipt =     {
        "adam_id" = 0;
        "app_item_id" = 0;
        "application_version" = "1.0";
        "bundle_id" = "com.dianzhong.kuaikan";
        "download_id" = 0;
        "in_app" =         (
                        {
                "is_trial_period" = false;
                "original_purchase_date" = "2016-09-18 07:55:33 Etc/GMT";
                "original_purchase_date_ms" = 1474185333000;
                "original_purchase_date_pst" = "2016-09-18 00:55:33 America/Los_Angeles";
                "original_transaction_id" = 1000000236789335;
                "product_id" = "com.dianzhong.kuaikan6";
                "purchase_date" = "2016-09-18 07:55:33 Etc/GMT";
                "purchase_date_ms" = 1474185333000;
                "purchase_date_pst" = "2016-09-18 00:55:33 America/Los_Angeles";
                quantity = 1;
                "transaction_id" = 1000000236789335;
            },
            ...
        );
        "original_application_version" = "1.0";
        "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
        "original_purchase_date_ms" = 1375340400000;
        "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
        "receipt_creation_date" = "2017-04-05 08:53:06 Etc/GMT";
        "receipt_creation_date_ms" = 1491382386000;
        "receipt_creation_date_pst" = "2017-04-05 01:53:06 America/Los_Angeles";
        "receipt_type" = ProductionSandbox;
        "request_date" = "2017-04-05 08:54:44 Etc/GMT";
        "request_date_ms" = 1491382484980;
        "request_date_pst" = "2017-04-05 01:54:44 America/Los_Angeles";
        "version_external_identifier" = 0;
    };
    status = 0;
}

iOS 6 style

{
    receipt =     {
        bid = "com.dianzhong.kuaikan";
        bvrs = "1.0";
        "item_id" = 1140823223;
        "original_purchase_date" = "2017-04-01 08:48:59 Etc/GMT";
        "original_purchase_date_ms" = 1491036539000;
        "original_purchase_date_pst" = "2017-04-01 01:48:59 America/Los_Angeles";
        "original_transaction_id" = 1000000286821320;
        "product_id" = "com.dianzhong.kuaikan12";
        "purchase_date" = "2017-04-01 08:48:59 Etc/GMT";
        "purchase_date_ms" = 1491036539000;
        "purchase_date_pst" = "2017-04-01 01:48:59 America/Los_Angeles";
        quantity = 1;
        "transaction_id" = 1000000286821320;
        "unique_identifier" = 367c781771909890ea8d59b25db3daf05ef0fbcb;
        "unique_vendor_identifier" = "7F144627-A82D-4D71-AACD-C3BAF2ED6684";
    };
    status = 0;
}

It can be found that the structures of the two are basically the same, and both contain a dictionary called receive. The difference is that the iOS 7 style receive puts each transaction information in_app array.

What the server should do:

When status=0, record all transaction information in the receipt.
The server can be based on the transaction_id to record the information of each transaction. As a record, it is written into the database to facilitate subsequent query and weight removal.

fieldtypedescribe
transaction_idintegerTransaction number
original_transaction_idintegerOriginal transaction number
product_idstringItem identifier
quantityintegerquantity
purchase_datestringDate of purchase
original_purchase_datestringOriginal purchase date
purchase_date_msintegerPurchase date (ms)
original_purchase_date_msintegerOriginal purchase date (ms)
purchase_date_pststringPurchase date (pst)
original_purchase_date_pststringOriginal purchase date (pst)
cancellation_datestringDate of cancellation of purchase

The process is as follows:

 

The process of the server processing the receipt

Refunded order

The orders refunded by the user will still appear in the receipt, so the App server needs to be able to identify the refunded orders when verifying, so as not to ship the refunded orders.

An order with a refund ID: cancellation_date field.

When the server verifies the credentials, if there is this field, the goods will not be distributed.

3, Security of receipt

Tang Qiao is in his List of iOS in app payment (IAP) development steps It is mentioned in:

Considering the abnormal conditions of the network, the sending voucher operation on the iOS side should be persistent. If the program exits, crashes or the network is abnormal, it can be resumed and retried.

In fact, we don't need to make wheels by hand!

As long as the SKPaymentQueue is monitored, the system will traverse all transactions of the application. As long as the transactions that are not ended with the finish Transaction: method will reappear in the updatedTransactions: method. The system has done a very safe operation of storing Transaction and receipt for us. The underlying principle is not clear at present. I've tried to delete the application and reinstall it. As long as the Bundle ID remains unchanged, I can still jump to the updatedTransactions: method.

be careful! If your back office creates an orderNum and the order number comes out before paying apple, you have to store the orderNum yourself.
In addition, since an order number is issued in the background, it is defaulted that the order number is associated with a certain commodity, so transaction is used when obtaining the receipt Transactionreceive, so as to ensure that the received contains only one transaction information.

For more information about receipt verification, please click here