This is actually a tricky problem to solve. In a perfect world it’s straightforward and additional context can be passed in the same API call as the purchase. The problem is that mobile apps rarely operate in a perfect world.
If the API call is interrupted due to a network error or the app is closed during the request, it could be dropped. This means the additional context would need to be cached, and re-matched with the correct line-item in the receipt to be re-tried later. There’s also the possibility that the customer redeems an in-app purchase through a promo-code, gift card, or some mechanism outside of your app. This means that it’s possible to see a purchase without any additional context.
Because of these known edge-cases, I’m not sure that there’s a way to accomplish this with 100% certainty .
You would need a solution that can operate with only the context available from Apple and achieve the result you want.
Some ideas:
- Use “coins” to unlock content instead of products for each feature. There’s a reason this is such a popular way to do it amongst games and other apps. Using “coins” or other in-app currency simplifies development because your server just needs to deposit the correct amount of coins when a product is purchased - no other context is required besides the product ID.
- Create a non-consumable product for each item that can be unlocked. This makes it really easy for your server to unlock the correct items, and has the added benefit that non-consumables can be restored across devices and after re-installing the app. Downside is you may end up with a huge amount of product IDs to manage and App Store Connect has some limits around the number of product IDs you can create.
- Some combination of 1 & 2. Maybe use non-consumables for lower volume items that don’t “consume” (e.g. new cars) and coins for higher volume items that need to be replenished (e.g. new tires, gas). Maybe you even make specific coins that only work for tires, etc.
- Design a UX to handle cases when a purchase is recorded on your server that is missing additional context. For example, in most cases you could have an API call to send additional context to your server and things will work as expected, but if you run into a scenario where you have a purchase recorded for a customer without additional context you could build a UI for “new tire credits” and have them re-select which car the new tires should go on.
@ryan thanks so much for the detailed reply. Let me mull over the ideas with the team.
Here’s the one we came up with before your reply.
We had a crazy idea where we would set a Subscriber attribute before calling the “.purchase(SKU)” API.
An example would look like this:
Flutter:
Purchases.setAttributes({ "lastCarEdited" : "19417188-0151-4487-9f4b-0548d6c6ddea"});
PurchaserInfo purchaserInfo = await Purchases.purchasePackage(package);
If I understand the system properly, the web-hook event our server would receive would now include the subscriber attribute we need to match the transaction with the car.
{
"api_version": "1.0",
"event": {
....
"store": "APP_STORE",
"subscriber_attributes": {
"$lastCarEdited": {
"updated_at_ms": 1581121853000,
"value": "19417188-0151-4487-9f4b-0548d6c6ddea"
}
},
...
"type": "INITIAL_PURCHASE"
}
}
I see two important points of failure with this approach.
- The only true record which holds the transaction ID and the exact state the “subscriber attributes” were in at the time of the purchase is the “WebHook Event Message”.
- Our team does not have clarity on how RevCat’s retry procedures work with respect to WebHook Events and in the event of a retry scenario, the value of the Subscriber Attribute may change due to another purchase thus changing the “lastCarEdited” value between retires.
Questions:
- We’re obviously not using subscriber attributes as intended in the approach above. Do you foresee any issues we’re not accounting for?
- If a webhook event message fails…and we end up in the 5 minute, 10 minute, 20 minute...loop...will the contents of the message remain the same across all retries no matter what changes in the subscriber’s account between made between each retry?
Hey @Clint Gossett,
Using subscriber attributes could work, but I would still have a system in place to handle edge case scenarios where a purchase is received without one of these attributes - you’d probably have more control here if you used your own database to manage this type of state.
You should never use subscriber attributes themselves to unlock any features or content, since they are writable through the SDK. However, it seems that your approach is safe since it would require a subscriber attribute and a purchase to get access.
We’re obviously not using subscriber attributes as intended in the approach above. Do you foresee any issues we’re not accounting for?
The first to come to mind are what I mentioned above. Know that you could still get a purchase without one of these set, and know that it’s possible for them to be manipulated from the client.
If a webhook event message fails…and we end up in the 5 minute, 10 minute, 20 minute...loop...will the contents of the message remain the same across all retries no matter what changes in the subscriber’s account between made between each retry?
Yes, the body will remain the same across retries. The payload is created when the event is generated and the same payload is sent for retries. However, this doesn’t mean that the customer might make an additional purchase between the retries and fire off an additional webhook event.