This is the second article of the ADAT project. The main content is a debunking of iOS and macOS in-app purchases, including the causes of various problems, how to fix them, and best practices. The author also provides some practical suggestions for developers and operators based on his personal experience.


An overview of the content

  • configuration
  • The error message
  • localization
  • paper
  • To subscribe to
  • Other problems


configuration

Do I have to upload an App to test in-app purchases?

Don’t need. Testing in-app purchases does not require uploading the App. Once you’ve created the item in App Store Connect and made sure there are no problems, you can test the in-app purchase.

Be careful not to upload an unfinished test package to App Store Connect and submit it for review, as it will most likely be rejected. If the audit is rejected, the in-app purchase will be made unavailable. Testing in-app purchase will definitely fail. Remember to modify the item to make it available before testing again.

The recommended practice is: after other functions of the App are basically completed, submit an App without in-app purchase function first. After the App is approved, add in-app purchase items and submit another review. The advantage of this is that if there are any problems, most of them will have been exposed during the first audit and there will be plenty of time to fix them. The last submission audit, basically just audit internal purchase items, will be much faster.

How to help Apple combat fraud during purchase?

SKPayment provides an applicationUsername attribute that helps Apple identify unusual activity when requesting a purchase. Its value is ideally a one-way hash of the user identifier generated by the server. After the transaction object is created, set this hash value to the value of the applicationUsername property and then add it to the transaction queue SKPaymentQueue.

Be careful not to populate applicationUsername with the Developer account’s Apple ID, the user’s Apple ID, or an unhashed user identifier.

Here is an example method to create a hash value that takes the user id and returns the SHA-256 value of the user ID:

// Custom method to calculate the SHA-256 hash using Common Crypto. - (NSString *)hashedValueForAccountName:(NSString *)userAccountName { const int HASH_SIZE = 32; unsigned char hashedChars[HASH_SIZE]; const char *accountName = [userAccountName UTF8String]; size_t accountNameLen = strlen(accountName); // Confirm that the length of the user name is small enough // to be recast when calling the hash function. if (accountNameLen > UINT32_MAX) { NSLog(@"Account name too long to hash: %@", userAccountName); return nil; } CC_SHA256(accountName, (CC_LONG)accountNameLen, hashedChars); // Convert the array of bytes into a string showing its hex representation. NSMutableString *userAccountHash = [[NSMutableString alloc] init]; for (int i = 0; i < HASH_SIZE; i++) { // Add a dash every four bytes, for readability. if (i ! = 0 && i%4 == 0) { [userAccountHash appendString:@"-"]; } [userAccountHash appendFormat:@"%02x", hashedChars[i]]; } return userAccountHash; }Copy the code

Populate the result returned by the method into the applicationUsername property of the SKMutablePayment object:

// product is an SKProduct object.
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
 
//Populate applicationUsername with your customer's username on your server.
payment.applicationUsername = [self hashedValueForAccountName:@"userNameOnYourServer"];
 
// Submit payment request.
[[SKPaymentQueue defaultQueue] addPayment:payment];

Copy the code

The lowest version of a subscription that supports automatic renewal.

Auto-renewals are currently only available on iOS and macOS, with the lowest versions being iOS 4.2 and macOS 10.9.

When to use restoreCompletedTransactions method?

This method can only be used for auto-renew subscriptions and non-consumable products in the following scenarios:

  • After subscribing on one device, users switch devices and need to resume transactions on the new device.
  • After the user uninstalls and reinstalls the App, the transaction needs to be resumed.

This approach provides good support for serverless apps. If your App has a Server that supports App Store Server Notifications and can manage users’ subscriptions, you can skip this method.

How many in-app purchases can be created per application?

Each developer account is allowed to create up to 10,000 in-app purchases. All apps in the account share this quota, so each App can create up to 10,000 in-app purchases. In-app purchase projects can be used across platforms and can be used in custom scenarios, which is quite useful.


The error message

Error messages refer to the messages that are displayed when the StoreKit interface is called. The following lists some common messages.

Your account info has changed

Your account information has been changed.

This message indicates that the test account is logged in to the App Store. Once a sandbox account is logged in to the App Store, it becomes a regular Apple ID, a regular user account, and no longer has sandbox features.

Solution: Log out of the Apple ID in the Settings, and create a new sandbox account in App Store Connect. Open the App call payment interface and enter the new sandbox account in the popup window.

