Add BYPASS_SIMULATED_STORE_RELEASE_CHECK compilation flag#6710
Merged
Conversation
Introduces a Swift conditional-compilation flag that, when defined, skips the Release-build safeguard that crashes when a Test Store API key is used. This unblocks SDK consumers (e.g. purchases-kmp) that ship purchases-ios as a pre-compiled binary always built in Release configuration, where the check would otherwise crash apps that legitimately use a Test Store API key during development. Default behavior is unchanged: the flag is opt-in and only meant to be set by trusted hybrid consumers. Made-with: Cursor
2 tasks
JayShortway
approved these changes
May 4, 2026
ajpallares
added a commit
to RevenueCat/purchases-kmp
that referenced
this pull request
May 4, 2026
* Bypass Test Store Release-build crash in purchases-ios In v3.0.0 (currently in beta), purchases-ios ships as a pre-compiled static library inside the published Kotlin/Native artifacts, always built in Release. As a result, any app using purchases-kmp v3 with a Test Store API key crashes at `configure` time on iOS (see #823). Define the `BYPASS_SIMULATED_STORE_RELEASE_CHECK` Swift compilation flag introduced in RevenueCat/purchases-ios#6710 when building the bundled `RevenueCat` target so the safeguard is skipped. Trade-off: apps shipped to production with a Test Store API key won't be caught at runtime. We plan to revisit a smarter detection (e.g. sandbox vs. production) in a follow-up. Also bumps the upstream/purchases-ios submodule to the tip of the companion PR branch. This needs to be re-bumped to a released purchases-ios version once the upstream PR is merged. Made-with: Cursor * Revert upstream/purchases-ios bump Drops the submodule bump from this PR. The bypass flag is being introduced in purchases-ios via a separate PR and will be propagated to this repo automatically by Renovate once a new purchases-ios version including it is released. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
JayShortway
pushed a commit
to RevenueCat/purchases-kmp
that referenced
this pull request
May 6, 2026
* Bypass Test Store Release-build crash in purchases-ios In v3.0.0 (currently in beta), purchases-ios ships as a pre-compiled static library inside the published Kotlin/Native artifacts, always built in Release. As a result, any app using purchases-kmp v3 with a Test Store API key crashes at `configure` time on iOS (see #823). Define the `BYPASS_SIMULATED_STORE_RELEASE_CHECK` Swift compilation flag introduced in RevenueCat/purchases-ios#6710 when building the bundled `RevenueCat` target so the safeguard is skipped. Trade-off: apps shipped to production with a Test Store API key won't be caught at runtime. We plan to revisit a smarter detection (e.g. sandbox vs. production) in a follow-up. Also bumps the upstream/purchases-ios submodule to the tip of the companion PR branch. This needs to be re-bumped to a released purchases-ios version once the upstream PR is merged. Made-with: Cursor * Revert upstream/purchases-ios bump Drops the submodule bump from this PR. The bypass flag is being introduced in purchases-ios via a separate PR and will be propagated to this repo automatically by Renovate once a new purchases-ios version including it is released. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This was referenced May 6, 2026
tonidero
pushed a commit
to RevenueCat/purchases-kmp
that referenced
this pull request
May 7, 2026
* Adds facade Gradle modules for Swift packages (#714) * Migrates iOS from PHC to kn-core and kn-ui (#723) * Migrates more iOS features from PHC to kn-core and kn-ui (#767) * Migrates Android from PHC to purchases-android (#768) * Deletes the deprecated datetime module (#769) * Fixes release automations (#770) * Updates Kotlin to 2.3.20 and Gradle to 9.4.1 (#771) * Updates Compose Multiplatform to 1.9.3 (#778) * Removes dead code (#779) * Fixes caching of the Swift build tasks (#780) * Adds iosApp run config (#785) * Adds 3.0.0 migration guide (#793) * Updates purchases-android to 10.0.0 (#797) * Updates Fastlane README.md (#798) Fastlane consistency updates * No longer tries to update podspec files during release (#799) * Release/3.0.0-beta.1 (#800) * Version bump for 3.0.0-beta.1 * Updates VERSIONS.md * Adds datetime module removal to migration guide (#802) * Updates purchases-android to 10.0.2 (#801) * Adds Amazon instructions to migration doc (#807) * Removes unnecessary CI steps (#810) * Add maestro E2E test app (#824) * Add maestro E2E test app for 3.0.0 Adds a minimal Kotlin Multiplatform Compose app under e2e-tests/MaestroTestApp for running Maestro E2E tests against the RevenueCat SDK. Adapted for the 3.0.0 architecture: - No CocoaPods dependency — iOS builds via Gradle + purchases-ios submodule - Uses projects.knUi for native iOS paywall types - Paywall presented as a native modal on iOS to work around Compose Multiplatform's UIKitViewController accessibility bridge limitation that prevents Maestro from seeing embedded SwiftUI content - Android uses inline Compose Paywall which works with Maestro - Uses RCPaywallViewControllerDelegateProtocol (not the removed PaywallProxy) Made-with: Cursor * Fix build.gradle.kts for Kotlin 2.3.20 Remove deprecated kotlinOptions { jvmTarget } block — the android {} compileOptions block already sets Java compatibility, and Kotlin 2.3.20 no longer supports the old KotlinCommonOptions.jvmTarget accessor. Made-with: Cursor * Exclude MaestroTestApp from binary compatibility validation The E2E test app is an application, not a library — it has no public API surface to validate. Made-with: Cursor * Use SDK's Paywall composable directly instead of custom PaywallPresenter Replace the expect/actual PaywallPresenter with the SDK's own Paywall(options) composable — the E2E app should test the KMP public API, not a custom native presentation layer. Made-with: Cursor * Skip test cases list in maestro tests using launch arguments Pass e2e_test_flow as a launchApp argument so the app navigates directly to the target test case screen, making tests faster. The Test Cases list is preserved for manual/local usage. Made-with: Cursor * Refactor test case routing to use a central registry Extract TEST_CASES registry to a dedicated TestCases.kt file, matching the pattern used by purchases-capacitor and purchases-flutter. The TestCasesScreen now dynamically builds from the registry, and App.kt routes by flow key instead of a hardcoded Screen enum. Made-with: Cursor * Use version catalog for MaestroTestApp minSdk Match the SDK's minSdk (21) instead of hardcoding 24. Made-with: Cursor * Document launch arguments and how to add new test cases Made-with: Cursor * Revert MaestroTestApp minSdk to 24 (required by revenuecatui) The revenuecatui module has minSdk 24, which is higher than the SDK's global android-minSdk of 21. Since MaestroTestApp depends on revenuecatui, it must use minSdk 24. Made-with: Cursor * Simplify customer info fetching, remove delegate Re-fetch customer info when the paywall is dismissed instead of maintaining a global delegate. Also add a comment explaining the minSdk 24 requirement. Made-with: Cursor * Review feedback: fix build phase and update README - Remove reference to non-existent disable-x64.init.gradle.kts from the Xcode build phase (ARCHS/EXCLUDED_ARCHS env vars already handle this) - Update README to reflect that Xcode handles the Kotlin framework build via its build phase, and clarify the CI vs local build flow Made-with: Cursor * Add .gitignore for MaestroTestApp Covers Xcode build output and user-specific project data. Made-with: Cursor * Update README API key section with lane name and local run instructions Made-with: Cursor * Move API key commit warning to apply to both local run methods Made-with: Cursor * Remove unused testTag modifiers from PurchaseThroughPaywallScreen Maestro uses visible text, not Compose test tags. Made-with: Cursor * Add maestro E2E test for purchase through paywall (#825) * Add maestro E2E test app for 3.0.0 Adds a minimal Kotlin Multiplatform Compose app under e2e-tests/MaestroTestApp for running Maestro E2E tests against the RevenueCat SDK. Adapted for the 3.0.0 architecture: - No CocoaPods dependency — iOS builds via Gradle + purchases-ios submodule - Uses projects.knUi for native iOS paywall types - Paywall presented as a native modal on iOS to work around Compose Multiplatform's UIKitViewController accessibility bridge limitation that prevents Maestro from seeing embedded SwiftUI content - Android uses inline Compose Paywall which works with Maestro - Uses RCPaywallViewControllerDelegateProtocol (not the removed PaywallProxy) Made-with: Cursor * Fix build.gradle.kts for Kotlin 2.3.20 Remove deprecated kotlinOptions { jvmTarget } block — the android {} compileOptions block already sets Java compatibility, and Kotlin 2.3.20 no longer supports the old KotlinCommonOptions.jvmTarget accessor. Made-with: Cursor * Exclude MaestroTestApp from binary compatibility validation The E2E test app is an application, not a library — it has no public API surface to validate. Made-with: Cursor * Use SDK's Paywall composable directly instead of custom PaywallPresenter Replace the expect/actual PaywallPresenter with the SDK's own Paywall(options) composable — the E2E app should test the KMP public API, not a custom native presentation layer. Made-with: Cursor * Skip test cases list in maestro tests using launch arguments Pass e2e_test_flow as a launchApp argument so the app navigates directly to the target test case screen, making tests faster. The Test Cases list is preserved for manual/local usage. Made-with: Cursor * Refactor test case routing to use a central registry Extract TEST_CASES registry to a dedicated TestCases.kt file, matching the pattern used by purchases-capacitor and purchases-flutter. The TestCasesScreen now dynamically builds from the registry, and App.kt routes by flow key instead of a hardcoded Screen enum. Made-with: Cursor * Use version catalog for MaestroTestApp minSdk Match the SDK's minSdk (21) instead of hardcoding 24. Made-with: Cursor * Document launch arguments and how to add new test cases Made-with: Cursor * Revert MaestroTestApp minSdk to 24 (required by revenuecatui) The revenuecatui module has minSdk 24, which is higher than the SDK's global android-minSdk of 21. Since MaestroTestApp depends on revenuecatui, it must use minSdk 24. Made-with: Cursor * Simplify customer info fetching, remove delegate Re-fetch customer info when the paywall is dismissed instead of maintaining a global delegate. Also add a comment explaining the minSdk 24 requirement. Made-with: Cursor * Review feedback: fix build phase and update README - Remove reference to non-existent disable-x64.init.gradle.kts from the Xcode build phase (ARCHS/EXCLUDED_ARCHS env vars already handle this) - Update README to reflect that Xcode handles the Kotlin framework build via its build phase, and clarify the CI vs local build flow Made-with: Cursor * Add .gitignore for MaestroTestApp Covers Xcode build output and user-specific project data. Made-with: Cursor * Update README API key section with lane name and local run instructions Made-with: Cursor * Move API key commit warning to apply to both local run methods Made-with: Cursor * Remove unused testTag modifiers from PurchaseThroughPaywallScreen Maestro uses visible text, not Compose test tags. Made-with: Cursor * Add maestro E2E test flows Adds Maestro YAML test flows under e2e-tests/maestro/: - purchase_through_paywall: navigates to the paywall screen, waits for the V2 paywall to render, confirms the test-store purchase, and verifies the "pro" entitlement is active - confirm_purchase: reusable subflow for the sandbox purchase dialog - config.yaml: sets the app ID for all flows Made-with: Cursor * Use launch arguments to skip test cases list in maestro tests Navigate directly to the target test case screen via e2e_test_flow launch argument instead of tapping through the test cases list. Made-with: Cursor * Use exact strings in confirm_purchase instead of regex Both iOS and Android now show the same text, so regex patterns are no longer needed. Matches the Capacitor approach. Made-with: Cursor * Remove redundant appId from confirm_purchase subflow The parent flow already sets appId, so the subflow doesn't need it. Made-with: Cursor * Restore appId in confirm_purchase subflow Maestro requires appId in any flow file that has a config section (--- separator). This is not redundant - without it Maestro fails with "Either 'url' or 'appId' must be specified in the config section." Made-with: Cursor * Bypass Test Store Release-build crash in purchases-ios (#829) * Bypass Test Store Release-build crash in purchases-ios In v3.0.0 (currently in beta), purchases-ios ships as a pre-compiled static library inside the published Kotlin/Native artifacts, always built in Release. As a result, any app using purchases-kmp v3 with a Test Store API key crashes at `configure` time on iOS (see #823). Define the `BYPASS_SIMULATED_STORE_RELEASE_CHECK` Swift compilation flag introduced in RevenueCat/purchases-ios#6710 when building the bundled `RevenueCat` target so the safeguard is skipped. Trade-off: apps shipped to production with a Test Store API key won't be caught at runtime. We plan to revisit a smarter detection (e.g. sandbox vs. production) in a follow-up. Also bumps the upstream/purchases-ios submodule to the tip of the companion PR branch. This needs to be re-bumped to a released purchases-ios version once the upstream PR is merged. Made-with: Cursor * Revert upstream/purchases-ios bump Drops the submodule bump from this PR. The bypass flag is being introduced in purchases-ios via a separate PR and will be propagated to this repo automatically by Renovate once a new purchases-ios version including it is released. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> * Add Xcode 26 compatibility (#828) * Fix cinterop header generation for Xcode 26 compatibility Two issues caused cinterop to fail with Xcode 26 / Swift 6.2: 1. Shared SPM scratch directory: -Xswiftc -emit-objc-header-path applies to ALL swiftc invocations during `swift build`, so a dependency module's header could overwrite the target module's header. Fix: use per-target scratch directories. 2. Forward-declared dependency types: the correct target header uses @Class forward declarations for dependency types (e.g. RCCustomerInfo from RevenueCat), which cinterop turns into unresolvable stubs. Fix: include dependency headers in the target's modulemap so the full @interface definitions are available, and remove dependency -I paths from cinterop so -fmodules doesn't attribute those types to a foreign module. Made-with: Cursor * Restore dependency -I paths for cinterop Xcode 16 compatibility The previous commit removed dependency module paths from cinterop compiler options. This broke Xcode 16 builds because -fmodules needs the dependency -I paths to resolve @Class forward declarations from the dependency module. With the dependency headers now also included in the modulemap, both Xcode versions work: Xcode 16 resolves types via -fmodules, Xcode 26 resolves them via the included header. Made-with: Cursor * Update all CI iOS jobs to Xcode 26.4.1 Rename the xcode16 executor to xcode26 and update all iOS CI jobs to use Xcode 26.4.1 instead of 16.4.0. Made-with: Cursor * Add cross-project Swift dependency tests Covers the multi-target Package.swift / cross-project cinterop wiring introduced for Xcode 26 compatibility: per-target scratch directories, dependency header copying, and modulemap inclusion of dependency headers. Also corrects the `dependencyHeaders` docstring on `SwiftBuildTask` to describe the actual modulemap-based mechanism. Made-with: Cursor * Restore removed comments in ConfigureSwiftDependencies Co-authored-by: Cursor <cursoragent@cursor.com> * Remove PR-specific wording from test comment Co-authored-by: Cursor <cursoragent@cursor.com> * Auto-derive evaluationDependsOn from target dependency graph The MultiTargetPackageBuilder already knows which targets depend on which, and the test context knows which subproject registered each target. Derive evaluationDependsOn calls automatically instead of requiring tests to pass dependsOnSubprojects explicitly. This closes a footgun where alphabetically-sorted subproject names could mask a missing dependency declaration. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove accidental build artifacts and add bin/ to gitignore Co-authored-by: Cursor <cursoragent@cursor.com> * Return both handles from setUpCrossProjectScenario Avoids a hardcoded path for the dep's scratch directory assertion; both dep and consumer now use getScratchDir(projectDir) so the test stays correct if the production layout changes. Co-authored-by: Cursor <cursoragent@cursor.com> * Assert scratch dirs are per-subproject, not just non-shared The previous assertion checked that a root-level shared scratch dir didn't exist, but that path would never exist anyway since the root project has no Swift packages. Instead, assert that each scratch dir lives under its own subproject's build directory and that they differ. Co-authored-by: Cursor <cursoragent@cursor.com> * Fail loudly when a wired dependency header is missing Replace the silent `if (depHeader.exists())` guard with a `check` assertion so a missing dependency header surfaces immediately instead of deferring to a confusing cinterop "unresolved type" error. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove redundant cross-project dependsOn on cinterop task The cinterop task already depends on swiftBuildTask, which itself depends on each cross-project compileSwift task. The explicit dependsOn on the cinterop task was therefore redundant and duplicated the wiring expressed on swiftBuildTask. Co-authored-by: Cursor <cursoragent@cursor.com> * Reuse taskSuffix instead of recomputing it in moduleDependencies loop Co-authored-by: Cursor <cursoragent@cursor.com> * Document direct-only dependency header wiring limitation Co-authored-by: Cursor <cursoragent@cursor.com> * Skip empty root build file write for subproject-only test setups Co-authored-by: Cursor <cursoragent@cursor.com> * Small cleanups: guard duplicate dep headers, use Set for eval deps, extract moduleDir helper Co-authored-by: Cursor <cursoragent@cursor.com> * Assorted nits: guard duplicate targets, drop spurious @optional, centralize task name, fix formatting Co-authored-by: Cursor <cursoragent@cursor.com> * Add negative test for duplicate target registration, mark targets internal Co-authored-by: Cursor <cursoragent@cursor.com> * Use check instead of require for duplicate header guard Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> * Migrates PostHog attribution from PHC to kn-core (#840) Migrates PostHog attribution from PHC to kn-core * Sets version to 3.0.0-SNAPSHOT (#839) --------- Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of SDK-4295
Motivation
In
purchases-kmpv3.0.0 (currently in beta),purchases-iosships as a pre-compiled static library inside the published Kotlin/Native artifacts, always built in Release. As a result, any app usingpurchases-kmpv3 with a Test Store API key crashes atconfiguretime on iOS, which has been reported by a customer.Ideally we'd detect at runtime whether the app is in TestFlight / App Review / Production and only fail there, but we haven't yet found a reliable way to do that and it'd need more investigation than we want before shipping v3 (see the discarded experiment in #6154).
To unblock v3, this PR adds a Swift conditional-compilation flag,
BYPASS_SIMULATED_STORE_RELEASE_CHECK, that skips the Release-build safeguard.purchases-kmpwill be the only consumer that sets it. The companionpurchases-kmpPR is in RevenueCat/purchases-kmp#829.Description
#if !DEBUGguard inConfiguration.APIKeyValidationResult.checkForSimulatedStoreAPIKeyInReleasewith an additional&& !BYPASS_SIMULATED_STORE_RELEASE_CHECKcondition.Testing
Tested publishing RevenueCat/purchases-kmp#829 to maven local
Note
Medium Risk
Low-touch code change, but it weakens a Release-build safeguard when the new compile-time flag is enabled, which could allow shipping apps with a Test Store API key without a runtime crash.
Overview
Adds a new Swift conditional-compilation flag,
BYPASS_SIMULATED_STORE_RELEASE_CHECK, to optionally skip the Release-build crash/alert inConfiguration.APIKeyValidationResult.checkForSimulatedStoreAPIKeyInReleasewhen a simulated-store (test_) API key is detected.Default behavior is unchanged; only consumers that explicitly define the flag bypass the safeguard.
Reviewed by Cursor Bugbot for commit f8cf445. Bugbot is set up for automated code reviews on this repo. Configure here.