iOS StoreKit 2 New Features Analysis

iOS StoreKit 2 New Features Analysis

1. Background

At WWDC 2021, a new StoreKit 2 library was launched on the iOS 15 system, which uses a completely new API to solve in-app purchase issues.

  • Meet StoreKit 2 - WWDC21 - Videos - Apple Developer: Highlights: Storekit 2 API introduction and code demo, and appAccountToken
  • Manage in-app purchases on your server - WWDC21 - Videos - Apple Developer: Highlights: Server verification of JWS signed transactions, new server APIs, new server notifications, new features for sandbox testing
  • Support customers and handle refunds - WWDC21 - Videos - Apple Developer: Highlights: Storekit 2 API and server API for supporting customers and handling refunds

2. Materials

3. Problems with StoreKit 1

1. Can I view the refunded order details in Apple backend?

No. Apple can only send a notification to our server after processing the refund, informing us that a refund has occurred.

2. Can refunds for consumable, non-consumable, non-renewable subscriptions, and auto-renewal be tested in the sandbox environment?

No. The system does not provide this kind of testing method.

3. Can the orderID in the Apple receipt provided by the user be associated with the specific transaction?

cannot.

  • After the receipt is parsed on the server side, it does not contain the orderID information, so it is impossible to directly associate the connection between them.
  • It does not support using the orderID in the Apple receipt to query the transaction information on the Apple server. This API is not provided (after StoreKit 2 is released, it supports querying StoreKit1 transactions, https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id?language=_9 ).

4. During the development process, it is impossible to directly associate the transaction with the orderID, although there is an applicationUserName field that can store information. However, this field is not 100% reliable and may lose stored data in some cases.

5. We cannot proactively go to Apple servers to obtain transaction history and refund information. We cannot proactively associate the order ID in the Apple receipt provided by the user with the orders we currently know.

6. Currently, sk1's skproduct cannot distinguish between consumables, non-consumables, subscription products, and non-continuous subscription products.

7. sk1 has queue monitoring. Each purchase needs to monitor the corresponding purchase status changes through the queue. All transaction callbacks are being monitored, and it is difficult to distinguish which are transactions for replenishing orders and normal purchases.

IV. New features of StoreKit v2

The new features of StoreKit 2 mainly include three parts:

  • StoreKit 2: Updates and changes to the API in the App, including in-app subscription changes, refunds, etc.;
  • Server to Server: Communication between Apple servers and developer servers, including Apple notifications, developers actively requesting Apple servers, new verification receipt processes, etc.
  • Sandbox Test: Updates related to the sandbox test environment, as well as some noteworthy events, etc.

StoreKit 2 API

The main updates of StoreKit 2 are as follows:

  • Develop with new features of Swift
  • Update receipts and transactions (data format and field changes)
  • More subscription type interfaces
  • Same StoreKit framework

5.1 Only supports Swift development

StoreKit 2 was developed using the new features of Swift 5.5, which completely changed the implementation of APIs for obtaining products, initiating transactions, and managing transaction information. https://swift.org/blog/

For example, the syntax for obtaining products is different:

Original way to obtain goods

 // 1. Request product
SKProductsRequest * request = [[ SKProductsRequest alloc ] initWithProductIdentifiers : identifiers ];
request . delegate = pipoRequest ;
[ request start ];

// 2. Implement SKProductsRequestDelegate
- ( void ) productsRequest :( SKProductsRequest * ) request didReceiveResponse :( SKProductsResponse * ) response
{
// success
}

- ( void ) request :( SKRequest * ) request didFailWithError :( NSError * ) error API_AVAILABLE ( ios ( 3.0 ), macos ( 10.7 ))
{
// failed
}

New way to get products

 // Get the product
let products = try await Product . products ( for : productIDs )
// Purchase product
func purchase ( _ product : Product ) async throws - > Transaction ? {
//Begin a purchase.
let result = try await product . purchase ()

switch result {
case .success (let verification ) :
let transaction = try checkVerified ( verification )

//Deliver content to the user.
await updatePurchasedIdentifiers ( transaction )

//Always finish a transaction.
await transaction.finish ( )

return transaction
case . userCancelled , . pending :
return nil
default :
return nil
}
}

5.2 New API

  • commodity
  • Buy
  • Transaction Information
  • Transaction History
  • Subscription Status

5.2.1 Product

Some new product types and subscription information have been added. These fields are not available in StoreKit 1.

