Skip to content

Commit a9d8b65

Browse files
committed
refactor(workflows): serve workflows from the config endpoint
WorkflowManager becomes a thin adapter over WorkflowsConfigProvider, which reads the workflows topic through RemoteConfigManager's topic()/body() facade instead of calling the backend per workflow. The old backend-per-workflow path is fully removed: no Backend.getWorkflow, no CDN envelope resolution, no WorkflowsCache reads, no stale-while-revalidate. PublishedWorkflow no longer carries uiConfig — PaywallViewModel now fetches it independently via awaitGetUiConfig() (added in the previous PR) and threads it through workflow presentation directly. OfferingsManager re-gains a WorkflowManager dependency and gates onSuccess on WorkflowManager.awaitWorkflowsReady(), restoring the guarantee that getOfferings doesn't return until workflow data is ready to query — the old code gated on getWorkflowsList() for the same reason; that fetch no longer exists, so the gate now forces the workflows topic to sync instead, which is a no-op on a warm cache since RemoteConfigManager.topic() returns immediately once committed.
1 parent 4a673d3 commit a9d8b65

22 files changed

Lines changed: 677 additions & 3103 deletions

File tree

purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,8 @@ public class Purchases internal constructor(
436436
}
437437

438438
@InternalRevenueCatAPI
439-
public fun workflowIdForOfferingId(offeringId: String): String? =
439+
@JvmSynthetic
440+
public suspend fun workflowIdForOfferingId(offeringId: String): String? =
440441
purchasesOrchestrator.workflowIdForOfferingId(offeringId)
441442