Cannot connect to iTunes Store

Unable to connect to iTunes Store.

This may occur when:

  • The sandbox environment is temporarily unavailable.
  • The AppInfo.plistmissingCFBundleVersionInformation.
  • The App runs in the emulator, which does not support in-app purchases.
  • The product you want to purchase is currently unavailable.

If the sandbox environment is not available, you can view its status by entering the following link in the device’s Safari:

https://developer.apple.com/system-status/
Copy the code

Status of system services:

This Apple ID has not yet been used in this iTunes Store

The Apple ID is not yet available in the iTunes Store.

The reason for this prompt is that you have logged into your test account in the iTunes Store.

Solution: See Your Account info has changed.

You’ve already purchased this. Tap OK to download it again for free

You have purchased the project, click OK to download it for free.

This is just a common tip, not an error. That’s because you’re buying a non-expendable item that you already bought, and StoreKit tells you that you won’t be charged again.

This tip is for non-consumable projects only, because only non-consumable projects are allowed to have hosted content, and thus download.

You’ve already purchased this. Would you like to get it again for free?

You have already purchased this item, do you want to get it for free?

For the same reason.

This In-App Purchase has already been bought. It will be restored for free.

You have purchased the in-app purchase item, which will be restored for free.

The reason for this prompt is that you are trying to purchase the same product after previously purchasing the product without calling the finishTransaction method of SKPaymentQueue.

The SKPaymentQueue is the global queue in which the App Store processes transactions. The finishTransaction method tells the App Store that I’ve completely processed the transaction and I don’t rely on it anymore. Once the App Store receives the information, it removes the transaction from the queue at the appropriate time. Once the removal is complete, the App Store will also notify the App of its successful removal via a callback:

- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions;
Copy the code

You’ve already purchased this In-App Purchase but it hasn’t been downloaded

You have purchased the in-app purchase item but have not yet downloaded it.

The reason is the same as above, but only for non-consumable products.

This is not a test user account. Please create a new account in the Sandbox environment

This account is not a test account, please create a new test account in the sandbox environment.

The reason for this prompt is that a non-sandbox account is entered in the login popup after the payment interface is invoked.

Solution: See Your Account info has changed.


localization

The device language is not English, but the localizedDescription and localizedTitle of the returned product are always English.

The localizedDescription and localizedTitle information returned is based on the current iTunes Store language, not the language in the Settings, which in turn depends on the test account. Create and use a test account for which locale you want to see localized information.


paper

How do I use the cancellation_date field?

This field only applies to auto-renewed subscriptions, non-renewed subscriptions, and non-consumable products. This field is available in the JSON data returned by ticket verification when a user applies for Refund or cancels Family Sharing. Therefore, this field can be used to monitor user refunds and recall products or services that have been issued in a timely manner.

How to choose ticket check address?

Sandbox address used in the test phase:

https://sandbox.itunes.apple.com/verifyReceipt
Copy the code

Use the official address after App Store launch:

https://buy.itunes.apple.com/verifyReceipt
Copy the code

The App review team tests in a sandbox environment, so they access the sandbox address to validate tickets.

Best practice: Always check the formal environment first, and then check the sandbox environment if the 21007 status code is returned. The advantage of this approach is that there is no need to switch addresses frequently during App testing, approval, release, etc.

Status code 21007 indicates that the ticket generated in sandbox environment is sent to the formal environment for verification. Status code 0 indicates that the ticket is verified successfully.

Failed to verify ticket and return status code as a string of digits

Possible causes include:

  • The original bill is not carried outBase64Code, but directly take the original bill to check.
  • There is a problem with the Base64 algorithm used that does not encode certain characters in tickets correctly. You are advised to use the official algorithm.
  • The data accessing the ticket verification address POST is not in JSON format.

Here is a SAMPLE JSON validation with a ticket request containing an automatically renewed subscription:

{ "exclude-old-transactions": true/false, "receipt-data" : "..." , "password" : "..." }Copy the code

The purchase was successful but not shipped during the audit period

Verify that the correct checksum address is used. Reference: How to choose the ticket check address?

The checksum ticket returns an empty in_APP array

The empty array indicates that the App Store has not logged any transactions by the current user, possibly because the ticket has not been updated.

