Skip to content

refactor(workflows): serve workflows and ui_config from the config endpoint#3708

Draft
vegaro wants to merge 9 commits into
mainfrom
cesar/workflows-config-endpoint-foundation
Draft

refactor(workflows): serve workflows and ui_config from the config endpoint#3708
vegaro wants to merge 9 commits into
mainfrom
cesar/workflows-config-endpoint-foundation

Conversation

@vegaro

@vegaro vegaro commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

  • WorkflowManager becomes a thin adapter over WorkflowsConfigProvider, reading the workflows topic through RemoteConfigManager's topic()/body() facade instead of the old backend-per-workflow path.
  • ui_config is served from its own topic: UiConfigProvider assembles app, localizations, and variable_config into one UiConfig, and the workflow parser now defaults PublishedWorkflow.uiConfig instead of failing when absent.
  • workflowIdForOfferingId becomes suspend so topic reads run on the manager's IO dispatcher.

Test plan

  • :purchases:testDefaultsBc8DebugUnitTest
  • :ui:revenuecatui:testDefaultsBc8DebugUnitTest

tonidero and others added 9 commits July 3, 2026 13:03
Adds suspend topic()/body() reads to RemoteConfigManager. On a cache
miss, body() waits for an in-flight sync or triggers one on demand,
unless the endpoint is disabled or no app user is known.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
topic()/body() and RemoteConfigTopicStore now take a closed
RemoteConfigTopic (Workflows/UiConfig/Sources) instead of a raw topic
string, so consumers can only read known topics. The disk cache stays
string-keyed; the enum maps to its wire name at the store boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two integration tests that read back through RemoteConfigManager's
public topic()/body() facade after a sync persists to disk:

- RemoteConfigManagerIntegrationTest: real disk cache + blob store, mocked
  backend; reads an inline-only item's topic and body off disk.
- ProductionRemoteConfigIntegrationTest: real backend; a cold body() drives
  a genuine /v1/config fetch + persist, then reads an inlined workflows
  blob back off disk through the facade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gate reads when disabled

Refines the read facade on the RemoteConfigManager:

- Rename `body(topic, itemKey)` to `blobData`, with two overloads: a
  reified `blobData<T>` that parses the resolved blob as JSON into a
  `@Serializable` T via the shared JsonProvider, and a `blobData(..., transform)`
  overload for non-JSON blobs the caller decodes itself.
- Drop the inline-content fallback: an item with no `blob_ref` now resolves
  to `null` (its inline data is exposed only through `topic()`), instead of
  returning the config JSON as a payload.
- Rename `RemoteConfiguration.ConfigItem.content` to `metadata`.
- Rename the session kill-switch `isUnavailable`/`unavailable` to
  `isDisabled`/`disabled`, and gate reads: `topic()` and `blobData()` return
  `null` once the endpoint is disabled (4xx), rather than serving cached data.
- Refresh remote config as part of syncAttributesAndOfferingsIfNeeded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reads

- PurchasesCommonTest: syncAttributesAndOfferingsIfNeeded triggers
  RemoteConfigManager.refreshRemoteConfig via the sync completion.
- RemoteConfigManagerTest: reified blobData<T> fetches on demand then
  deserializes, matching the transform overload's wait/download path.
- ProductionRemoteConfigIntegrationTest: end-to-end typed blobData<T> read of
  a workflows blob. Uses a minimal test-only type rather than PublishedWorkflow,
  which the shared defaultJson can't decode (polymorphic component tree) and
  which these lazy paywall workflows omit ui_config for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make RemoteConfigManager.topic() mirror blobData(): when no topic is
committed yet, wait for an in-flight refresh (or trigger one on demand)
and re-read before returning null, so a topic read during the initial
sync returns fresh data instead of a spurious empty result. A committed
topic still returns immediately, and a disabled endpoint or unknown user
returns null with no network call.

Simplify the integration tests to read topic() directly (the cold-load
priming step is now redundant) and add unit coverage mirroring the
blobData self-prime suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dpoint

WorkflowManager stays the consumer-facing seam but becomes a thin adapter
over WorkflowsConfigProvider, which reads the `workflows` topic through
RemoteConfigManager's topic()/body() facade. The old backend-per-workflow
path is unwired: no Backend.getWorkflow, no CDN envelope resolution, no
WorkflowsCache reads, no stale-while-revalidate, and offerings no longer
fan out a workflows-list fetch.

ui_config is served from its own topic: UiConfigProvider assembles the
`app`, `localizations` and `variable_config` parts into one UiConfig and
the workflow parser swaps it in (PublishedWorkflow.uiConfig now defaults
instead of failing when absent).

The workflowIdForOfferingId chain (Purchases → orchestrator → manager →
provider) becomes suspend so topic reads run on the manager's IO
dispatcher instead of the caller's thread; PaywallViewModel already calls
it from a suspend context.

PurchasesFactory constructs the RemoteConfigManager whenever useWorkflows
is on (the config layer is now a prerequisite for workflows), keeping
ENABLE_REMOTE_CONFIG as the standalone flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@RevenueCat-Danger-Bot

Copy link
Copy Markdown
1 Error
🚫 This PR changes 690 lines of production Kotlin/Java code, over the 300-line limit. Split it into smaller PRs, or add the skip-pr-lines-changed-check label to bypass.

Generated by 🚫 Danger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants