Add RemoteConfigManager and TopicFetcher#3437
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3437 +/- ##
==========================================
+ Coverage 79.45% 79.52% +0.06%
==========================================
Files 364 366 +2
Lines 14647 14754 +107
Branches 1999 2013 +14
==========================================
+ Hits 11638 11733 +95
- Misses 2200 2208 +8
- Partials 809 813 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
1bd9380 to
1a10e10
Compare
39308f9 to
01f4829
Compare
9d39923 to
07ee1a7
Compare
07ee1a7 to
3a747f6
Compare
3a747f6 to
85a2bc8
Compare
| ) { | ||
| val source = response.blobSources.firstOrNull() | ||
| val tasks = response.manifest.topics.mapNotNull { (topic, variants) -> | ||
| val entry = variants[DEFAULT_VARIANT] ?: return@mapNotNull null |
There was a problem hiding this comment.
This is still todo, right now only product entitlement mapping is fetched, which only has a a single entry in the topic, which is the default.
a4228d0 to
8e9c782
Compare
8e9c782 to
29cea95
Compare
RemoteConfigManager calls Backend.getRemoteConfig, picks the first
asset source and the DEFAULT variant for each known topic, and
delegates per-topic downloads to TopicFetcher.
TopicFetcher downloads each topic asset into noBackupFilesDir/RevenueCat/topics/{topic_key}/{blob_ref},
verifies the bytes against the assetBlobRef SHA-256, and uses a temp
file plus atomic rename to avoid partial writes. Existing files are
trusted by name (filename = SHA-256 hash) and skip download.
Both classes are wired up but not yet invoked from PurchasesOrchestrator;
that lands in a follow-up stacked PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update RemoteConfigManager and TopicFetcher to reference the renamed types (AssetSource -> Source, assetSources -> sources, asset_blob_ref -> blob_ref). No behavior change — purely a rename to match the trimmed wire surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tcher Follows the schema and URL change in the parent branch: the manager now reads `response.blobSources` instead of `response.sources`, the fetcher takes a `BlobSource` (the renamed data class), and `updateRemoteConfigIfNeeded` no longer accepts an `appUserID` since /v2/config is not user-scoped.
29cea95 to
ee20e11
Compare
ajpallares
left a comment
There was a problem hiding this comment.
Some initial comments. But this is looking good!
ee20e11 to
ba8adae
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ba8adae. Configure here.
…ceeds + Update tests to use lowercase defaults
ajpallares
left a comment
There was a problem hiding this comment.
I think it looks good! Just one minor comment, not a blocker though
| const val TOPICS_ROOT = "RevenueCat/topics" | ||
| const val BLOB_REF_PLACEHOLDER = "{blob_ref}" | ||
| const val MAX_PARALLEL_TOPIC_DOWNLOADS = 4 | ||
| val BLOB_REF_PATTERN = Regex("^[a-zA-Z0-9]+$") |
There was a problem hiding this comment.
Should we tighten BLOB_REF_PATTERN to exactly ^[a-fA-F0-9]{64}$? Right now any alphanumeric string of any length (including non-hex chars like G–Z) passes validation and we only catch obviously-bad values after downloading and recomputing SHA-256. Since blobRef is the SHA-256 hex digest itself, enforcing the exact shape feels like cheap insurance and would make the "accepts mixed-case 64-char hex blobRef" test case actually exercise the real constraint.
There was a problem hiding this comment.
Indeed, I started with that, and then thought to futureproof it by relaxing the constraints... Though it's true that this just means it will fail later when verifying the checksum... So yeah, can change it back 👍
There was a problem hiding this comment.
Will do this in the follow-up PR, so we can merge this and avoid the extra iterations. Thanks again!
**This is an automatic release.** ## RevenueCat SDK ### 📦 Dependency Updates * [RENOVATE] Update dependency gradle to v8.14.5 (#3459) via RevenueCat Git Bot (@RCGitBot) ## RevenueCatUI SDK ### ✨ New Features * Pre-warm image cache for workflow step states (#3447) via Cesar de la Vega (@vegaro) ### Paywallv2 #### ✨ New Features * Add `close_workflow` button action (#3453) via Cesar de la Vega (@vegaro) #### 🐞 Bugfixes * Fix preload VideoComponent fallback override images (#3449) via Cesar de la Vega (@vegaro) ### 🔄 Other Changes * Select blob source by priority and weighted random (#3458) via Toni Rico (@tonidero) * [AUTOMATIC] Update golden test files for backend integration tests (#3473) via RevenueCat Git Bot (@RCGitBot) * Clean up unreferenced topic files after successful remote-config refresh (#3439) via Toni Rico (@tonidero) * Cache remote config response in memory with TTL and persist to disk (#3457) via Toni Rico (@tonidero) * build(deps): bump fastlane from 2.233.1 to 2.234.0 (#3463) via dependabot[bot] (@dependabot[bot]) * Update codelabs links (#3460) via Jaewoong Eum (@skydoves) * Add RemoteConfigManager and TopicFetcher (#3437) via Toni Rico (@tonidero) * Add exit offers support to workflows (#3452) via Cesar de la Vega (@vegaro) * Update baseline profiles (#3461) via RevenueCat Git Bot (@RCGitBot) * Add network scaffolding for remote config endpoint (#3435) via Toni Rico (@tonidero) * test: cover singleStepFallbackId == initialStepId edge case (#3445) via Facundo Menzella (@facumenzella) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: this is a release/versioning update (SNAPSHOT -> final) plus docs deployment path changes, with no functional code changes beyond version constants. > > **Overview** > Finalizes the `10.6.0` release by switching all version references from `10.6.0-SNAPSHOT` to `10.6.0` (root `.version`, `gradle.properties`, `Config.frameworkVersion`, and sample/test app `libs.versions.toml` files). > > Updates documentation publishing to point at the `10.6.0` docs path (CircleCI S3 sync target and `docs/index.html` redirect), and prepends the `10.6.0` section to `CHANGELOG.md`/`CHANGELOG.latest.md`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4da1697. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Motivation
Builds on #3435 by adding the orchestration layer that turns a remote-config response into a populated on-disk cache of topic blobs. With this PR, calling
RemoteConfigManager.updateRemoteConfigIfNeededwill (a) hit the backend for the latest manifest and (b) download every referenced topic blob, verify its SHA-256, and persist it undernoBackupFilesDir. Subsequent stack PRs build on this orchestration.Description
Adds two classes in the
remoteconfigpackage:TopicFetcher— exposessuspend fun fetchTopicIfNeeded(topic, variant, topicEntry, source): PurchasesError?. Given the inputs, downloads the blob fromsource.urlFormat(with{blob_ref}substituted), writes to a temp file, verifies SHA-256 againsttopicEntry.blobRef, and atomically renames intonoBackupFilesDir/RevenueCat/topics/<topic.key>/<blobRef>. The blocking I/O is wrapped inwithContext(Dispatchers.IO). Skips the download entirely when the target file already exists. Errors surface asPurchasesError(NetworkError, ...)for checksum mismatch orIOException → toPurchasesError()for transport failures.RemoteConfigManager— keeps a callback boundary externally (fun updateRemoteConfigIfNeeded(appInBackground, completion)) since its caller (PurchasesOrchestrator) is callback-shaped. Internally launches aprivate suspend fun refresh(...)on a privateCoroutineScope(SupervisorJob() + dispatcher)(dispatcher: CoroutineDispatcher = Dispatchers.IO, overridable for tests). The refresh:Backend.getRemoteConfigviasuspendCancellableCoroutine+safeResume/safeResumeWithException(PurchasesException(error)). The backend network call stays callback-based intentionally.coroutineScope { tasks.map { async { topicFetcher.fetchTopicIfNeeded(...) } }.awaitAll().firstNotNullOfOrNull { it } }. The first error wins; structured concurrency makes this much smaller than a hand-rolledCompletionTracker.Currently only the
DEFAULTvariant of each known topic is downloaded.Tests
runTest+UnconfinedTestDispatcher(testScheduler)for both.coEvery/coVerifyfor the suspendTopicFetcher. Coverage:TopicFetcherTest— cache hit, successful download + verify + persist, URL-format substitution, HTTP failure, IO failure, SHA mismatch, multi-variant filename isolation.RemoteConfigManagerTest— empty-source / empty-topics / no-DEFAULT skip paths, first-error-wins fan-out, backend-error short-circuit, background flag forwarded, null completion.All new types are
internal. No public API surface change.Stack
Add network scaffolding for remote config endpoint: Add network scaffolding for remote config endpoint #3435Clean up unreferenced topic files after successful remote-config refresh: Clean up unreferenced topic files after successful remote-config refresh #3439Checklist
purchases-ios/ hybrids (deferred)Note
Medium Risk
Adds new remote-config orchestration that performs concurrent network downloads and filesystem writes under
noBackupFilesDir, including checksum verification and new error-handling paths. While internal-only, bugs here could impact startup/network behavior and caching correctness.Overview
Adds a new remote-config orchestration layer:
RemoteConfigManager.updateRemoteConfigIfNeededfetches the remote-config manifest fromBackend.getRemoteConfig(bridged into coroutines) and concurrently downloads each topic’sdefaultentry via a newTopicFetcher, returning the first download error (if any).Introduces
TopicFetcherto cache topic blobs on disk undernoBackupFilesDir/RevenueCat/topics/<topic>/<blob_ref>, with blob-ref input validation, limited parallel downloads, temp-file writes, and SHA-256 verification.Refactors
FontLoaderto reuse newUrlConnectionFactory.downloadToFilehelpers, and extendsUrlConnectionFactorywith shared download + checksum-verification utilities. Updates/expands tests to cover the new manager/fetcher behavior and changes the manifest entry key fromDEFAULTtodefaultinRemoteConfigResponseTest.Reviewed by Cursor Bugbot for commit 8cdae44. Bugbot is set up for automated code reviews on this repo. Configure here.