-
Notifications
You must be signed in to change notification settings - Fork 96
Improve accuracy of transactions origin Part 3: Merge cached data when posting receipts + Cache amazon data #2989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: sdk-4188-puchases-android-improve-accuracy-and-reliability-of-2
Are you sure you want to change the base?
Conversation
…ity-of-2' into sdk-4188-puchases-android-improve-accuracy-and-reliability-of-3
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## sdk-4188-puchases-android-improve-accuracy-and-reliability-of-2 #2989 +/- ##
===================================================================================================
- Coverage 78.98% 78.97% -0.02%
===================================================================================================
Files 341 341
Lines 13449 13476 +27
Branches 1804 1820 +16
===================================================================================================
+ Hits 10623 10642 +19
+ Misses 2079 2078 -1
- Partials 747 756 +9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ity-of-2' of github.com:RevenueCat/purchases-android into sdk-4188-puchases-android-improve-accuracy-and-reliability-of-3
JayShortway
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesomeee! Some minor comments and some philosophical ones 😄
| return@let mapOf("revision" to it.revision, "rule_id" to it.ruleId) | ||
| }, | ||
| "observer_mode" to !finishTransactions, | ||
| "observer_mode" to (originalObserverMode ?: !finishTransactions), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we even fall back to the current value? That makes the field ambiguous, as it can now mean either "observer mode at time of transaction" or "observer mode at time of posting". Do we even care about the latter? If yes, maybe we should make separate fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I went back and forth on this (see commit 😅 ).
My thoughts in the end were, we should always use the cached value if we have any cached data. If we don't, we should use the current value, since we can assume the purchase was just performed and/or comes from the queue.
My main reason for not separating it is that we're already sending this information on a header for all requests, so maybe we don't really need to send it at all unless we have a cached value. However, not sending it would mean possible changes in the backend, so I decided to leave the current behavior if the purchase is not cached.
But yeah, this is indeed confusing, so might be worth sending this in a new field to avoid confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I think we should avoid confusion, for us and for the backend. It could even lead to some subtly incorrect attributions. Maybe we leave the header as-is and introduce a new field in the body. (I'd also be okay removing the header if the backend isn't reading it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I split this into a separate field: 3878eef
@antoniobg we will need to accept this new field in the backend. It's meaning will be the value of this field at the time when the purchase was made (which could have potentially changed if the value changed before the data was successfully posted)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's a new parameter, can we change the name that matches better the configuration, like purchase_completion_method or completion_method ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed... what do you think of purchase_completed_by, with possible values being my_app and revenuecat? Note that the public API for this is going to be PurchasesAreCompletedBy.MY_APP or PurchasesAreCompletedBy.REVENUECAT. cc @RevenueCat/coresdk @antoniobg
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok renamed in 95af91f
purchases/src/main/kotlin/com/revenuecat/purchases/common/ReceiptInfo.kt
Outdated
Show resolved
Hide resolved
| val presentedPaywall = paywallPresentedCache.getAndRemovePresentedEvent() | ||
| val effectivePaywallData = presentedPaywall?.toPaywallPostReceiptData() | ||
| ?: cachedTransactionMetadata?.paywallPostReceiptData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be a potential source of bugs if we're posting cached receipts while a paywall is showing? It might accidentally attribute the cached receipt to the currently showing paywall, if I understand correctly, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm you're indeed right that this may potentially happen... and could also be happening right now I think, since the event storage mechanism is completely unattached to the posting of the receipt here, we don't know if it's related... We should think on how to fix this issue more permanently... Great catch!!
For this PR, I'm thinking we could potentially check, if there is a cached value for the given token we don't even need to call getAndRemovePresentedEvent above, to avoid missattributions later, but as mentioned, this wouldn't fix the existing issue I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there is a cached value for the given token we don't even need to call getAndRemovePresentedEvent above
True, but we should make sure the paywallPresentedCache doesn't contain stale data in any case.
An additional thing we could do is check if the presented paywall's Offering corresponds to the PresentedOfferingContext we have? Not airtight by any means, but it does allow us to eliminate obvious missattributions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should make sure the paywallPresentedCache doesn't contain stale data in any case.
Right, I was checking and we basically should be clearing the cached paywallPresentedCache when:
- Performing a successful
POST /receipt. - When closing the paywall (or at least asked to be dismissed). This includes:
- When navigating back with the back button
- When clicking on close buttons with the navigate back action
- When navigation to a web checkout url.
I believe this shouldn't leave cached paywall information around.
An additional thing we could do is check if the presented paywall's Offering corresponds to the PresentedOfferingContext we have? Not airtight by any means, but it does allow us to eliminate obvious missattributions.
I do think this is a valid check, but will likely do that in a separate PR, since it seems like a different thing than what we're trying to do in this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the order of operations in a7f7d22.
Will take note and do try to figure a more robust solution for the cached paywall events in a follow-up PR
purchases/src/main/kotlin/com/revenuecat/purchases/common/ReceiptInfo.kt
Outdated
Show resolved
Hide resolved
purchases/src/main/kotlin/com/revenuecat/purchases/common/ReceiptInfo.kt
Show resolved
Hide resolved
purchases/src/main/kotlin/com/revenuecat/purchases/common/ReceiptInfo.kt
Show resolved
Hide resolved
…nd-reliability-of
…ity-of' into sdk-4188-puchases-android-improve-accuracy-and-reliability-of-2
…ity-of-2' of github.com:RevenueCat/purchases-android into sdk-4188-puchases-android-improve-accuracy-and-reliability-of-3
rickvdl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work!
|
|
||
| val presentedPaywall = paywallPresentedCache.getAndRemovePresentedEvent() | ||
| val presentedPaywall = if (cachedTransactionMetadata == null) { | ||
| paywallPresentedCache.getAndRemovePresentedEvent() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also remove the in-memory cached value if we don't use it (if we don't call getAndRemovePresentedEvent()) to prevent it from being used in the next (non-paywall?) purchase?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't, since it's possible the paywall is still displayed while we're syncing purchases in the background. We should only clear it in the same situations we were clearing it before (upon a successful purchase and/or when closing the paywall)
| } else { | ||
| null | ||
| } | ||
| val effectivePaywallData = cachedTransactionMetadata?.paywallPostReceiptData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we decide on keying the in-memory cached presentedPaywall by product id / transaction ID or similar? In order to avoid mixing this data with an unrelated transaction? Not sure if it's a realistic scenario or only theoretical one, but what if a purchase comes in from the queue while this in-memory value is set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the cachedTransactionMetadata's paywall is indeed keyed by the purchase token hash. As for the in memory event (that came from before these PRs), it's indeed a problem I'm thinking about how to solve... It normally shouldn't be an issue since we clear the in memory cache when closing the paywall, but there are edge cases that are not correctly handled, like when making a purchase when PurchasesAreCompletedBy.MY_APP or if for any reason presenting multiple paywalls at once.
purchases/src/main/kotlin/com/revenuecat/purchases/PostReceiptHelper.kt
Outdated
Show resolved
Hide resolved
purchases/src/main/kotlin/com/revenuecat/purchases/common/caching/LocalTransactionMetadata.kt
Outdated
Show resolved
Hide resolved
JayShortway
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good stuff!
| return@let mapOf("revision" to it.revision, "rule_id" to it.ruleId) | ||
| }, | ||
| "observer_mode" to !finishTransactions, | ||
| "purchase_completed_by" to purchasesAreCompletedBy.name.lowercase(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
| * Merges this [ReceiptInfo] with another [ReceiptInfo], giving precedence to the values in this | ||
| * instance when there are conflicts. | ||
| */ | ||
| fun mergeWith(receiptInfo: ReceiptInfo): ReceiptInfo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🫶
544e8d2 to
d7fff68
Compare
…ity-of-2' of github.com:RevenueCat/purchases-android into sdk-4188-puchases-android-improve-accuracy-and-reliability-of-3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR improves transaction origin accuracy by implementing cached data merging when posting receipts and adding Amazon-specific fields to ReceiptInfo. The changes enable better handling of transaction metadata that may have been cached during the purchase flow.
Key Changes
- Added
storeUserIDandmarketplacefields toReceiptInfoto support Amazon store data - Implemented
mergeWith()method inReceiptInfoto intelligently combine current and cached receipt information - Replaced
observerModewithpurchasesAreCompletedByenum inLocalTransactionMetadatafor more explicit transaction completion tracking - Modified
PostReceiptHelperto merge cached transaction metadata (including receipt info, paywall data, and purchasesAreCompletedBy) when posting receipts
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
ReceiptInfo.kt |
Added Amazon fields (storeUserID, marketplace) and implemented mergeWith logic for combining current and cached data |
Backend.kt |
Refactored postReceiptData to use ReceiptInfo fields directly and added purchasesAreCompletedBy parameter |
PostReceiptHelper.kt |
Enhanced to retrieve and merge cached transaction metadata when posting receipts |
LocalTransactionMetadata.kt |
Replaced observerMode boolean with purchasesAreCompletedBy enum for clearer semantics |
AppConfig.kt |
Made purchasesAreCompletedBy publicly accessible |
SyncPurchasesHelper.kt |
Updated to include storeUserID and marketplace in ReceiptInfo |
PurchasesOrchestrator.kt |
Updated to include storeUserID and marketplace in ReceiptInfo for Amazon purchases |
| Test files | Comprehensive test updates and additions covering new merge functionality and cached data scenarios |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "applied_targeting_rule" to receiptInfo.presentedOfferingContext?.targetingContext?.let { | ||
| return@let mapOf("revision" to it.revision, "rule_id" to it.ruleId) | ||
| }, | ||
| "observer_mode" to !finishTransactions, |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both observer_mode and purchase_completed_by are being sent in the request body. This appears to be redundant since they represent similar information. Consider whether both fields are needed or if observer_mode should be removed in favor of purchase_completed_by.
| "observer_mode" to !finishTransactions, |
| isRestore.toString(), | ||
| finishTransactions.toString(), | ||
| subscriberAttributes.toString(), | ||
| receiptInfo.toString(), |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cache key for deduplicating postReceiptData calls doesn't include purchasesAreCompletedBy. This could lead to incorrect request deduplication when the same receipt is posted with different purchasesAreCompletedBy values. Consider adding purchasesAreCompletedBy.toString() to the cacheKey list.
| receiptInfo.toString(), | |
| receiptInfo.toString(), | |
| purchasesAreCompletedBy.toString(), |
| @SerialName("purchases_are_completed_by") | ||
| val purchasesAreCompletedBy: PurchasesAreCompletedBy, |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The serialization field name is changed from observer_mode to purchases_are_completed_by, but the field type has also changed from nullable Boolean to non-nullable PurchasesAreCompletedBy enum. This is a breaking change for persisted data. Any previously cached transactions with observer_mode will fail to deserialize. Consider adding a migration strategy or using a custom deserializer to handle backward compatibility.
| } | ||
| val effectivePaywallData = cachedTransactionMetadata?.paywallPostReceiptData | ||
| ?: presentedPaywall?.toPaywallPostReceiptData() | ||
| val effectiveReceiptInfo = cachedTransactionMetadata?.receiptInfo?.let { receiptInfo.mergeWith(it) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder, if the result of the mergeWith actually results in changes, shouldn't we update the cached metadata with the merged information? Otherwise, a failure in the POST /receipt could result in data loss?
Description
These are the main changes in this PR:
observer_modein post receipt request body. The cached value will have priority over the current one.