Skip to content

Add RemoteConfigManager and TopicFetcher#3437

Merged
tonidero merged 8 commits into
mainfrom
toniricodiez/add-remote-config-manager-and-topic-fetcher
May 11, 2026
Merged

Add RemoteConfigManager and TopicFetcher#3437
tonidero merged 8 commits into
mainfrom
toniricodiez/add-remote-config-manager-and-topic-fetcher

Conversation

@tonidero
Copy link
Copy Markdown
Contributor

@tonidero tonidero commented May 5, 2026

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.updateRemoteConfigIfNeeded will (a) hit the backend for the latest manifest and (b) download every referenced topic blob, verify its SHA-256, and persist it under noBackupFilesDir. Subsequent stack PRs build on this orchestration.

Description

Adds two classes in the remoteconfig package:

  • TopicFetcher — exposes suspend fun fetchTopicIfNeeded(topic, variant, topicEntry, source): PurchasesError?. Given the inputs, downloads the blob from source.urlFormat (with {blob_ref} substituted), writes to a temp file, verifies SHA-256 against topicEntry.blobRef, and atomically renames into noBackupFilesDir/RevenueCat/topics/<topic.key>/<blobRef>. The blocking I/O is wrapped in withContext(Dispatchers.IO). Skips the download entirely when the target file already exists. Errors surface as PurchasesError(NetworkError, ...) for checksum mismatch or IOException → toPurchasesError() for transport failures.
  • RemoteConfigManager — keeps a callback boundary externally (fun updateRemoteConfigIfNeeded(appInBackground, completion)) since its caller (PurchasesOrchestrator) is callback-shaped. Internally launches a private suspend fun refresh(...) on a private CoroutineScope(SupervisorJob() + dispatcher) (dispatcher: CoroutineDispatcher = Dispatchers.IO, overridable for tests). The refresh:
    • Bridges the legacy callback-based Backend.getRemoteConfig via suspendCancellableCoroutine + safeResume/safeResumeWithException(PurchasesException(error)). The backend network call stays callback-based intentionally.
    • Fans the topic downloads out with coroutineScope { tasks.map { async { topicFetcher.fetchTopicIfNeeded(...) } }.awaitAll().firstNotNullOfOrNull { it } }. The first error wins; structured concurrency makes this much smaller than a hand-rolled CompletionTracker.

Currently only the DEFAULT variant of each known topic is downloaded.

Tests

runTest + UnconfinedTestDispatcher(testScheduler) for both. coEvery / coVerify for the suspend TopicFetcher. 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

Checklist

  • Unit tests
  • Follow-up issues for 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.updateRemoteConfigIfNeeded fetches the remote-config manifest from Backend.getRemoteConfig (bridged into coroutines) and concurrently downloads each topic’s default entry via a new TopicFetcher, returning the first download error (if any).

Introduces TopicFetcher to cache topic blobs on disk under noBackupFilesDir/RevenueCat/topics/<topic>/<blob_ref>, with blob-ref input validation, limited parallel downloads, temp-file writes, and SHA-256 verification.

Refactors FontLoader to reuse new UrlConnectionFactory.downloadToFile helpers, and extends UrlConnectionFactory with shared download + checksum-verification utilities. Updates/expands tests to cover the new manager/fetcher behavior and changes the manifest entry key from DEFAULT to default in RemoteConfigResponseTest.

Reviewed by Cursor Bugbot for commit 8cdae44. Bugbot is set up for automated code reviews on this repo. Configure here.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 89.76378% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.52%. Comparing base (315b770) to head (8cdae44).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...ecat/purchases/common/remoteconfig/TopicFetcher.kt 79.59% 7 Missing and 3 partials ⚠️
...revenuecat/purchases/utils/UrlConnectionFactory.kt 94.87% 0 Missing and 2 partials ⚠️
...rchases/common/remoteconfig/RemoteConfigManager.kt 97.36% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch 2 times, most recently from 1bd9380 to 1a10e10 Compare May 6, 2026 09:42
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-network-scaffolding branch from 39308f9 to 01f4829 Compare May 6, 2026 13:33
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch 2 times, most recently from 9d39923 to 07ee1a7 Compare May 7, 2026 12:29
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch from 07ee1a7 to 3a747f6 Compare May 7, 2026 15:40
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch from 3a747f6 to 85a2bc8 Compare May 8, 2026 08:36
@tonidero tonidero marked this pull request as ready for review May 8, 2026 09:11
@tonidero tonidero requested a review from a team as a code owner May 8, 2026 09:11
) {
val source = response.blobSources.firstOrNull()
val tasks = response.manifest.topics.mapNotNull { (topic, variants) ->
val entry = variants[DEFAULT_VARIANT] ?: return@mapNotNull null
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch from a4228d0 to 8e9c782 Compare May 8, 2026 10:20
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch from 8e9c782 to 29cea95 Compare May 8, 2026 11:27
Base automatically changed from toniricodiez/add-remote-config-network-scaffolding to main May 8, 2026 11:42
tonidero and others added 5 commits May 8, 2026 14:09
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.
@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch from 29cea95 to ee20e11 Compare May 8, 2026 12:09
Copy link
Copy Markdown
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments. But this is looking good!

@tonidero tonidero force-pushed the toniricodiez/add-remote-config-manager-and-topic-fetcher branch from ee20e11 to ba8adae Compare May 8, 2026 13:48
@tonidero tonidero requested a review from a team as a code owner May 8, 2026 13:48
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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.

@tonidero tonidero requested a review from ajpallares May 11, 2026 11:11
Copy link
Copy Markdown
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]+$")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do this in the follow-up PR, so we can merge this and avoid the extra iterations. Thanks again!

@tonidero tonidero added this pull request to the merge queue May 11, 2026
Merged via the queue into main with commit cb5f336 May 11, 2026
51 checks passed
@tonidero tonidero deleted the toniricodiez/add-remote-config-manager-and-topic-fetcher branch May 11, 2026 12:27
github-merge-queue Bot pushed a commit that referenced this pull request May 13, 2026
**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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants