Adds facade Gradle modules for Swift packages#714
Conversation
12c999f to
89097b0
Compare
89097b0 to
64a70f4
Compare
| if (!OperatingSystem.current().isMacOsX) { | ||
| logger.info("Skipping Swift dependency configuration on non-macOS host") | ||
| return@afterEvaluate | ||
| } |
There was a problem hiding this comment.
Added this to allow some of our CI jobs to still be run on Docker executors.
| executor: jdk17 | ||
| executor: xcode16 |
There was a problem hiding this comment.
This is probably not needed, as afaik detekt runs on JVM, but I figured better safe than sorry.
There was a problem hiding this comment.
I guess the only thing is that it might delay this job when we don't have resources in CircleCI... but I guess it should be normally ok 🤞
There was a problem hiding this comment.
Yea indeed. We can keep an eye out.
tonidero
left a comment
There was a problem hiding this comment.
I think this makes sense! 🚢
| executor: jdk17 | ||
| executor: xcode16 |
There was a problem hiding this comment.
I guess the only thing is that it might delay this job when we don't have resources in CircleCI... but I guess it should be normally ok 🤞
| name: "AdditionalSwift", | ||
| platforms: [ | ||
| .macOS(.v10_15), | ||
| .watchOS("6.2"), |
There was a problem hiding this comment.
Huh, interesting that watchos is the only that needs to be a string?
There was a problem hiding this comment.
Yea, it's the same in purchases-ios. Apparently there's no enum case for 6.2.
| // Force cinterop binding generation for types otherwise not in the public API | ||
| static inline int __forceBindings( | ||
| enum RCStoreMessageType _1 | ||
| ) { return 0; } |
There was a problem hiding this comment.
I had to think about this for a while 😅
* 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>
Motivation
I'm working on improving the integration experience of purchases-kmp, by avoiding the need for app developers to add the purchases-hybrid-common dependency manually as this adds friction and prevents properly testing Kotlin/Native modules that depend on purchases-kmp. Instead, we will be embedding purchases-ios directly in purchases-kmp, handling all complexity for the app developers.
Changes
This PR adds 2 facade modules:
kn-corekn-uiThese are Kotlin/Native facades for the
RevenueCatandRevenueCatUISwift packages respectively. These Swift packages are added as dependencies using our ownswiftPackage(). This means that these modules will contain the automatically generated Kotlin bindings for this Swift code. The modules aren't used yet.Note
This PR targets a new
3.0.0-devbranch. The facade modules aren't used, but they would be published if the release train runs. The integration changes will require a major, which I would like to release as alpha/beta first. So that's why I created a separate parallel branch. I will ensure that3.0.0-devstays up to date withmain.