Fields that are convenient for us to use:

  • With the newly added product type, we can easily know whether the current product is a consumable or a subscription product.
  • For the first purchase discount of automatic continuous subscription, we can directly perceive whether the current product is the first purchase under the user's Apple ID

For example:

Some apps have membership subscription services, and those services will have automatic renewals for 1 month, 3 months, 12 months, etc. There will also be some first-time purchase discounts, and this first-time purchase discount is linked to the Apple ID and has nothing to do with the app's own account system. For example, Pony.com's products have their own account system of QQ number + WeChat number. So we couldn't simply judge whether your Apple ID had enjoyed the first-time purchase discount before. After all, users can have multiple QQ numbers or multiple WeChat numbers. Before Apple's purchase page pops up, we didn't know whether this Apple ID had enjoyed the first-time purchase discount, which would cause misunderstandings to users. On the previous page, I was told that the first month was only 18 yuan, but why did it cost 25 yuan when I actually paid? This has visibly reduced users' willingness to buy.

Now we can use the isEligibleForIntroOffer property to easily and conveniently obtain this information in advance, and not display this offer to Apple ID accounts that have already enjoyed it.

Provides a new interface for obtaining products

 public static func products ( for identifiers : Identifiers ) async throws - >
[ Product ] where Identifiers : Collection , Identifiers . Element == String

A new purchase interface is provided. Some optional parameters PurchaseOption structure are added when purchasing goods. The structure contains a new and particularly important field appAccountToken, which is similar to the SKPayment.applicationUsername field, but the appAccountToken information will be permanently saved in the Transaction information.

The appAccountToken field is created by the developer; it is associated with the user account in the App; it uses the UUID format; and it is permanently stored in the Transaction information.

PS: Apple means that the appAccountToken field here is used to store user account information, but it can also be used to store orderID-related information. The orderID needs to be converted into UUID format and inserted into the Transaction information to facilitate operations such as order replenishment and refund.

 public func purchase ( options : Set < Product . PurchaseOption > = []) async throws - > Product . PurchaseResult

let uuid = Product . PurchaseOption . appAccountToken ( UUID . init ( uuidString : "uid" ) ! )

// After initiating a purchase, directly wait for the return result from Apple without waiting for the transaction status to be updated in the paymenqueue.
//Using the purchase order information initiated by sk2, all callback interfaces in sk1 will not get the corresponding transaction update status
let result = try await product . purchase ( options : [ uuid ])

//demo
func purchase ( _ product : Product ) async throws - > Transaction ? {
//Begin a purchase.
let result = try await product . purchase ()

switch result {
case .success (let verification ) :
let transaction = try checkVerified ( verification )

//Deliver content to the user.
await updatePurchasedIdentifiers ( transaction )

//Always finish a transaction.
await transaction.finish ( )

return transaction
case . userCancelled , . pending :
return nil
default :
return nil
}
}

Processing and verifying transactions. The system will verify whether it is a legitimate transaction. At this time, the system no longer provides base64 receipt string information. You only need to upload transaction.id and transaction.originalID. The server side selects the appropriate ID for verification as needed.

 func checkVerified < T > ( _ result : VerificationResult < T > ) throws - > T {
//Check if the transaction passes StoreKit verification.
switch result {
case . unverified :
//StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
throw StoreError . failedVerification
case . verified ( let safe ):
//If the transaction is verified, unwrap and return it.
return safe
}
}

Listening for Transaction Updates

 func listenForTransactions () - > Task < Void , Error > {
return Task . detached {
//Iterate through any transactions which didn't come from a direct call to `purchase()`.
for await result in Transaction . updates {
do {
let transaction = try self . checkVerified ( result )

//Deliver content to the user.
await self . updatePurchasedIdentifiers ( transaction )

//Always finish a transaction.
await transaction.finish ( )
} catch {
//StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
print ( "Transaction failed verification" )
}
}
}
}

For transaction updates, this listener allows us to listen:

  • The user has started a purchase, but the order has not yet been processed by Apple. The user kills the app or uninstalls and reinstalls it. At this time, we can receive the corresponding transaction update through this listener.
  • A user initiates a purchase. This purchase may receive a failed transaction callback on the client due to poor network conditions. However, Apple later discovers this case and resends the transaction to the client for corresponding verification.

5.2.2 Transaction History