Consumable products are added to the ticket after a successful purchase, removed from the transaction queue after a successful finishTransaction, and officially removed from the ticket when the ticket is updated next time, while auto-renewed subscriptions, non-renewed subscriptions, and non-consumable products remain permanently in the ticket after a successful purchase. If the application only provides consumable products, an empty array will appear if the ticket is checked before the purchase has been updated and there is currently no product available.

In this case, you can use SKReceiptRefreshRequest to explicitly refresh the ticket. This method pops up to ask the user to authorize it.

How do I handle the case where appStoreReceiptURL is empty?

If appStoreReceiptURL is empty, you can assume that the user did not purchase successfully and do not ship. Then pop up a message to the user indicating that the ticket is expired or lost and needs to be refreshed. After the user agrees, use SKReceiptRefreshRequest to refresh the ticket and determine the refresh result in requestDidFinish and didFailWithError call-back. If the refresh succeeds, go to the normal delivery process after you get the bill. If the refresh fails, handle it according to your own business.

SKReceiptRefreshRequest will pop up to ask the user to authorize, and the user may cancel authorization. The App must handle the cancellation of authorization correctly.

A typical scenario for this is to install an App from TestFlight or Xcode. From an App Store install or iCloud restore install, tickets usually exist, but there are some unknown cases where tickets do not exist.


To subscribe to

How do I create and upload a managed non-consumable product

Create a new Target In Xcode and select the In-App Purchase Content under Other. After completing the Content, click Product -> Archive to open the Organizer. Select the project of type In-App daylight In the list In the upper left corner, and then click Distribute Content to upload to App Store Connect.

How to deal with the situation where the auto-renew subscription products are not of the same length?

The subscription management page allows users to adjust their subscriptions automatically. Open the following address in the App:

https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions
Copy the code

When opened, it will jump to Safari and then iTunes Store, and the subscription management page will pop up automatically.

Latest edition address:

https://apps.apple.com/account/subscriptions
Copy the code

This address will directly open the App Store and automatically pop up the subscription management page.

How do I convert auto-renew subscriptions into regular in-app purchases?

The operation steps are as follows:

  1. Remove subscription items in App Store Connect.
  2. Create a new in-app purchase item, remove the subscription from the code and use the new in-app purchase item.
  3. The test.

The first step causes the subscription to become non-renewable and subscribers to receive an email notification. For users who have already subscribed, the corresponding service should still be provided until the subscription ends. For example, if a customer buys a one-month automatic renewal subscription on April 1, you remove it on April 19, but you have to offer the service until May 1.

Why didn’t the App receive any renewal notice at the front desk

First check to see if transaction listeners are added as soon as the App starts, and make sure they are not released throughout the App’s life cycle. It then switches the App to the background, and then to the foreground, which triggers StoreKit to check the App Store for a renewal notification. If there is a renewal, so trade listener paymentQueue: updatedTransactions: is invoked, which is received the renewal notice.

The server rarely receives notifications of type RENEWAL

RENEWAL notifications will only be issued if the App Store successfully renewed expired subscriptions. RENEWAL notifications will not be issued if the subscription is successfully renewed before it expires, so check that your code matches this processing logic first.

24 hours before the subscription expires, the App Store will deduct money from the payment method the user is bound to, and if the deduction fails for some reason, it will technically be expired, even if it is not actually expired. A notification of type RENEWAL is also issued if a subsequent deduction attempt is successful.

Although RENEWAL has been marked DEPRECATED and will not appear in server notifications after March 10, 2021, authors will still receive a notification from RENEWAL until post time (2021.12.09).


Other problems

Why the product ID in invalidProductIdentifiers array?

Possible causes include:

  • The Explicit Bundle ID (Explicit Bundle ID) is not used, i.e. the wildcard Bundle ID is used.
  • App review rejected or developer withdraws review.
  • Missing metadata for in-purchased items.
  • No description file matching the Bundle ID was used for signing.
  • Modified product information, but the information has not been synchronized to all App Store servers.
  • Did not complete the App Store Connect background as requiredAgreements, taxes and bankingThe configuration.
  • Non-consumable products have content that Needs to be hosted by Apple, but that content has not yet been uploaded.
  • To pass toSKProductsRequestThe product ID has not been created in the App Store Connect background.

For non-consumable products, as long as there is content that Needs to be hosted by Apple but has not been uploaded, it will remain unavailable until the content is uploaded successfully. You can turn off hosting and open it when the content is ready.

If you have just renewed your Developer Membership, head over to agreements, tax and banking to check if there are any new terms that need to be agreed or new bank details that need to be updated. If the membership expires, this term information will expire with it.

The App has been careful, why will there be any product ID in the invalidProductIdentifiers array?

After an App is approved, it must be approved by the developer before it can be published on the App Store. Once approved, the Application ID is activated and accessible in the App Store. The in-app purchase product ID of the App in the App Store also needs to be activated and also depends on this approval operation. In some cases, the activation time of in-purchase product ids may lag the application activation time by up to 48 hours.

If the developer does not approve, any subsequent product ids created will not be activated. To test a new product ID, you must first approve it. Once the product ID is activated, subsequent submitted App updates can be used directly, even if the App update has not been approved for distribution.

For game operators, if the card is approved at opening time, there is a high risk that the game will not be found in the App Store. There is often a lot of kryptonite activity in the opening of the server, and if this is combined with the issue of in-app purchase product ID activation lag, then players will definitely fry the pot. If this happens, please understand the developer, he/she is more scared than anyone, trust him/her, and wait patiently.

It is recommended to approve the release at least one day in advance. On the one hand, enough time is left for the App Store server to synchronize data, which can largely avoid the above two problems. Players who have already made an appointment or heard about the upcoming game from some source will download the game and sign up for an account. At this point, part of the account registration flow can be digested in advance, so that the account related service pressure is reduced. Finally, if a major BUG is found, there is still time to fix it. Immediately after the fix, submit a new package for review. Hopefully, it is still too late.

Call the restoreCompletedTransactions method did not restore any product

Possible causes include:

  • There are pending transactions in the queue. The recovery operation does not return any products as long as there are outstanding transactions.
  • You have not purchased an automatic renewal subscription, a non-consumable product, or a free subscription before, so you cannot restore the product.
  • Try to restore non-renewed subscription or consumable products that do not support restore operations.
  • CFBundleVersion not set according to the specification: three non-negative integers terminated by a period (.) A concatenated string.

When the auditor buys the product, the App doesn’t react or crashes

Possible causes include:

  • throughaddPayment:Method passed inSKPaymentThe object may be empty or its associated product ID is not available.
  • If the App delivers the goods according to the check result of the bill, it is likely that the check address is not correct, which leads to the check failure. The solution is referred to above: How to choose the check address?

For the first case, here’s an example of an error (forbidden in App and likely to crash) :

@property SKProductsRequest *request; @property SKProduct *product; - (IBAction)purchase:(id)sender { NSSet *productID = [NSSet setWithObject:@"product_identifier"]; // Create a product request. self.request = [[SKProductsRequest alloc] initWithProductIdentifiers:productID]; self.request.delegate = self; // Send the product request to the App Store. [self.request start]; } - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { // product is  an instance of SKProduct. The app assumes that response.products is // not empty by getting its first element without any checks. self.product = [response.products firstObject]; NSLog(@"Name: %@", self.product.localizedTitle); // The app creates a payment request for a product whose value could be nil. SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:self.product]; // If the product was nil, the app will crash when executing addPayment:. [[SKPaymentQueue defaultQueue] addPayment:payment]; }Copy the code

Best practices:

  1. Request product details from the App Store
  2. Check whether Response. products contains the ID of the product to be purchased and get the correspondingSKProductobject
  3. Determines whether the SKProduct object is empty and, if not, calls the addPayment: method.

Recommended practice examples:

@property SKProduct *product;
@property SKProductsRequest *request;

- (void)fetchProductInformation {
     NSSet *productID = [NSSet setWithObject:@"product_identifier"];
     // Create a product request.
     self.request = [[SKProductsRequest alloc] initWithProductIdentifiers:productID];
     self.request.delegate = self;
 
    // Send the product request to the App Store.
    [self.request start];
}

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    // Be sure that the products array is not empty before fetching its content.
    if ([response.products count] > 0)
    {
       // product is an instance of SKProduct.
       self.product = [response.products firstObject];
       NSLog(@"Name: %@", self.product.localizedTitle);
    }
}

- (IBAction)purchase:(id)sender {
    if (self.product != nil)
    {
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:self.product];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}
Copy the code


Reference documentation

English text: developer.apple.com/library/arc…


A Craftsman’s Diary – the author of this article