442443
/**

purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,14 @@ import com.revenuecat.purchases.common.remoteconfig.UiConfigProvider
4444
import com.revenuecat.purchases.common.verification.SignatureVerificationMode
4545
import com.revenuecat.purchases.common.verification.SigningManager
4646
import com.revenuecat.purchases.common.warnLog
47-
import com.revenuecat.purchases.common.workflows.FileCachedWorkflowCdnFetcher
48-
import com.revenuecat.purchases.common.workflows.WorkflowDetailResolver
4947
import com.revenuecat.purchases.common.workflows.WorkflowManager
5048
import com.revenuecat.purchases.common.workflows.WorkflowsCache
49+
import com.revenuecat.purchases.common.workflows.WorkflowsConfigProvider
5150
import com.revenuecat.purchases.identity.IdentityManager
5251
import com.revenuecat.purchases.paywalls.FontLoader
5352
import com.revenuecat.purchases.paywalls.OfferingFontPreDownloader
5453
import com.revenuecat.purchases.paywalls.PaywallPresentedCache
5554
import com.revenuecat.purchases.paywalls.events.PaywallStoredEvent
56-
import com.revenuecat.purchases.storage.DefaultFileCache
5755
import com.revenuecat.purchases.storage.DefaultFileRepository
5856
import com.revenuecat.purchases.strings.ConfigureStrings
5957
import com.revenuecat.purchases.strings.Emojis
@@ -66,13 +64,8 @@ import com.revenuecat.purchases.utils.IsDebugBuildProvider
6664
import com.revenuecat.purchases.utils.OfferingImagePreDownloader
6765
import com.revenuecat.purchases.utils.PaywallComponentsImagePreDownloader
6866
import com.revenuecat.purchases.utils.PurchaseParamsValidator
69-
import com.revenuecat.purchases.utils.WorkflowAssetPreDownloader
7067
import com.revenuecat.purchases.utils.isAndroidNOrNewer
7168
import com.revenuecat.purchases.virtualcurrencies.VirtualCurrencyManager
72-
import kotlinx.coroutines.CoroutineScope
73-
import kotlinx.coroutines.Dispatchers
74-
import kotlinx.coroutines.ExperimentalCoroutinesApi
75-
import kotlinx.coroutines.NonCancellable
7669
import java.net.URL
7770
import java.util.concurrent.ExecutorService
7871
import java.util.concurrent.Executors
@@ -86,7 +79,6 @@ internal class PurchasesFactory(
8679
@OptIn(
8780
ExperimentalPreviewRevenueCatPurchasesAPI::class,
8881
InternalRevenueCatAPI::class,
89-
ExperimentalCoroutinesApi::class,
9082
)
9183
@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod")
9284
fun createPurchases(
@@ -275,7 +267,12 @@ internal class PurchasesFactory(
275267

276268
val workflowsCache = if (appConfig.useWorkflows) WorkflowsCache(deviceCache = cache) else null
277269

278-
val remoteConfigManager = if (BuildConfig.ENABLE_REMOTE_CONFIG && !appConfig.customEntitlementComputation) {
270+
// useWorkflows implies the config layer: workflows are served from `/v1/config`, so the manager
271+
// must exist whenever workflows are on, not only when the standalone flag is set. Neither flag
272+
// applies to the customEntitlementComputation flavor, which doesn't serve paywalls this way.
273+
val remoteConfigManager = if (
274+
(BuildConfig.ENABLE_REMOTE_CONFIG || appConfig.useWorkflows) && !appConfig.customEntitlementComputation
275+
) {
279276
RemoteConfigManager(
280277
backend = backend,
281278
diskCache = RemoteConfigDiskCache(contextForStorage),
@@ -386,32 +383,14 @@ internal class PurchasesFactory(
386383
fontLoader = fontLoader,
387384
)
388385

389-
val workflowManager = workflowsCache?.let {
390-
WorkflowManager(
391-
backend = backend,
392-
workflowDetailResolver = WorkflowDetailResolver(
393-
workflowCdnFetcher = FileCachedWorkflowCdnFetcher(
394-
// Dedicated FileRepository instance with a concurrency-limited scope, so workflow
395-
// CDN downloads are capped without affecting the instances used for images/video.
396-
fileRepository = DefaultFileRepository(
397-
fileCacheManager = DefaultFileCache(contextForStorage, "rc_compiled_workflows"),
398-
ioScope = CoroutineScope(
399-
Dispatchers.IO.limitedParallelism(MAX_CONCURRENT_WORKFLOW_CDN_FETCHES) +
400-
NonCancellable,
401-
),
402-
),
403-
),
404-
),
405-
workflowAssetPreDownloader = WorkflowAssetPreDownloader(
406-
paywallComponentsImagePreDownloader = paywallComponentsImagePreDownloader,
407-
offeringFontPreDownloader = offeringFontPreDownloader,
408-
),
409-
workflowsCache = it,
410-
prefetchDispatcher = Dispatcher(
411-
createConcurrentExecutor(),
412-
runningIntegrationTests = runningIntegrationTests,
413-
),
414-
)
386+
// Workflows are served from the `/v1/config` layer: WorkflowManager stays the consumer-facing seam,
387+
// but behind it sit the RemoteConfig stack (sync + blob store + on-demand fetch) and the
388+
// WorkflowsConfigProvider. Lifecycle (foreground refresh, identity clearCache, teardown) is driven
389+
// through remoteConfigManager, which the orchestrator and IdentityManager already own.
390+
val workflowManager = if (appConfig.useWorkflows && remoteConfigManager != null) {
391+
WorkflowManager(WorkflowsConfigProvider(remoteConfigManager))
392+
} else {
393+
null
415394
}
416395

417396
val offeringsManager = OfferingsManager(
@@ -578,12 +557,6 @@ internal class PurchasesFactory(
578557
return Executors.newSingleThreadScheduledExecutor(LowPriorityThreadFactory("revenuecat-events-thread"))
579558
}
580559

581-
// Bounded pool for backend calls that are issued many-at-a-time (workflow detail prefetch), so
582-
// they run concurrently instead of serializing on the single-threaded default executor.
583-
private fun createConcurrentExecutor(): ExecutorService {
584-
return Executors.newScheduledThreadPool(CONCURRENT_BACKEND_CALLS)
585-
}
586-
587560
private class LowPriorityThreadFactory(private val threadName: String) : ThreadFactory {
588561
override fun newThread(r: Runnable?): Thread {
589562
val wrapperRunnable = Runnable {
@@ -597,12 +570,6 @@ internal class PurchasesFactory(
597570
}
598571

599572
companion object {
600-
private const val CONCURRENT_BACKEND_CALLS = 4
601-
602-
// Caps concurrent workflow CDN downloads on the dedicated workflows FileRepository scope, the
603-
// same way CONCURRENT_BACKEND_CALLS caps concurrent workflow detail fetches on prefetchDispatcher.
604-
private const val MAX_CONCURRENT_WORKFLOW_CDN_FETCHES = 4
605-
606573
@VisibleForTesting
607574
internal fun shouldInitializeDiagnostics(
608575
diagnosticsEnabled: Boolean,

purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -617,15 +617,13 @@ internal class PurchasesOrchestrator(
617617
return
618618
}
619619
workflowManager.getWorkflow(
620-
appUserID = identityManager.currentAppUserID,
621620
workflowOrOfferingId = workflowId,
622-
appInBackground = state.appInBackground,
623621
onSuccess = { dispatch { onSuccess(it) } },
624622
onError = { dispatch { onError(it) } },
625623
)
626624
}
627625

628-
fun workflowIdForOfferingId(offeringId: String): String? =
626+
suspend fun workflowIdForOfferingId(offeringId: String): String? =
629627
workflowManager?.workflowIdForOfferingId(offeringId)
630628

631629
fun getUiConfig(

purchases/src/main/kotlin/com/revenuecat/purchases/common/offerings/OfferingsManager.kt

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,7 @@ internal class OfferingsManager(
158158
null,
159159
)
160160
val dispatchSuccess = { dispatch { onSuccess?.invoke(cachedOfferings) } }
161-
workflowManager?.getWorkflowsList(appUserID, appInBackground, onComplete = dispatchSuccess)
162-
?: dispatchSuccess()
161+
workflowManager?.awaitWorkflowsReady(onComplete = dispatchSuccess) ?: dispatchSuccess()
163162
if (isCacheStale) {
164163
log(LogIntent.DEBUG) {
165164
if (appInBackground) {
@@ -200,8 +199,6 @@ internal class OfferingsManager(
200199
appInBackground,
201200
{ body, originalDataSource ->
202201
createAndCacheOfferings(
203-
appUserID = appUserID,
204-
appInBackground = appInBackground,
205202
offeringsJSON = body,
206203
originalDataSource = originalDataSource,
207204
loadedFromDiskCache = false,
@@ -228,8 +225,6 @@ internal class OfferingsManager(
228225
}
229226
} ?: HTTPResponseOriginalSource.MAIN
230227
createAndCacheOfferings(
231-
appUserID = appUserID,
232-
appInBackground = appInBackground,
233228
offeringsJSON = cachedOfferingsResponse,
234229
originalDataSource = originalDataSource,
235230
loadedFromDiskCache = true,
@@ -247,8 +242,6 @@ internal class OfferingsManager(
247242
}
248243

249244
private fun createAndCacheOfferings(
250-
appUserID: String,
251-
appInBackground: Boolean,
252245
offeringsJSON: JSONObject,
253246
originalDataSource: HTTPResponseOriginalSource,
254247
loadedFromDiskCache: Boolean,
@@ -269,13 +262,7 @@ internal class OfferingsManager(
269262
offeringFontPreDownloader.preDownloadOfferingFontsIfNeeded(offeringsResultData.offerings)
270263
offeringsCache.cacheOfferings(offeringsResultData.offerings, offeringsJSON)
271264
val dispatchSuccess = { dispatch { onSuccess?.invoke(offeringsResultData) } }
272-
workflowManager?.getWorkflowsList(
273-
appUserID,
274-
appInBackground,
275-
// Refetch workflows only when these offerings are fresh from the network.
276-
forceRefresh = !loadedFromDiskCache,
277-
onComplete = dispatchSuccess,
278-
) ?: dispatchSuccess()
265+
workflowManager?.awaitWorkflowsReady(onComplete = dispatchSuccess) ?: dispatchSuccess()
279266
},
280267
)
281268
}

0 commit comments

Comments
 (0)