Provides three new transaction-related APIs:

  • All transactions: All purchase transaction orders, obtained in transaction
  • Latest transactions: The latest purchase transaction order.
  • Current entitlements: All transactions for current subscriptions, and all non-consumable items purchased (and not returned).

You can filter out purchased items based on available subscription items and non-consumable items.

 extension Transaction {
public static var all : Transaction . Transactions { get }
public static var currentEntitlements : Transaction . Transactions { get }
public static func currentEntitlement ( for productID : String ) async - > VerificationResult < Transaction > ?
public static func latest ( for productID : String ) async - > VerificationResult < Transaction > ?
public static var unfinished : Transaction . Transactions { get }
}

Synchronize purchase records of different devices. This API can replace the restore purchase API in StoreKit 1. After calling this method, the system will pop up a prompt box to ask you to enter your AppleID account and password information.

 extension AppStore {
public static func sync () async throws
}

5.2.3 Subscription status

The status of subscription-type items, such as actively obtaining the latest transaction, obtaining the status of updated subscriptions, obtaining the information of updated subscriptions, etc. Among them, obtaining the information of updated subscriptions can obtain the updated status, item ID, and if expired, the reason for expiration can be known. (For example, user cancellation, deduction failure, normal subscription expiration, etc.) All the data obtained is JWS format verification.

5.2.4 show manager subscriptions

You can directly call up the Manage Subscriptions page in the App Store.

 extension AppStore {

@ available ( iOS 15.0 , * )
@ available ( macOS , unavailable )
@ available ( watchOS , unavailable )
@ available ( tvOS , unavailable )
public static func showManageSubscriptions ( in scene : UIWindowScene ) async throws
}

5.2.5 Request refund API

A new refund API is provided, allowing users to apply for a refund directly in the developer's app. After the user applies for a refund, the app will receive a notification, and the Apple server will also notify the developer's server. (Refund testing can also be performed in the sandbox environment, but this feature has not yet been enabled in the App Store.)

 extension Transaction {
public static func beginRefundRequest ( for transactionID : UInt64 , in scene : UIWindowScene ) async throws - > Transaction . RefundRequestStatus
}

5.2.6 Summary:

  • The StoreKit 2 library is rewritten using the latest features of Swift 5.5. It only supports Swift and iOS 15+, and provides some new API interfaces, which will lead to some changes in the new payment process.
  • Provides information about transaction history and a list of items that can be purchased (auto-renewable subscriptions and non-consumable items).
  • Provides interfaces for obtaining and managing subscription status
  • Support initiating refunds within the App

6. Server to Server

Building a developer server can achieve the following functions:

  • Receive notifications about in-app purchase status changes
  • Track in-app purchase status changes through the interface (obtain subscription status, obtain all transaction history records)
  • Verify user access rights at any time (whether the purchase has been made, whether a refund has been made, etc.)
  • Manage subscription status
  • Tracking Refund Information

6.1 Validate status with receipts

When the server verifies the receipt through the /verifyReceipt interface, the data structure of the new API has also changed. For example, the purchase time, expiration time, and original purchase time formats have been unified, and the appAcountToken field, in-app purchase type field, refund time, refund reason, promotional offer type, etc. have been added. For details, please refer to Manage in-app purchases on your server - WWDC21 - Videos - Apple Developer video, or Validating Receipts with the App Store | Apple Developer Documentation

6.2 Check status with APIs

Some new APIs have been added to actively obtain subscription status, transaction history, etc. For details, please refer to this document: App Store Server API | Apple Developer Documentation

  • Get subscription status: get_all_subscription_statuses, only needs an originalTransactionId parameter to get various status of user subscription
  • Get transaction history: get_transaction_history, you only need the originalTransactionId in any transaction to get all the history records
  • After your server receives the consumption request notification, it sends the consumption information about the in-app consumable purchase to the App Store. (It is mainly used to inform the App Store whether the purchased goods have been consumed after the user applies for a refund, so as to evaluate whether a refund is required.) send_consumption_information
  • App Store Server API standards: JSON Web Signature (JWS)

6.3 Track status with notifications

When the subscription status changes, Apple server will proactively notify our server of the changes. The functionality is the same as the previous version, but some statuses have been removed and some have been added.

To facilitate testing refund notifications in the sandbox environment, the App Store can set up a separate server URL configuration for the sandbox environment.

6.4 Changes in purchasing process

For example, when you purchase a subscription product for the first time, after the purchase is successful, the Apple server will proactively notify our server of the status. At this point, our server does not need to verify with the Apple server again. Even if you want to verify later, you can also verify at any time through the /inApps/v1/subscriptions interface.

In addition to accepting Apple's notifications for renewal, billing grace period, user refunds, etc., you can also actively request Apple's server to obtain the latest status. For example, when your own server is down or you do not receive Apple's notification for some reason, actively requesting Apple's server to obtain transaction history and transaction status information will play a huge role.

6.5 Server Migration and Upgrade to JWS Format

For StoreKit 2, Apple has abandoned the receipt verification logic, and only needs to provide the originalTransactionId of the transaction to obtain complete transaction information. So how to upgrade from StoreKit 1 to StoreKit 2?

  • Upload the receipt to our server
  • Take the receipt to Apple's server for verification and obtain the originalTransactionId information
  • Go to Apple's server to get historical transaction records based on originalTransactionId, find the specific originalTransactionId, Transaction
  • If it is a subscription product, you can continue to get the subscription status

6.6 Manager family sharing

Manage family sharing. Currently, Apple supports family sharing for non-consumable and automatic subscription items. In addition, Apple will return a field inAppOwnershipType to indicate whether the current user is the primary user of the purchased item. It is more convenient to track the user's status

6.7 Sandbox test

  • Clear sandbox account purchase records
  • Added callback URL configuration for sandbox environment
  • Changing the sandbox account country/region
  • Adjust sandbox renewal frequency
  • Security Improvement: TestFlight version verification ticket will fail

6.8 Summary

  • Previously, we only supported Apple server to send status change notifications to our own server. Now we support actively requesting Apple server to obtain historical transaction records and subscribe to status information.
  • The StoreKit 2 server-side interface supports a new format (JWS format).
  • Sandbox environment testing optimization

7. Customer Support and Handle refunds

Support customers and handle refunds - WWDC21 - Videos - Apple Developer

7.1 How do I identify the in-app purchase made by this customer?

How to identify the user's purchase items. When the user is charged but does not receive the product, the user will return the question and provide a screenshot of the charge information in the Apple mailbox.

Then our server can use the invoice order ID in the screenshot to request the /inApps/v1/lookup/{customer_order_id} interface to find the corresponding transaction information. Then we can verify whether the purchase is successful, whether a refund has been applied, whether the goods need to be reissued, etc.

https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id/inApps/v1/lookup/{customer_order_id}

7.2 How do I lookup this customer's past refunds?

How to check the user's past refund information?

The current situation is that if our server is down or we don’t receive a refund notification, we don’t know whether the user has made a refund. Although StoreKit 2 provides an API for obtaining transaction records, it is not the best way to filter refund transactions through this API. Therefore, Apple has provided a new API to query all refund records of this user, and only needs any original_transaction_id.

https://developer.apple.com/documentation/appstoreserverapi/get_refund_history/inApps/v1/refund/lookup/{original_transaction_id}

7.3 How do I compensate subscribers for a service issue?

How are subscribers compensated for service issues?

For example, when a server problem occurs, in order to retain users/attract more users, how to plan to compensate users. Developers can provide an in-app purchase redemption code (all types of in-app purchases are acceptable) and generate it in the Apple backend. Then let users redeem it in the App Store, or call the presentCodeRedemptionSheet() interface in the App to pop up the system redemption interface:

7.4 How do I appease customers for outages or canceled events?

How to appease customers about interrupted or cancelled events?

The main purpose is to give users some benefits and appease them. Similar to other apps where users can get a one-month membership for free after signing in for a month. However, the expiration time of this method is determined by the backend of the server.

Apple also provides an interface that allows developers to give 90 days of free compensation to users who subscribe to in-app purchases twice a year. That is, for apps with automatic subscriptions, developers can proactively compensate users on the server (extend the order time for free), up to 90 days each time. ​​​​

 ttps : //developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date/inApps/v1/subscription/extend/{original_transaction_id

7.5 How to manage subscriptions in the App

As in 5.2.4 above, a showManageSubscriptions interface is provided to directly call up the subscription management page.

 extension AppStore {

@ available ( iOS 15.0 , * )
@ available ( macOS , unavailable )
@ available ( watchOS , unavailable )
@ available ( tvOS , unavailable )
public static func showManageSubscriptions ( in scene : UIWindowScene ) async throws
}

7.6 How to apply for a refund in the APP

Same as 5.2.5 request refund API above

