Skip to content

Commit 24ee300

Browse files
tonideroclaude
andcommitted
Allow Test Store in release build for e2e tests via internal opt-in
The Maestro e2e job now runs against a minified release build, which is non-debuggable and trips the guard in PurchasesFactory that blocks Test Store keys in release builds (it shows SimulatedStoreErrorDialogActivity, crashing the app on launch). uiPreviewMode bypasses the guard but disables billing, so it can't drive the real purchase flow. Add an internal opt-in, DangerousSettings.forTestStoreInReleaseBuild() (@InternalRevenueCatAPI, mirroring forPreviewMode()), that the e2etests app sets so it can use the Test Store in a true release build. Extend the guard to honor it. For RevenueCat-internal testing only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a5a60e9 commit 24ee300

4 files changed

Lines changed: 62 additions & 6 deletions

File tree

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ public class DangerousSettings internal constructor(
2222
internal val uiPreviewMode: Boolean = false,
2323

2424
internal val applyObfuscatedAccountIdToSubscriptionChanges: Boolean = false,
25+
26+
internal val allowTestStoreInReleaseBuild: Boolean = false,
2527
) : Parcelable {
26-
public constructor(autoSyncPurchases: Boolean = true) : this(autoSyncPurchases, false, false, false)
28+
public constructor(autoSyncPurchases: Boolean = true) : this(autoSyncPurchases, false, false, false, false)
2729

2830
public companion object {
2931
/**
@@ -39,5 +41,17 @@ public class DangerousSettings internal constructor(
3941
uiPreviewMode = true,
4042
applyObfuscatedAccountIdToSubscriptionChanges = false,
4143
)
44+
45+
/**
46+
* Creates a [DangerousSettings] that allows configuring the SDK with a Test Store API key
47+
* in a release (non-debuggable) build, bypassing the safety check that otherwise blocks it.
48+
*
49+
* For RevenueCat-internal end-to-end testing only. Do not use in production apps.
50+
*/
51+
@InternalRevenueCatAPI
52+
@JvmStatic
53+
public fun forTestStoreInReleaseBuild(): DangerousSettings = DangerousSettings(
54+
allowTestStoreInReleaseBuild = true,
55+
)
4256
}
4357
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -518,10 +518,14 @@ internal class PurchasesFactory(
518518

519519
val apiKeyValidationResult = apiKeyValidator.validateAndLog(apiKey, store)
520520

521-
if (!isDebugBuild() &&
522-
apiKeyValidationResult == APIKeyValidator.ValidationResult.SIMULATED_STORE &&
523-
!dangerousSettings.uiPreviewMode
524-
) {
521+
// Test Store keys are only meant for development. uiPreviewMode and
522+
// allowTestStoreInReleaseBuild are internal opt-ins that intentionally bypass this guard.
523+
val isTestStoreKeyInReleaseBuild = !isDebugBuild() &&
524+
apiKeyValidationResult == APIKeyValidator.ValidationResult.SIMULATED_STORE
525+
val testStoreReleaseBuildAllowed = dangerousSettings.uiPreviewMode ||
526+
dangerousSettings.allowTestStoreInReleaseBuild
527+
528+
if (isTestStoreKeyInReleaseBuild && !testStoreReleaseBuildAllowed) {
525529
val redactedApiKey = apiKeyValidator.redactApiKey(apiKey)
526530
errorLog(
527531
error = PurchasesError(

purchases/src/test/java/com/revenuecat/purchases/PurchasesFactoryTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,36 @@ class PurchasesFactoryTest {
165165
verify(exactly = 0) { applicationMock.startActivity(any()) }
166166
}
167167

168+
@OptIn(InternalRevenueCatAPI::class)
169+
@Test
170+
fun `configuring SDK with simulated store api key in release mode and allowTestStoreInReleaseBuild does not show error activity`() {
171+
// Arrange
172+
purchasesFactory = PurchasesFactory(
173+
isDebugBuild = { false },
174+
apiKeyValidator = apiKeyValidatorMock,
175+
)
176+
val applicationContextMock = mockk<Application>()
177+
every {
178+
applicationMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET)
179+
} returns PackageManager.PERMISSION_GRANTED
180+
every {
181+
applicationMock.applicationContext
182+
} returns applicationContextMock
183+
every {
184+
apiKeyValidatorMock.validateAndLog("fakeApiKey", Store.PLAY_STORE)
185+
} returns APIKeyValidator.ValidationResult.SIMULATED_STORE
186+
187+
// Act
188+
purchasesFactory.validateConfiguration(
189+
createConfiguration(
190+
dangerousSettings = DangerousSettings.forTestStoreInReleaseBuild(),
191+
),
192+
)
193+
194+
// Assert
195+
verify(exactly = 0) { applicationMock.startActivity(any()) }
196+
}
197+
168198
// region shouldInitializeDiagnostics
169199

170200
@Test

test-apps/e2etests/src/main/java/com/revenuecat/e2etests/E2ETestsApplication.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.revenuecat.e2etests
22

33
import android.app.Application
4+
import com.revenuecat.purchases.DangerousSettings
5+
import com.revenuecat.purchases.InternalRevenueCatAPI
46
import com.revenuecat.purchases.LogLevel
57
import com.revenuecat.purchases.Purchases
68
import com.revenuecat.purchases.PurchasesConfiguration
79

810
class E2ETestsApplication : Application() {
11+
@OptIn(InternalRevenueCatAPI::class)
912
override fun onCreate() {
1013
super.onCreate()
1114

@@ -16,7 +19,12 @@ class E2ETestsApplication : Application() {
1619
PurchasesConfiguration.Builder(
1720
context = this,
1821
apiKey = Constants.API_KEY,
19-
).build(),
22+
)
23+
// This app runs as a minified release build in the Maestro e2e CI job (to exercise
24+
// the SDK's consumer R8 rules), but uses a Test Store API key. Opt in to allow that
25+
// combination, which the SDK otherwise blocks in non-debuggable builds.
26+
.dangerousSettings(DangerousSettings.forTestStoreInReleaseBuild())
27+
.build(),
2028
)
2129
}
2230
}

0 commit comments

Comments
 (0)