Use product_entitlement_mapping topic blob for offline entitlements when remote config enabled#3455
Draft
tonidero wants to merge 1 commit into
Conversation
Contributor
Author
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
This was referenced May 7, 2026
Draft
646e5e7 to
3b891ef
Compare
a220a93 to
5554a15
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## toniricodiez/wire-remote-config-manager-on-configure-and-login #3455 +/- ##
==================================================================================================
- Coverage 79.98% 79.98% -0.01%
==================================================================================================
Files 369 371 +2
Lines 14951 15015 +64
Branches 2069 2083 +14
==================================================================================================
+ Hits 11959 12010 +51
- Misses 2153 2164 +11
- Partials 839 841 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
3b891ef to
f25993a
Compare
a00cdc7 to
8feda35
Compare
8feda35 to
63a76b2
Compare
f25993a to
dd05dba
Compare
📸 Snapshot Test591 unchanged
🛸 Powered by Emerge Tools |
63a76b2 to
dbef916
Compare
dd05dba to
bafed02
Compare
dbef916 to
b206de3
Compare
bafed02 to
475500e
Compare
4 tasks
b206de3 to
ed2adf1
Compare
5d17b34 to
03c6f6f
Compare
ed2adf1 to
49cac31
Compare
03c6f6f to
d128c04
Compare
49cac31 to
87d6e93
Compare
d128c04 to
f057f59
Compare
e90d04d to
cd26018
Compare
f057f59 to
08f5042
Compare
e4ffa02 to
7f3963c
Compare
61eade2 to
a822583
Compare
3b1b781 to
9be534c
Compare
a822583 to
0ac6c71
Compare
3b1b781 to
9be534c
Compare
a822583 to
0ac6c71
Compare
9be534c to
e3fd14c
Compare
8b87d1a to
dab20f7
Compare
e3fd14c to
8107411
Compare
2 tasks
dab20f7 to
87f1fd5
Compare
8107411 to
3944eb0
Compare
87f1fd5 to
488944c
Compare
9c184f6 to
b03be9e
Compare
6e1a280 to
c6390a4
Compare
30c82b7 to
6fb4f33
Compare
c6390a4 to
98f0ddd
Compare
f7fa6f8 to
bae0fd9
Compare
4446628 to
d08fa32
Compare
…hen remote config enabled When ENABLE_REMOTE_CONFIG is on, route the offline-entitlements read path through the topic file at noBackupFilesDir/RevenueCat/topics/product_entitlement_mapping/ instead of the /v1/product_entitlement_mapping backend endpoint. The legacy SharedPreferences cache remains as a fallback (e.g. before the first remote-config refresh has landed). - New ProductEntitlementMappingTopicReader reads the blob off the main thread and caches the parsed mapping in memory; concurrent reads coalesce into a single dispatch. - TopicFetcher now invokes a topicUpdatedListener after a successful download so the reader can invalidate its cache mid-session when a fresh blob arrives. - ProductEntitlementMappingSource abstracts read access; PurchasesFactory picks DeviceCacheProductEntitlementMappingSource (flag off) or RemoteConfigProductEntitlementMappingSource (flag on). - OfflineEntitlementsManager.updateProductEntitlementMappingCacheIfStale is a no-op when the flag is on, so Backend.getProductEntitlementMapping is never issued. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bae0fd9 to
f23483c
Compare
d08fa32 to
bf72a8a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Motivation
When
BuildConfig.ENABLE_REMOTE_CONFIGis on, the remote-config pipeline (#3435/#3437/#3439/#3450) downloads theproduct_entitlement_mappingtopic blob to disk. The SDK should consume that file as the source-of-truth for offline entitlements instead of issuing the legacyGET /v1/product_entitlement_mappingcall. When the flag is off, behavior must be unchanged.Description
Behavior gated on
BuildConfig.ENABLE_REMOTE_CONFIG, surfaced into the SDK via a singleuseRemoteConfigForProductEntitlementMappingvalue computed once inPurchasesFactory:OfflineEntitlementsManager.updateProductEntitlementMappingCacheIfStalebecomes a no-op (completion?.invoke(null)and return) when the flag is on, soBackend.getProductEntitlementMappingis never issued.ProductEntitlementMappingTopicReader:suspend fun read(): ProductEntitlementMapping?— reads the single non-rc_topic_file innoBackupFilesDir/RevenueCat/topics/product_entitlement_mapping/and parses it viaProductEntitlementMapping.fromJson(JSONObject(...), loadedFromCache = true).Deferred<ProductEntitlementMapping?>held ininFlight, guarded bysynchronized(lock). Started withCoroutineStart.LAZYsoinFlightis assigned before the body runs (otherwise an eager dispatcher would let the body'sinFlight = nullexecute and.also { inFlight = it }would overwrite it back with a completed deferred, leaking it into the next read).CoroutineScope(SupervisorJob() + dispatcher)(dispatcher: CoroutineDispatcher = Dispatchers.IO, overridable for tests) so cancellation of one caller doesn't cancel the load others are awaiting.fun invalidate()is non-suspend (it's called from a synchronous topic-update listener); just clearscachedunder the lock.SharedPreferencescache if the topic file is absent (first launch, download in-flight). NewProductEntitlementMappingSourceinterface (kept callback-shaped because its consumerPurchasedProductsFetcher.queryActiveProductsis pre-existing callback API):DeviceCacheProductEntitlementMappingSource— synchronous, used when the flag is off.RemoteConfigProductEntitlementMappingSource— owns a smallCoroutineScope, launchesreader.read()(suspend) and forwardstopicMapping ?: deviceCache.getProductEntitlementMapping()to the callback.TopicFetchergains atopicUpdatedListener: ((Topic) -> Unit)? = nullparameter, invoked after a successfuldownloadVerifyAndStore.PurchasesFactoryregisters a listener that callsproductEntitlementMappingTopicReader.invalidate()when thePRODUCT_ENTITLEMENT_MAPPINGtopic is rewritten, so a new manifest takes effect within the same session.PurchasesFactory). HoistsTopicFetcherout ofRemoteConfigManager's inline construction so the same instance is shared between the manager and the reader's invalidation hook. The flag funnels in at one place, matching the existingPurchasesOrchestrator.refreshRemoteConfigIfEnabledgating style.Tests
runTestfor everything;UnconfinedTestDispatcherfor the simple paths andStandardTestDispatcherfor the coalescing test (so two callers can interleave before the body runs):ProductEntitlementMappingTopicReaderTest(new) — missing dir, empty dir, valid blob parsed, second read returns same instance with no extra disk access (counted vianoBackupFilesDirmock answer), ignoresrc_topic_prefix files, corrupt JSON returns null,invalidate()clears the cache, concurrent reads coalesce into a single disk access.OfflineEntitlementsManagerTest— adds a "skips backend fetch whenuseRemoteConfigForProductEntitlementMapping = true" case and confirms the existing 25h staleness logic still runs when the flag is off.PurchasedProductsFetcherTest— already callback-shaped viaProductEntitlementMappingSource; reused unchanged.TopicFetcherTest— adds three listener cases (fires after successful download, doesn't fire on cache hit, doesn't fire on download error).Trade-offs
ByteArrayand decoded as UTF-8 JSON. Realistic mapping size is well under 1 MB; revisit if it grows materially.SharedPreferences. The two stores stay independent — SharedPreferences exists only as a fallback. With the flag on and the topic system healthy, the SharedPreferences cache will go stale; that's intentional.Backend.getProductEntitlementMappingstays callback-based — it's the legacy network path.All changes are
internal; no public API surface change.Stack
Add network scaffolding for remote config endpoint: Add network scaffolding for remote config endpoint #3435Add RemoteConfigManager and TopicFetcher: Add RemoteConfigManager and TopicFetcher #3437Clean up unreferenced topic files after successful remote-config refresh: Clean up unreferenced topic files after successful remote-config refresh #3439Wire RemoteConfigManager refresh on first foreground and identity changes: Wire RemoteConfigManager refresh on first foreground and identity changes (off by default) #3450Checklist
purchases-ios/ hybrids (deferred — feature is not yet wired to any consumer)