8. New API Purchase Process

Original API Purchase Process

  • Request Product
  • Initiate a purchase
  • Receive callback and upload receipt after success
  • The server verifies the receipt and ships the goods

New API Purchase Process

The entire payment purchase process is the same as the original API purchase process, except that when uploading transaction information in step 3.1, you no longer need to upload receipt/token information, but only upload transaction_id. The server can use transaction_id to get the transaction result from Apple server, and no longer need to use receipt/token to verify the ticket.

IX. QA

9.1 How to choose between the new API (StoreKit 2) and the original API (StoreKit 1)

  • Choosing a StoreKit API for In-App Purchase | Apple Developer Documentation

If your app relies on any of the following features, you may want to use the original In-App Purchasing API:

  • Provides support for the Volume Purchasing Program (VPP). For more information, see Device Management.
  • Offer your app for pre-order. For more information, see Offering Your App for Pre-order.
  • Your app changes from Premium to Free or vice versa.

Use the original API for existing and legacy applications.

  • If it is a brand new app that only supports iOS 15+ and supports Swift5, and the development cost is not high, it is recommended to use StoreKit 2, but there is no problem using StoreKit 1;

Old App:

  • From the SDK perspective, a separate sub-warehouse can be added to support StoreKit 2. Even if it is not supported now, it will be supported sooner or later.
  • From the host's perspective, whether to introduce new features aggressively, whether the introduction of new features will cause problems with mixed compilation of OC and Swift, and whether corresponding backup plans need to be prepared after the introduction of new features.

9.2 The client uses StoreKit 1, and the server upgrades to the StoreKit 2 API. Can it be used like this?

Can.

For the backend, both Apple Server API V1 and Apple Server API V2 can be used, regardless of whether the client is upgraded to StoreKit 2.

9.3 Will the interaction process change after Native SDK uses StoreKit 2? And will the communication process with the server change?

You can refer to the new API purchase flow chart above.

9.4 Will there be order loss in StoreKit 2? How to solve the problem?

It is still possible that an order may be lost, for example, the purchase is successful, but Apple fails to return the result due to network problems, but this will be easier to resolve.

Solution:

  • During a cold start, you can monitor transaction changes and re-upload after receiving a successful transaction, similar to StoreKit 1.
  • When purchasing, insert the business party's orderID related information into the appAccountToken field in the product. When the user reports that the payment has been deducted but the goods have not been shipped, the user can provide Apple's orderID, which can be used to directly obtain the corresponding transaction information from the Apple server, find Transaction.appAccountToken, and then ship the goods to the user. (Here, an automated processing tool can be made, which only requires the user to provide Apple's orderID to find the corresponding business party's orderID for shipment.)

9.5 If the purchase is successful but the transaction is not finished, will the transaction be reissued after the next cold start?

Yes, it has the same functionality as StoreKit 1, but the APIs are different.

9.6 After upgrading from StoreKit 1 to StoreKit 2, can I see the items I purchased using StoreKit 1?

As you can see, they are compatible with each other.

9.7 For apps using StoreKit1, can we give up reading the local receipt and pass it to the server for verification, and directly use the transaction_id of StoreKit2 to pass it to the Apple server for ticket verification?

Can.

9.8 Regarding the transaction information returned by Apple, can we determine what type of product the transaction status information corresponds to?

Yes, storekit2 will clearly tell us the current product type in the transaction return information. As the server has two different logics for consumables and subscription products, we can easily distinguish whether it is a subscription product and then request the corresponding interface through this field.

<<:  9 decentralized, end-to-end, open-source alternatives to major social media platforms

>>:  Android 13 introduces new restrictions, malware will not be able to use accessibility APIs

Recommend

Will Xiaomi and Midea "divorce"?

Xiaomi and Midea got married without dating each ...

True high-quality watering: When irrigation meets high-tech

In order to ensure the country's food securit...

Product Operation: How to position the product?

Sometimes in the workplace, we encounter products...

What big news did Microsoft make yesterday?

Yesterday was a very special day for people like ...

Xiaomi is on the left and Meizu is on the right

Meizu has been waiting for a long time for the op...

An analysis of the operation of a maternal and infant APP product activity

This month we planned a WeChat fan-raising activi...

To B operation methodology!

In the past year, To B has been very popular, and...

Security concerns, a stumbling block to the explosion of wearables?

[[134520]] When Google Glass was first launched, ...