diff --git a/api-tester/src/main/java/com/revenuecat/apitester/kotlin/DangerousSettingsAPI.kt b/api-tester/src/main/java/com/revenuecat/apitester/kotlin/DangerousSettingsAPI.kt index a61ecbbdf8..62a4297ad4 100644 --- a/api-tester/src/main/java/com/revenuecat/apitester/kotlin/DangerousSettingsAPI.kt +++ b/api-tester/src/main/java/com/revenuecat/apitester/kotlin/DangerousSettingsAPI.kt @@ -1,18 +1,10 @@ package com.revenuecat.apitester.kotlin import com.revenuecat.purchases.DangerousSettings -import com.revenuecat.purchases.InternalRevenueCatAPI @Suppress("unused", "UNUSED_VARIABLE") private class DangerousSettingsAPI { fun check(dangerousSettings: DangerousSettings) { val autoSync: Boolean = dangerousSettings.autoSyncPurchases } - - @OptIn(InternalRevenueCatAPI::class) - fun checkInternalRevenueCatAPIs() { - val forWorkflows: DangerousSettings = DangerousSettings.forWorkflows() - val forWorkflowsNoSync: DangerousSettings = DangerousSettings.forWorkflows(autoSyncPurchases = false) - val useWorkflows: Boolean = forWorkflows.useWorkflows - } } diff --git a/purchases/api-defaults-bc7.txt b/purchases/api-defaults-bc7.txt index 7a82f0d140..ab7d2ccdca 100644 --- a/purchases/api-defaults-bc7.txt +++ b/purchases/api-defaults-bc7.txt @@ -88,9 +88,7 @@ package com.revenuecat.purchases { @dev.drewhamilton.poko.Poko @kotlinx.parcelize.Parcelize public final class DangerousSettings implements android.os.Parcelable { ctor public DangerousSettings(optional boolean autoSyncPurchases); method public boolean getAutoSyncPurchases(); - method public boolean getUseWorkflows(); property public final boolean autoSyncPurchases; - property public final boolean useWorkflows; field public static final com.revenuecat.purchases.DangerousSettings.Companion Companion; } diff --git a/purchases/api-defauts.txt b/purchases/api-defauts.txt index 7a82f0d140..ab7d2ccdca 100644 --- a/purchases/api-defauts.txt +++ b/purchases/api-defauts.txt @@ -88,9 +88,7 @@ package com.revenuecat.purchases { @dev.drewhamilton.poko.Poko @kotlinx.parcelize.Parcelize public final class DangerousSettings implements android.os.Parcelable { ctor public DangerousSettings(optional boolean autoSyncPurchases); method public boolean getAutoSyncPurchases(); - method public boolean getUseWorkflows(); property public final boolean autoSyncPurchases; - property public final boolean useWorkflows; field public static final com.revenuecat.purchases.DangerousSettings.Companion Companion; } diff --git a/purchases/api-entitlement.txt b/purchases/api-entitlement.txt index 24f8305f33..862cf61482 100644 --- a/purchases/api-entitlement.txt +++ b/purchases/api-entitlement.txt @@ -76,9 +76,7 @@ package com.revenuecat.purchases { @dev.drewhamilton.poko.Poko @kotlinx.parcelize.Parcelize public final class DangerousSettings implements android.os.Parcelable { ctor public DangerousSettings(optional boolean autoSyncPurchases); method public boolean getAutoSyncPurchases(); - method public boolean getUseWorkflows(); property public final boolean autoSyncPurchases; - property public final boolean useWorkflows; field public static final com.revenuecat.purchases.DangerousSettings.Companion Companion; } diff --git a/purchases/src/main/baseline-prof.txt b/purchases/src/main/baseline-prof.txt index fab1e8b040..1438fb2406 100644 --- a/purchases/src/main/baseline-prof.txt +++ b/purchases/src/main/baseline-prof.txt @@ -98,12 +98,11 @@ Lcom/revenuecat/purchases/DangerousSettings; HSPLcom/revenuecat/purchases/DangerousSettings;->()V HSPLcom/revenuecat/purchases/DangerousSettings;->(Z)V HSPLcom/revenuecat/purchases/DangerousSettings;->(ZILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/revenuecat/purchases/DangerousSettings;->(ZZZZZ)V +HSPLcom/revenuecat/purchases/DangerousSettings;->(ZZZZ)V HSPLcom/revenuecat/purchases/DangerousSettings;->getApplyObfuscatedAccountIdToSubscriptionChanges$purchases_defaultsBc8Release()Z HSPLcom/revenuecat/purchases/DangerousSettings;->getAutoSyncPurchases()Z HSPLcom/revenuecat/purchases/DangerousSettings;->getCustomEntitlementComputation$purchases_defaultsBc8Release()Z HSPLcom/revenuecat/purchases/DangerousSettings;->getUiPreviewMode$purchases_defaultsBc8Release()Z -HSPLcom/revenuecat/purchases/DangerousSettings;->getUseWorkflows()Z Lcom/revenuecat/purchases/DangerousSettings$Companion; HSPLcom/revenuecat/purchases/DangerousSettings$Companion;->()V HSPLcom/revenuecat/purchases/DangerousSettings$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -2199,4 +2198,4 @@ Lcom/revenuecat/purchases/utils/Result$Success; Lcom/revenuecat/purchases/utils/SerializationException; Lcom/revenuecat/purchases/utils/UrlConnectionFactory; Lcom/revenuecat/purchases/virtualcurrencies/VirtualCurrencyManager; -HSPLcom/revenuecat/purchases/virtualcurrencies/VirtualCurrencyManager;->(Lcom/revenuecat/purchases/identity/IdentityManager;Lcom/revenuecat/purchases/common/caching/DeviceCache;Lcom/revenuecat/purchases/common/Backend;Lcom/revenuecat/purchases/common/AppConfig;)V \ No newline at end of file +HSPLcom/revenuecat/purchases/virtualcurrencies/VirtualCurrencyManager;->(Lcom/revenuecat/purchases/identity/IdentityManager;Lcom/revenuecat/purchases/common/caching/DeviceCache;Lcom/revenuecat/purchases/common/Backend;Lcom/revenuecat/purchases/common/AppConfig;)V diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt index 1850b09419..df7d792e3e 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt @@ -22,20 +22,12 @@ public class DangerousSettings internal constructor( internal val uiPreviewMode: Boolean = false, internal val applyObfuscatedAccountIdToSubscriptionChanges: Boolean = false, - - /** - * Enables RevenueCat Workflows (multipage paywalls). Internal RevenueCat use only. - */ - @InternalRevenueCatAPI - public val useWorkflows: Boolean = false, ) : Parcelable { - @OptIn(InternalRevenueCatAPI::class) public constructor(autoSyncPurchases: Boolean = true) : this( autoSyncPurchases = autoSyncPurchases, customEntitlementComputation = false, uiPreviewMode = false, applyObfuscatedAccountIdToSubscriptionChanges = false, - useWorkflows = false, ) public companion object { @@ -52,19 +44,5 @@ public class DangerousSettings internal constructor( uiPreviewMode = true, applyObfuscatedAccountIdToSubscriptionChanges = false, ) - - /** - * Creates a [DangerousSettings] with RevenueCat Workflows (multipage paywalls) enabled. - * Internal RevenueCat use only; behavior may change without warning. - */ - @InternalRevenueCatAPI - @JvmStatic - public fun forWorkflows(autoSyncPurchases: Boolean = true): DangerousSettings = DangerousSettings( - autoSyncPurchases = autoSyncPurchases, - customEntitlementComputation = false, - uiPreviewMode = false, - applyObfuscatedAccountIdToSubscriptionChanges = false, - useWorkflows = true, - ) } } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt index a1772d8a40..d94f9a0774 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt @@ -268,7 +268,7 @@ internal class PurchasesFactory( localeProvider = localeProvider, ) - val workflowsCache = if (appConfig.useWorkflows) WorkflowsCache(deviceCache = cache) else null + val workflowsCache = WorkflowsCache(deviceCache = cache) val identityManager = IdentityManager( cache, @@ -366,33 +366,31 @@ internal class PurchasesFactory( fontLoader = fontLoader, ) - val workflowManager = workflowsCache?.let { - WorkflowManager( - backend = backend, - workflowDetailResolver = WorkflowDetailResolver( - workflowCdnFetcher = FileCachedWorkflowCdnFetcher( - // Dedicated FileRepository instance with a concurrency-limited scope, so workflow - // CDN downloads are capped without affecting the instances used for images/video. - fileRepository = DefaultFileRepository( - fileCacheManager = DefaultFileCache(contextForStorage, "rc_compiled_workflows"), - ioScope = CoroutineScope( - Dispatchers.IO.limitedParallelism(MAX_CONCURRENT_WORKFLOW_CDN_FETCHES) + - NonCancellable, - ), + val workflowManager = WorkflowManager( + backend = backend, + workflowDetailResolver = WorkflowDetailResolver( + workflowCdnFetcher = FileCachedWorkflowCdnFetcher( + // Dedicated FileRepository instance with a concurrency-limited scope, so workflow + // CDN downloads are capped without affecting the instances used for images/video. + fileRepository = DefaultFileRepository( + fileCacheManager = DefaultFileCache(contextForStorage, "rc_compiled_workflows"), + ioScope = CoroutineScope( + Dispatchers.IO.limitedParallelism(MAX_CONCURRENT_WORKFLOW_CDN_FETCHES) + + NonCancellable, ), ), ), - workflowAssetPreDownloader = WorkflowAssetPreDownloader( - paywallComponentsImagePreDownloader = paywallComponentsImagePreDownloader, - offeringFontPreDownloader = offeringFontPreDownloader, - ), - workflowsCache = it, - prefetchDispatcher = Dispatcher( - createConcurrentExecutor(), - runningIntegrationTests = runningIntegrationTests, - ), - ) - } + ), + workflowAssetPreDownloader = WorkflowAssetPreDownloader( + paywallComponentsImagePreDownloader = paywallComponentsImagePreDownloader, + offeringFontPreDownloader = offeringFontPreDownloader, + ), + workflowsCache = workflowsCache, + prefetchDispatcher = Dispatcher( + createConcurrentExecutor(), + runningIntegrationTests = runningIntegrationTests, + ), + ) val offeringsManager = OfferingsManager( offeringsCache, diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/AppConfig.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/AppConfig.kt index e2773d0a04..788452c6f6 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/AppConfig.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/AppConfig.kt @@ -3,7 +3,6 @@ package com.revenuecat.purchases.common import android.content.Context import com.revenuecat.purchases.APIKeyValidator import com.revenuecat.purchases.DangerousSettings -import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.PurchasesAreCompletedBy import com.revenuecat.purchases.Store import com.revenuecat.purchases.finishTransactions @@ -66,10 +65,6 @@ internal class AppConfig( val applyObfuscatedAccountIdToSubscriptionChanges: Boolean get() = dangerousSettings.applyObfuscatedAccountIdToSubscriptionChanges - @OptIn(InternalRevenueCatAPI::class) - val useWorkflows: Boolean - get() = dangerousSettings.useWorkflows - val playStoreVersionName = context.playStoreVersionName val playServicesVersionName = context.playServicesVersionName diff --git a/purchases/src/test/java/com/revenuecat/purchases/DangerousSettingsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/DangerousSettingsTest.kt index d983afae6b..eda32dfe4c 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/DangerousSettingsTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/DangerousSettingsTest.kt @@ -40,22 +40,4 @@ class DangerousSettingsTest { assertThat(dangerousSettings.customEntitlementComputation).isFalse assertThat(dangerousSettings.applyObfuscatedAccountIdToSubscriptionChanges).isFalse } - - @OptIn(InternalRevenueCatAPI::class) - @Test - fun `default useWorkflows is false`() { - val dangerousSettings = DangerousSettings() - assertThat(dangerousSettings.useWorkflows).isFalse - } - - @OptIn(InternalRevenueCatAPI::class) - @Test - fun `forWorkflows sets useWorkflows to true and leaves other settings at defaults`() { - val dangerousSettings = DangerousSettings.forWorkflows() - assertThat(dangerousSettings.useWorkflows).isTrue - assertThat(dangerousSettings.autoSyncPurchases).isTrue - assertThat(dangerousSettings.customEntitlementComputation).isFalse - assertThat(dangerousSettings.uiPreviewMode).isFalse - assertThat(dangerousSettings.applyObfuscatedAccountIdToSubscriptionChanges).isFalse - } } diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/AppConfigTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/AppConfigTest.kt index 9f21738375..5a360e1c7a 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/AppConfigTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/AppConfigTest.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.APIKeyValidator import com.revenuecat.purchases.DangerousSettings -import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.PurchasesAreCompletedBy.MY_APP import com.revenuecat.purchases.PurchasesAreCompletedBy.REVENUECAT import com.revenuecat.purchases.Store @@ -507,8 +506,7 @@ class AppConfigTest { "autoSyncPurchases=true, " + "customEntitlementComputation=false, " + "uiPreviewMode=false, " + - "applyObfuscatedAccountIdToSubscriptionChanges=false, " + - "useWorkflows=false), " + + "applyObfuscatedAccountIdToSubscriptionChanges=false), " + "languageTag='', " + "versionName='', " + "packageName='', " + @@ -551,33 +549,4 @@ class AppConfigTest { } // endregion Fallback API host - - @OptIn(InternalRevenueCatAPI::class) - @Test - fun `useWorkflows reflects dangerousSettings`() { - val enabled = AppConfig( - context = mockk(relaxed = true), - purchasesAreCompletedBy = REVENUECAT, - showInAppMessagesAutomatically = false, - platformInfo = PlatformInfo(flavor = "native", version = "3.2.0"), - proxyURL = null, - store = Store.PLAY_STORE, - isDebugBuild = false, - apiKeyValidationResult = APIKeyValidator.ValidationResult.VALID, - dangerousSettings = DangerousSettings.forWorkflows(), - ) - assertThat(enabled.useWorkflows).isTrue - - val disabled = AppConfig( - context = mockk(relaxed = true), - purchasesAreCompletedBy = REVENUECAT, - showInAppMessagesAutomatically = false, - platformInfo = PlatformInfo(flavor = "native", version = "3.2.0"), - proxyURL = null, - store = Store.PLAY_STORE, - isDebugBuild = false, - apiKeyValidationResult = APIKeyValidator.ValidationResult.VALID, - ) - assertThat(disabled.useWorkflows).isFalse - } } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt index baab916072..513fc428d1 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt @@ -24,7 +24,6 @@ internal class MockPurchasesType( override val purchasesAreCompletedBy: PurchasesAreCompletedBy = PurchasesAreCompletedBy.REVENUECAT, override val storefrontCountryCode: String? = null, override val customerCenterListener: CustomerCenterListener? = null, - override val useWorkflows: Boolean = false, ) : PurchasesType { override suspend fun awaitPurchase(purchaseParams: PurchaseParams.Builder): PurchaseResult { throw NotImplementedError("Mock implementation for previews only") diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt index fcf9854671..0c248d7183 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt @@ -163,7 +163,6 @@ internal class PaywallViewModelImpl( private val shouldDisplayBlock: ((CustomerInfo) -> Boolean)?, preview: Boolean = false, private val productChangeCalculator: ProductChangeCalculator = ProductChangeCalculator(purchases), - private val useWorkflowsEndpoint: Boolean = purchases.useWorkflows, private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ViewModel(), PaywallViewModel { private val variableDataProvider = VariableDataProvider(resourceProvider, preview) @@ -761,12 +760,11 @@ internal class PaywallViewModelImpl( val resolvedOfferingSelection = resolveOfferingSelection(offeringSelection) val selectedOffering = resolvedOfferingSelection.selectedOffering - // When workflows are enabled, every non-legacy paywall is served through the /workflows - // endpoint. `offering.paywall == null` is the durable marker of a non-legacy (workflow) - // paywall: a legacy v1 paywall always carries `offering.paywall`, and that field stays - // even after `paywallComponents` is removed and all V2 paywalls move to workflows. We - // deliberately do NOT gate on `paywallComponents`, which is going away. - if (useWorkflowsEndpoint && selectedOffering != null && selectedOffering.paywall == null) { + // Every non-legacy paywall is served through the /workflows endpoint. `offering.paywall == null` + // is the durable marker of a non-legacy (workflow) paywall: a legacy v1 paywall always carries + // `offering.paywall`, and that field stays even after `paywallComponents` is removed and all V2 + // paywalls move to workflows. We deliberately do NOT gate on `paywallComponents`, which is going away. + if (selectedOffering != null && selectedOffering.paywall == null) { presentWorkflow(selectedOffering, resolvedOfferingSelection.offeringsForExitOfferLookup) return } diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PurchasesType.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PurchasesType.kt index 8e8ca4f738..4063dfa71d 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PurchasesType.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PurchasesType.kt @@ -70,8 +70,6 @@ internal interface PurchasesType { suspend fun awaitGetWorkflow(workflowId: String): WorkflowDataResult fun workflowIdForOfferingId(offeringId: String): String? - - val useWorkflows: Boolean } @Suppress("TooManyFunctions") @@ -149,8 +147,4 @@ internal class PurchasesImpl(private val purchases: Purchases = Purchases.shared @OptIn(InternalRevenueCatAPI::class) override fun workflowIdForOfferingId(offeringId: String): String? = purchases.workflowIdForOfferingId(offeringId) - - @OptIn(InternalRevenueCatAPI::class) - override val useWorkflows: Boolean - get() = purchases.currentConfiguration.dangerousSettings.useWorkflows } diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialogTests.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialogTests.kt index 244d20855d..feeb2ad29d 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialogTests.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialogTests.kt @@ -10,10 +10,12 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.Offering +import com.revenuecat.purchases.Offerings import com.revenuecat.purchases.Package import com.revenuecat.purchases.DangerousSettings import com.revenuecat.purchases.Purchases import com.revenuecat.purchases.PurchasesAreCompletedBy +import com.revenuecat.purchases.interfaces.ReceiveOfferingsCallback import com.revenuecat.purchases.paywalls.components.ButtonComponent import com.revenuecat.purchases.paywalls.components.PackageComponent import com.revenuecat.purchases.paywalls.components.PartialTextComponent @@ -33,10 +35,15 @@ import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConf import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData import com.revenuecat.purchases.paywalls.components.properties.ColorInfo import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.common.workflows.PublishedWorkflow +import com.revenuecat.purchases.common.workflows.WorkflowDataResult +import com.revenuecat.purchases.common.workflows.WorkflowScreen +import com.revenuecat.purchases.common.workflows.WorkflowStep import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData import com.revenuecat.purchases.ui.revenuecatui.helpers.UiConfig import com.revenuecat.purchases.ui.revenuecatui.helpers.nonEmptyMapOf import io.mockk.Runs +import io.mockk.coEvery import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -67,6 +74,7 @@ class PaywallDialogTests { every { mockPurchases.storefrontCountryCode } returns "US" every { mockPurchases.preferredUILocaleOverride } returns null every { mockPurchases.track(any()) } just Runs + every { mockPurchases.workflowIdForOfferingId(any()) } returns null every { mockPurchases.currentConfiguration } returns mockk { every { dangerousSettings } returns DangerousSettings() } @@ -81,16 +89,33 @@ class PaywallDialogTests { fun `PaywallDialog dismisses after toggling sticky footer control on first presentation`() { var dismissCount = 0 + // Workflows are always on, so a non-legacy (paywall == null) offering is served through the + // /workflows endpoint. Wrap the offering's components in a single-screen workflow so the dialog + // renders the same sticky-footer content it did under the pre-workflows path. + val offering = fakeOffering() + val components = offering.paywallComponents!! + every { mockPurchases.getOfferings(any()) } answers { + firstArg().onReceived( + Offerings(current = offering, all = mapOf(offering.identifier to offering)), + ) + } + coEvery { mockPurchases.awaitGetWorkflow(any()) } returns + WorkflowDataResult(singleScreenWorkflow(components.data), null) + composeTestRule.setContent { PaywallDialog( PaywallDialogOptions.Builder() - .setOffering(fakeOffering()) + .setOffering(offering) .setDismissRequest { dismissCount++ } .build(), ) } with(composeTestRule) { + // The workflow paywall is fetched asynchronously, so wait until its sticky-footer + // toggle is rendered before interacting with it. + waitUntil { onAllNodes(isToggleable()).fetchSemanticsNodes().isNotEmpty() } + onNode(isToggleable()) .assertIsOn() .performClick() @@ -177,6 +202,25 @@ class PaywallDialogTests { ) } + private fun singleScreenWorkflow(data: PaywallComponentsData): PublishedWorkflow { + val screen = WorkflowScreen( + templateName = data.templateName, + assetBaseURL = data.assetBaseURL, + componentsConfig = data.componentsConfig, + componentsLocalizations = data.componentsLocalizations, + defaultLocaleIdentifier = data.defaultLocaleIdentifier, + offeringIdentifier = "offering-id", + ) + return PublishedWorkflow( + id = "offering-id", + displayName = "Test Workflow", + initialStepId = "step-1", + steps = mapOf("step-1" to WorkflowStep(id = "step-1", type = "screen", screenId = "screen-1")), + screens = mapOf("screen-1" to screen), + uiConfig = UiConfig(), + ) + } + private fun tab(id: String, pkg: Package): TabsComponent.Tab = TabsComponent.Tab( id = id, diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt index 11a7713189..93d6a50f72 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/MockPurchasesType.kt @@ -22,7 +22,6 @@ internal class MockPurchasesType( override val purchasesAreCompletedBy: PurchasesAreCompletedBy = PurchasesAreCompletedBy.REVENUECAT, override val storefrontCountryCode: String? = null, override val customerCenterListener: CustomerCenterListener? = null, - override val useWorkflows: Boolean = false, ) : PurchasesType { override suspend fun awaitPurchase(purchaseParams: PurchaseParams.Builder): PurchaseResult { throw NotImplementedError("Mock implementation") diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt index e1acecebe1..43b761080d 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt @@ -232,7 +232,6 @@ class PaywallViewModelTest { every { purchases.track(any()) } just Runs coEvery { purchases.awaitSyncPurchases() } returns customerInfo every { purchases.preferredUILocaleOverride } returns null - every { purchases.useWorkflows } returns false every { purchases.workflowIdForOfferingId(any()) } returns null every { listener.onPurchaseStarted(any()) } just runs @@ -736,6 +735,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents( UiConfig(), PaywallComponentsData( @@ -834,6 +834,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) @@ -852,6 +853,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) @@ -873,6 +875,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) @@ -945,6 +948,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -979,6 +983,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -1003,6 +1008,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -1070,6 +1076,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -1150,6 +1157,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -1194,6 +1202,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create( @@ -1220,6 +1229,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create( @@ -1260,6 +1270,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create( @@ -1316,6 +1327,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -1439,7 +1451,6 @@ class PaywallViewModelTest { TestData.Constants.currentColorScheme, isDarkMode = false, shouldDisplayBlock = null, - useWorkflowsEndpoint = true, ) assertThat(model.workflowState.value?.currentStepId).isEqualTo("step-1") @@ -1498,7 +1509,6 @@ class PaywallViewModelTest { TestData.Constants.currentColorScheme, isDarkMode = false, shouldDisplayBlock = null, - useWorkflowsEndpoint = true, ) coVerify(exactly = 1) { purchases.awaitGetWorkflow(any()) } @@ -1788,6 +1798,7 @@ class PaywallViewModelTest { TestData.Packages.monthly.copy(offeringId), TestData.Packages.annual.copy(offeringId), ), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering).apply { @@ -1837,6 +1848,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -1865,6 +1877,7 @@ class PaywallViewModelTest { TestData.Packages.monthly.copy(offeringId), TestData.Packages.annual.copy(offeringId), ), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) @@ -1896,13 +1909,6 @@ class PaywallViewModelTest { verify(exactly = 0) { purchases.track(any()) } } - @Test - fun `trackPaywallImpression does nothing if offering does not have a paywall`() { - val model = create(offering = defaultOffering.copy(paywall = null)) - model.trackPaywallImpressionIfNeeded() - verify(exactly = 0) { purchases.track(any()) } - } - // region PURCHASE_INITIATED event tests @Test @@ -1913,6 +1919,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -1992,6 +1999,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -2033,6 +2041,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) val model = create(offering = offering) @@ -2221,7 +2230,7 @@ class PaywallViewModelTest { @Test fun `getWebCheckoutUrl returns expected state when no selected package`(): Unit = runBlocking { - val model = create(offering = offeringWithWPL) + val model = create(offering = offeringWithWPL.copy(paywall = defaultOffering.paywall)) assertThat( model.getWebCheckoutUrl(launchWebCheckoutWithCustomUrlAndPackage), ).isEqualTo("https://revenuecat.com?rc_package=%24rc_monthly") @@ -2249,7 +2258,7 @@ class PaywallViewModelTest { @Test fun `getWebCheckoutUrl returns expected state when selected package`(): Unit = runBlocking { - val model = create(offering = offeringWithWPL) + val model = create(offering = offeringWithWPL.copy(paywall = defaultOffering.paywall)) val state = model.state.value as? PaywallState.Loaded.Components ?: error("Expected to have loaded components state") @@ -2299,7 +2308,7 @@ class PaywallViewModelTest { packageParam = "rc_package", ), ) - val model = create(offering = offeringWithWPL) + val model = create(offering = offeringWithWPL.copy(paywall = defaultOffering.paywall)) assertThat( model.getWebCheckoutUrl(action), @@ -2308,7 +2317,7 @@ class PaywallViewModelTest { @Test fun `purchaseButtonInteractionComponentUrl matches resolved launch url for in app browser`(): Unit = runBlocking { - val model = create(offering = offeringWithWPL) + val model = create(offering = offeringWithWPL.copy(paywall = defaultOffering.paywall)) val state = model.state.value as? PaywallState.Loaded.Components ?: error("Expected to have loaded components state") @@ -2328,7 +2337,7 @@ class PaywallViewModelTest { @Test fun `purchaseButtonInteractionComponentUrl matches resolved launch url for deep link`(): Unit = runBlocking { - val model = create(offering = offeringWithWPL) + val model = create(offering = offeringWithWPL.copy(paywall = defaultOffering.paywall)) val state = model.state.value as? PaywallState.Loaded.Components ?: error("Expected to have loaded components state") @@ -2351,7 +2360,7 @@ class PaywallViewModelTest { @Test fun `invalidateCustomerInfoCache invalidates previously obtained customer info`() { - val model = create(offering = offeringWithWPL) + val model = create(offering = offeringWithWPL.copy(paywall = defaultOffering.paywall)) every { purchases.invalidateVirtualCurrenciesCache() } just Runs model.invalidateCustomerInfoCache() verify(exactly = 1) { @@ -2386,6 +2395,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents( UiConfig(), paywallComponentsDataWithProductChange, @@ -2450,6 +2460,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents(UiConfig(), emptyPaywallComponentsData), ) @@ -2515,6 +2526,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents( UiConfig(), paywallComponentsDataWithProductChange, @@ -2642,6 +2654,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents( UiConfig(), paywallComponentsDataWithProductChange, @@ -2720,6 +2733,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents( UiConfig(), paywallComponentsDataWithProductChange, @@ -2798,6 +2812,7 @@ class PaywallViewModelTest { serverDescription = "description", metadata = emptyMap(), availablePackages = listOf(TestData.Packages.monthly, TestData.Packages.annual), + paywall = defaultOffering.paywall, paywallComponents = Offering.PaywallComponents( UiConfig(), paywallComponentsDataWithProductChange, @@ -2859,7 +2874,7 @@ class PaywallViewModelTest { purchases, PaywallOptions.Builder(dismissRequest = { dismissInvoked = true }) .setListener(listener) - .setOffering(offeringWithWPL) + .setOffering(offeringWithWPL.copy(paywall = defaultOffering.paywall)) .setPurchaseLogic(myAppPurchaseLogic) .build(), TestData.Constants.currentColorScheme, @@ -3082,7 +3097,6 @@ class PaywallViewModelTest { TestData.Constants.currentColorScheme, isDarkMode = false, shouldDisplayBlock = null, - useWorkflowsEndpoint = true, ) assertThat(model.workflowState.value?.currentStepId).isEqualTo("step-1") @@ -3091,9 +3105,9 @@ class PaywallViewModelTest { } @Test - fun `when useWorkflows is true and offering has a legacy paywall, does not fetch workflow`() { + fun `when offering has a legacy paywall, does not fetch workflow`() { // defaultOffering has a legacy paywall (offering.paywall != null), so it renders through - // the legacy path and never hits the workflows endpoint, even with workflows enabled. + // the legacy path and never hits the workflows endpoint. PaywallViewModelImpl( MockResourceProvider(), purchases, @@ -3104,14 +3118,13 @@ class PaywallViewModelTest { TestData.Constants.currentColorScheme, isDarkMode = false, shouldDisplayBlock = null, - useWorkflowsEndpoint = true, ) coVerify(exactly = 0) { purchases.awaitGetWorkflow(any()) } } @Test - fun `when useWorkflows is true and offering has a legacy paywall, renders legacy paywall`() { + fun `when offering has a legacy paywall, renders legacy paywall`() { val model = PaywallViewModelImpl( MockResourceProvider(), purchases, @@ -3122,14 +3135,13 @@ class PaywallViewModelTest { TestData.Constants.currentColorScheme, isDarkMode = false, shouldDisplayBlock = null, - useWorkflowsEndpoint = true, ) assertThat(model.state.value).isInstanceOf(PaywallState.Loaded.Legacy::class.java) } @Test - fun `when useWorkflows is true and offering has no legacy paywall, fetches by workflow id from map`() { + fun `when offering has no legacy paywall, fetches by workflow id from map`() { // offeringWithWPL has no legacy paywall (offering.paywall == null), so it is served through // the workflows endpoint. With a mapped workflow id, that id (not the offering id) is used. val workflowId = "wfl-real-id" @@ -3170,7 +3182,6 @@ class PaywallViewModelTest { TestData.Constants.currentColorScheme, isDarkMode = false, shouldDisplayBlock = null, - useWorkflowsEndpoint = true, ) assertThat(model.state.value).isInstanceOf(PaywallState.Loaded.Components::class.java) @@ -3179,10 +3190,10 @@ class PaywallViewModelTest { } @Test - fun `when useWorkflows is true and offering has no legacy paywall but no mapped workflow, fetches by offering id`() { + fun `when offering has no legacy paywall but no mapped workflow, fetches by offering id`() { // offeringWithWPL has no legacy paywall and no mapped workflow (not yet converted). It must - // still hit the workflows endpoint, passing the offering id so the backend lazily converts - // it — rather than falling back to a legacy paywall that does not exist. + // hit the workflows endpoint, passing the offering id so the backend lazily converts it + // rather than falling back to a legacy paywall that does not exist. every { purchases.workflowIdForOfferingId(offeringWithWPL.identifier) } returns null val workflowScreen = WorkflowScreen( templateName = "template", @@ -3220,7 +3231,6 @@ class PaywallViewModelTest { TestData.Constants.currentColorScheme, isDarkMode = false, shouldDisplayBlock = null, - useWorkflowsEndpoint = true, ) assertThat(model.state.value).isInstanceOf(PaywallState.Loaded.Components::class.java) diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelWorkflowTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelWorkflowTest.kt index 1556784146..5fb4cb0e35 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelWorkflowTest.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelWorkflowTest.kt @@ -454,7 +454,6 @@ class PaywallViewModelWorkflowTest { every { storefrontCountryCode } returns "US" every { preferredUILocaleOverride } returns null every { purchasesAreCompletedBy } returns PurchasesAreCompletedBy.REVENUECAT - every { useWorkflows } returns true every { track(any()) } just Runs coEvery { awaitOfferings() } returns testOfferings coEvery { awaitCustomerInfo(any()) } returns mockk {