diff --git a/.circleci/config.yml b/.circleci/config.yml index 4dab7a8ec..889165908 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,30 +48,6 @@ commands: name: Install Android SDK platform tools command: sdkmanager "platform-tools" - install-cocoapods-on-macos: - steps: - - run: - name: Install CocoaPods - command: bundle exec pod install --repo-update - working_directory: iosApp - - pre-install-pods-for-kotlin-modules: - description: > - Run podInstallSyntheticIos for all Kotlin modules sequentially. - Parallel pod installs race on the shared CocoaPods cache, causing - Errno::ENOENT failures. Running them first with --max-workers=1 - lets the subsequent compile step skip them (UP-TO-DATE). - steps: - - run: - name: Pre-install CocoaPods for Kotlin modules - command: | - ./gradlew \ - :models:podInstallSyntheticIos \ - :mappings:podInstallSyntheticIos \ - :core:podInstallSyntheticIos \ - :revenuecatui:podInstallSyntheticIos \ - --no-parallel --max-workers=1 - save-kotlin-native-compiler-to-cache: steps: - run: @@ -109,7 +85,7 @@ commands: - persist_to_workspace: name: Save incremental Gradle build to workspace root: ./ - paths: + paths: - .gradle # This doesn't support directory glob patterns (**), unfortunately. - build @@ -122,17 +98,24 @@ commands: name: Attach workspace at working directory at: ./ + checkout-submodule: + description: "Checks out a git submodule." + parameters: + path: + type: string + default: "" + steps: + - run: + name: Check out submodule << parameters.path >> + command: | + git submodule init << parameters.path >> + git submodule update << parameters.path >> + parameters: action: type: enum - enum: [ build, upgrade-hybrid-common, bump ] + enum: [ build, bump ] default: build - automatic: - type: boolean - default: false - version: - type: string - default: '' executors: android202409: @@ -155,24 +138,6 @@ executors: HOMEBREW_NO_AUTO_UPDATE: 1 jobs: - update-purchases-hybrid-common-version: - description: "Opens a PR updating the purchases-hybrid-common dependency to the latest version." - executor: xcode16 - steps: - - checkout - - pin-ruby-version - - revenuecat/install-gem-mac-dependencies: - cache-version: v1 - - revenuecat/trust-github-key - - revenuecat/setup-git-credentials - - run: - name: Update purchases-hybrid-common to << pipeline.parameters.version >> - command: | - bundle exec fastlane update_hybrid_common \ - version:<< pipeline.parameters.version >> \ - open_pr:true \ - automatic_release:<< pipeline.parameters.automatic >> - prepare-next-snapshot-version: description: "Opens a PR updating the SDK version to the next minor, appending -SNAPSHOT." executor: ruby3 @@ -187,9 +152,10 @@ jobs: command: bundle exec fastlane prepare_next_snapshot_version detekt: - executor: jdk17 + executor: xcode16 steps: - checkout + - checkout-submodule - restore-gradle-user-home-directory-from-cache - run: name: Run Detekt on commonMain @@ -207,6 +173,7 @@ jobs: steps: - install-android-sdk-on-macos - checkout + - checkout-submodule - restore-gradle-user-home-directory-from-cache - restore-kotlin-native-compiler-from-cache - attach-workspace-at-working-directory @@ -227,7 +194,7 @@ jobs: name: Build libraries for Android command: | tasks=() - for module in core revenuecatui datetime either result; do + for module in core revenuecatui either result; do for buildType in Debug Release; do tasks+=(":$module:compile${buildType}KotlinAndroid") done @@ -240,19 +207,18 @@ jobs: executor: xcode16 steps: - checkout + - checkout-submodule - restore-gradle-user-home-directory-from-cache - restore-kotlin-native-compiler-from-cache - attach-workspace-at-working-directory - pin-ruby-version - revenuecat/install-gem-mac-dependencies: cache-version: v1 - - install-cocoapods-on-macos - - pre-install-pods-for-kotlin-modules - run: name: Build libraries for iOS command: | tasks=() - for module in core revenuecatui datetime either result; do + for module in core revenuecatui either result; do for arch in Arm64 SimulatorArm64 X64; do tasks+=(":$module:compileKotlinIos$arch") done @@ -283,21 +249,30 @@ jobs: executor: xcode16 steps: - checkout + - checkout-submodule - restore-gradle-user-home-directory-from-cache - restore-kotlin-native-compiler-from-cache - attach-workspace-at-working-directory - pin-ruby-version - revenuecat/install-gem-mac-dependencies: cache-version: v1 - - install-cocoapods-on-macos - run: - name: Build sample app for iOS + name: Build Kotlin code for iOS sample app for every architecture command: | tasks=() for arch in Arm64 SimulatorArm64 X64; do tasks+=(":composeApp:compileKotlinIos$arch") done ./gradlew "${tasks[@]}" + - run: + name: Build iOS sample app + command: | + xcodebuild \ + -project iosApp/iosApp.xcodeproj \ + -scheme iosApp \ + -configuration Debug \ + -sdk iphonesimulator \ + build; - save-gradle-user-home-directory-to-cache - save-kotlin-native-compiler-to-cache - save-incremental-gradle-build-to-workspace @@ -323,13 +298,13 @@ jobs: executor: xcode16 steps: - checkout + - checkout-submodule - restore-gradle-user-home-directory-from-cache - restore-kotlin-native-compiler-from-cache - attach-workspace-at-working-directory - pin-ruby-version - revenuecat/install-gem-mac-dependencies: cache-version: v1 - - install-cocoapods-on-macos - run: name: Test public API for iOS command: | @@ -363,6 +338,7 @@ jobs: executor: xcode16 steps: - checkout + - checkout-submodule - restore-gradle-user-home-directory-from-cache - restore-kotlin-native-compiler-from-cache - attach-workspace-at-working-directory @@ -382,6 +358,7 @@ jobs: executor: xcode16 steps: - checkout + - checkout-submodule - restore-gradle-user-home-directory-from-cache - attach-workspace-at-working-directory - run: @@ -401,11 +378,11 @@ jobs: type: boolean steps: - checkout + - checkout-submodule - install-android-sdk-on-macos - pin-ruby-version - revenuecat/install-gem-mac-dependencies: cache-version: v1 - - install-cocoapods-on-macos - restore-gradle-user-home-directory-from-cache - restore-kotlin-native-compiler-from-cache - when: @@ -436,15 +413,6 @@ jobs: command: bundle exec fastlane github_release_current_version workflows: - on-action-upgrade-hybrid-common: - when: - equal: [ upgrade-hybrid-common, << pipeline.parameters.action >> ] - jobs: - - update-purchases-hybrid-common-version: - context: - - git-user-ops - - github-bot-public - # This workflow runs for any commit to main, as well as for any other # branch with an open PR, except for release branches. This is controlled # by the "Only build pull requests" setting on CircleCI. diff --git a/.gitignore b/.gitignore index 0b8896b3a..1a448532b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.iml .gradle -.idea +**/.idea/* .DS_Store build captures @@ -21,4 +21,8 @@ vendor/ .bundle/ # Share IssueNavigationConfiguration -!.idea/vcs.xml +!/.idea/vcs.xml +# Ensure iosApp run configuration works +!/.idea/xcode.xml +!/.idea/purchases-kmp.iosApp.iml +!/.idea/modules.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..98a3307f4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/purchases-kmp.iosApp.iml b/.idea/purchases-kmp.iosApp.iml new file mode 100644 index 000000000..1e2970164 --- /dev/null +++ b/.idea/purchases-kmp.iosApp.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/xcode.xml b/.idea/xcode.xml new file mode 100644 index 000000000..df355d6ca --- /dev/null +++ b/.idea/xcode.xml @@ -0,0 +1,4 @@ + + + + diff --git a/.run/iosApp.run.xml b/.run/iosApp.run.xml new file mode 100644 index 000000000..25d82394a --- /dev/null +++ b/.run/iosApp.run.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 1799db134..f12f235c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,6 @@ purchases-kmp/ ├── models/ # Shared data models and domain objects ├── mappings/ # Platform-specific mappings ├── revenuecatui/ # Jetpack Compose UI components for paywalls -├── datetime/ # KMP datetime utilities ├── either/ # Either/Result type implementations ├── result/ # Result wrapper types ├── build-logic/ # Custom Gradle build convention plugins diff --git a/apiTester/build.gradle.kts b/apiTester/build.gradle.kts index 8f1cc5d36..41788fb94 100644 --- a/apiTester/build.gradle.kts +++ b/apiTester/build.gradle.kts @@ -49,7 +49,6 @@ kotlin { implementation(compose.foundation) implementation(compose.ui) implementation(projects.core) - implementation(projects.datetime) implementation(projects.either) implementation(projects.revenuecatui) implementation(projects.result) diff --git a/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/CustomerInfoAPI.kt b/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/CustomerInfoAPI.kt index eec72fb33..9ab5ef4c2 100644 --- a/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/CustomerInfoAPI.kt +++ b/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/CustomerInfoAPI.kt @@ -2,17 +2,12 @@ package com.revenuecat.purchases.kmp.apitester -import com.revenuecat.purchases.kmp.datetime.allExpirationInstants -import com.revenuecat.purchases.kmp.datetime.allPurchaseInstants -import com.revenuecat.purchases.kmp.datetime.firstSeenInstant -import com.revenuecat.purchases.kmp.datetime.latestExpirationInstant -import com.revenuecat.purchases.kmp.datetime.originalPurchaseInstant -import com.revenuecat.purchases.kmp.datetime.requestInstant import com.revenuecat.purchases.kmp.models.CustomerInfo import com.revenuecat.purchases.kmp.models.EntitlementInfos import com.revenuecat.purchases.kmp.models.SubscriptionInfo import com.revenuecat.purchases.kmp.models.Transaction import kotlin.time.ExperimentalTime +import kotlin.time.Instant private class CustomerInfoAPI { @OptIn(ExperimentalTime::class) @@ -22,27 +17,21 @@ private class CustomerInfoAPI { val asubs = activeSubscriptions val productIds: Set = allPurchasedProductIdentifiers val ledm: Long? = latestExpirationDateMillis - val lei: kotlinx.datetime.Instant? = latestExpirationInstant - val led: kotlin.time.Instant? = latestExpirationDate + val led: Instant? = latestExpirationDate val si: Map = subscriptionsByProductIdentifier val nst: List = nonSubscriptionTransactions val opdm: Long? = originalPurchaseDateMillis - val opi: kotlinx.datetime.Instant? = originalPurchaseInstant - val opd: kotlin.time.Instant? = originalPurchaseDate + val opd: Instant? = originalPurchaseDate val rdm: Long = requestDateMillis - val ri: kotlinx.datetime.Instant = requestInstant - val rd: kotlin.time.Instant = requestDate + val rd: Instant = requestDate val fsm: Long = firstSeenMillis - val fsi: kotlinx.datetime.Instant = firstSeenInstant - val fs: kotlin.time.Instant = firstSeen + val fs: Instant = firstSeen val oaui: String = originalAppUserId val mu: String? = managementUrlString val allExpirationMillisByProduct: Map = allExpirationDateMillis - val allExpirationInstantsByProduct: Map = allExpirationInstants - val allExpirationDatesByProduct: Map = allExpirationDates + val allExpirationDatesByProduct: Map = allExpirationDates val allPurchaseMillisByProduct: Map = allPurchaseDateMillis - val allPurchaseInstantsByProduct: Map = allPurchaseInstants - val allPurchaseDatesByProduct: Map = allPurchaseDates + val allPurchaseDatesByProduct: Map = allPurchaseDates } } } diff --git a/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/EntitlementInfoAPI.kt b/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/EntitlementInfoAPI.kt index 9048853bf..91ad220fb 100644 --- a/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/EntitlementInfoAPI.kt +++ b/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/EntitlementInfoAPI.kt @@ -1,12 +1,9 @@ package com.revenuecat.purchases.kmp.apitester -import com.revenuecat.purchases.kmp.datetime.billingIssueDetectedAtInstant -import com.revenuecat.purchases.kmp.datetime.expirationInstant -import com.revenuecat.purchases.kmp.datetime.latestPurchaseInstant -import com.revenuecat.purchases.kmp.datetime.originalPurchaseInstant -import com.revenuecat.purchases.kmp.datetime.unsubscribeDetectedAtInstant + import com.revenuecat.purchases.kmp.models.EntitlementInfo import kotlin.time.ExperimentalTime +import kotlin.time.Instant @Suppress("unused", "UNUSED_VARIABLE") private class EntitlementInfoAPI { @@ -19,25 +16,20 @@ private class EntitlementInfoAPI { // FIXME re-enable in SDK-3530 // val periodType: PeriodType = periodType val latestPurchaseDateMillis: Long? = latestPurchaseDateMillis - val latestPurchaseInstant: kotlinx.datetime.Instant? = latestPurchaseInstant - val latestPurchaseDate: kotlin.time.Instant? = latestPurchaseDate + val latestPurchaseDate: Instant? = latestPurchaseDate val originalPurchaseDateMillis: Long? = originalPurchaseDateMillis - val originalPurchaseInstant: kotlinx.datetime.Instant? = originalPurchaseInstant - val originalPurchaseDate: kotlin.time.Instant? = originalPurchaseDate + val originalPurchaseDate: Instant? = originalPurchaseDate val expirationDateMillis: Long? = expirationDateMillis - val expirationInstant: kotlinx.datetime.Instant? = expirationInstant - val expirationDate: kotlin.time.Instant? = expirationDate + val expirationDate: Instant? = expirationDate // FIXME re-enable in SDK-3530 // val store: Store = store val productIdentifier: String = productIdentifier val productPlanIdentifier: String? = productPlanIdentifier val sandbox: Boolean = isSandbox val unsubscribeDetectedAtMillis: Long? = unsubscribeDetectedAtMillis - val unsubscribeDetectedAtInstant: kotlinx.datetime.Instant? = unsubscribeDetectedAtInstant - val unsubscribeDetectedAt: kotlin.time.Instant? = unsubscribeDetectedAt + val unsubscribeDetectedAt: Instant? = unsubscribeDetectedAt val billingIssueDetectedAtMillis: Long? = billingIssueDetectedAtMillis - val billingIssueDetectedAtInstant: kotlinx.datetime.Instant? = billingIssueDetectedAtInstant - val billingIssueDetectedAt: kotlin.time.Instant? = billingIssueDetectedAt + val billingIssueDetectedAt: Instant? = billingIssueDetectedAt // FIXME re-enable in SDK-3530 // val ownershipType: OwnershipType = ownershipType // FIXME re-enable in SDK-3530 diff --git a/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/TransactionAPI.kt b/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/TransactionAPI.kt index 4fdc61855..59f8129f7 100644 --- a/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/TransactionAPI.kt +++ b/apiTester/src/commonMain/kotlin/com/revenuecat/purchases/kmp/apitester/TransactionAPI.kt @@ -1,8 +1,8 @@ package com.revenuecat.purchases.kmp.apitester -import com.revenuecat.purchases.kmp.datetime.purchaseInstant import com.revenuecat.purchases.kmp.models.Transaction import kotlin.time.ExperimentalTime +import kotlin.time.Instant @Suppress("unused", "UNUSED_VARIABLE") private class TransactionAPI { @@ -12,8 +12,7 @@ private class TransactionAPI { val transactionIdentifier: String = transactionIdentifier val productIdentifier: String = productIdentifier val purchaseDateMillis: Long = purchaseDateMillis - val purchaseInstant: kotlinx.datetime.Instant = purchaseInstant - val purchaseDate: kotlin.time.Instant = purchaseDate + val purchaseDate: Instant = purchaseDate } } } diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index ea726e7b0..cb982fc92 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { testImplementation(libs.kotlin.test.junit) testRuntimeOnly(libs.junit.engine) + testRuntimeOnly(libs.junit.platformLauncher) } tasks.test { diff --git a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/ConfigureSwiftDependencies.kt b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/ConfigureSwiftDependencies.kt index 2b46bade1..d5b031cba 100644 --- a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/ConfigureSwiftDependencies.kt +++ b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/ConfigureSwiftDependencies.kt @@ -10,6 +10,7 @@ import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.gradle.api.provider.Provider import org.gradle.api.tasks.TaskProvider +import org.gradle.internal.os.OperatingSystem import org.gradle.kotlin.dsl.withType import org.jetbrains.compose.ComposeExtension import org.jetbrains.compose.resources.ResourcesExtension @@ -26,6 +27,10 @@ fun Project.configureSwiftDependencies() { afterEvaluate { if (registry.packages.isNotEmpty()) { + if (!OperatingSystem.current().isMacOsX) { + logger.info("Skipping Swift dependency configuration on non-macOS host") + return@afterEvaluate + } configureAllSwiftDependencies(registry.packages) } } @@ -46,7 +51,7 @@ private fun Project.configureAllSwiftDependencies(dependencies: List + swiftDependencies: List, + packageInfos: Map, ) { // Register a task to generate the cinterop .def file val defFile = layout.buildDirectory.file("generated/cinterop/${dependency.target}.def") @@ -130,6 +136,15 @@ private fun Project.configureSwiftDependency( ?.takeIf { it.project != this } } + // Add cross-project dependency source directories so the build cache is invalidated when a + // dependency target's source changes. + val transitiveDepSourceDirs = moduleDependencies.mapNotNull { dep -> + packageInfos[dep.dependency.packageDir]?.getTargetSourceDir( + targetName = dep.dependency.target, + packageDir = dep.dependency.packageDir + ) + } + kotlin.targets.withType { // Skip this target if it doesn't include the source set this dependency is added to. if (!includesSourceSet(dependency.sourceSetName)) return@withType @@ -140,7 +155,13 @@ private fun Project.configureSwiftDependency( ) val mainCompilation = compilations.getByName("main") - val swiftBuildTask = registerSwiftBuildTask(this, dependency, targetSourceDir) + val swiftBuildTask = registerSwiftBuildTask( + kotlinTarget = this, + dependency = dependency, + targetSourceDir = targetSourceDir, + transitiveDepSourceDirs = transitiveDepSourceDirs + ) + val swiftOutputDir = layout.buildDirectory .dir("swift-packages/${dependency.target}/${konanTarget.name}") .get().asFile @@ -234,7 +255,8 @@ private fun KotlinNativeTarget.includesSourceSet(sourceSetName: String): Boolean private fun Project.registerSwiftBuildTask( kotlinTarget: KotlinNativeTarget, dependency: SwiftDependency, - targetSourceDir: File + targetSourceDir: File, + transitiveDepSourceDirs: List, ): TaskProvider { val taskSuffix = getTaskSuffix(kotlinTarget.konanTarget) val taskName = "compileSwift${dependency.target}$taskSuffix" @@ -248,11 +270,13 @@ private fun Project.registerSwiftBuildTask( moduleName.set(dependency.moduleName) configuration.set(getSwiftConfiguration()) packageDir.set(dependency.packageDir) + packageSwiftFile.set(dependency.packageDir.resolve("Package.swift")) this.targetSourceDir.set(targetSourceDir) outputDir.set(layout.buildDirectory.dir("swift-packages/${dependency.target}/${kotlinTarget.konanTarget.name}")) // Use a shared scratch directory at root project level for SPM build cache sharing scratchDir.set(rootProject.layout.buildDirectory.dir("swift-packages/.build")) swiftSettingsArgs.set(dependency.swiftSettings?.toCommandLineArgs() ?: emptyList()) + this.transitiveDepSourceDirs.from(transitiveDepSourceDirs) } } @@ -313,7 +337,7 @@ private fun Project.getSwiftConfiguration(): String { return "debug" } -private fun Project.getToolchainPath(): Provider = +private fun Project.getToolchainPath(): Provider = providers.exec { commandLine("xcrun", "--find", "swift") }.standardOutput.asText.map { swiftPath -> diff --git a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/GenerateDefFileTask.kt b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/GenerateDefFileTask.kt index 2bdac4468..ecc614460 100644 --- a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/GenerateDefFileTask.kt +++ b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/GenerateDefFileTask.kt @@ -4,6 +4,8 @@ import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.IgnoreEmptyDirectories import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Optional @@ -19,6 +21,7 @@ import java.security.MessageDigest * The .def file tells Kotlin/Native cinterop how to process the Swift-generated * Objective-C header and link against the static library. */ +@CacheableTask abstract class GenerateDefFileTask : DefaultTask() { /** The Kotlin package name for the generated bindings */ @@ -45,6 +48,7 @@ abstract class GenerateDefFileTask : DefaultTask() { /** The Swift source directory - used to compute a hash for cache invalidation */ @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) + @get:IgnoreEmptyDirectories abstract val swiftSourceDir: DirectoryProperty /** The output .def file */ diff --git a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/ProcessSwiftResourcesTask.kt b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/ProcessSwiftResourcesTask.kt index 1eed50ab8..7f25651ab 100644 --- a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/ProcessSwiftResourcesTask.kt +++ b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/ProcessSwiftResourcesTask.kt @@ -7,6 +7,8 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.IgnoreEmptyDirectories import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputDirectory @@ -20,6 +22,7 @@ import javax.inject.Inject /** * Processes Swift package resources to include in the .klib via Compose Resources. */ +@CacheableTask abstract class ProcessSwiftResourcesTask @Inject constructor( private val execOperations: ExecOperations ) : DefaultTask() { @@ -41,6 +44,7 @@ abstract class ProcessSwiftResourcesTask @Inject constructor( */ @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) + @get:IgnoreEmptyDirectories abstract val resourceFiles: ConfigurableFileCollection /** diff --git a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/SwiftBuildTask.kt b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/SwiftBuildTask.kt index 20c48917f..edc4be63d 100644 --- a/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/SwiftBuildTask.kt +++ b/build-logic/convention/src/main/kotlin/com/revenuecat/purchases/kmp/buildlogic/swift/task/SwiftBuildTask.kt @@ -1,11 +1,17 @@ package com.revenuecat.purchases.kmp.buildlogic.swift.task import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.IgnoreEmptyDirectories import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory @@ -22,6 +28,7 @@ import javax.inject.Inject * - An Objective-C header (-Swift.h) * - A module.modulemap for cinterop */ +@CacheableTask abstract class SwiftBuildTask @Inject constructor( private val execOperations: ExecOperations ) : DefaultTask() { @@ -58,11 +65,27 @@ abstract class SwiftBuildTask @Inject constructor( @get:Internal abstract val packageDir: DirectoryProperty + /** The Package.swift file. Tracked for cache invalidation on package structure changes. */ + @get:InputFile + @get:PathSensitive(PathSensitivity.NAME_ONLY) + abstract val packageSwiftFile: RegularFileProperty + /** The Swift target's source directory (for tracking incremental builds) */ @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) + @get:IgnoreEmptyDirectories abstract val targetSourceDir: DirectoryProperty + /** + * Source directories of cross-project Swift target dependencies. Ensures the build cache + * is invalidated when a dependency target's source changes (e.g. RevenueCat sources + * affecting RevenueCatUI compilation). + */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:IgnoreEmptyDirectories + abstract val transitiveDepSourceDirs: ConfigurableFileCollection + /** The output directory for built artifacts */ @get:OutputDirectory abstract val outputDir: DirectoryProperty diff --git a/build.gradle.kts b/build.gradle.kts index ea3265e63..bc5909545 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,6 @@ import com.vanniktech.maven.publish.MavenPublishPlugin import com.vanniktech.maven.publish.SonatypeHost import dev.iurysouza.modulegraph.Orientation import io.gitlab.arturbosch.detekt.Detekt -import org.gradle.configurationcache.extensions.capitalized plugins { //trick: for the same plugin versions in all sub-modules @@ -14,7 +13,6 @@ plugins { alias(libs.plugins.jetbrains.compose).apply(false) alias(libs.plugins.compose.compiler).apply(false) alias(libs.plugins.kotlin.multiplatform).apply(false) - alias(libs.plugins.kotlin.cocoapods).apply(false) alias(libs.plugins.kotlinx.binaryCompatibilityValidator) alias(libs.plugins.adamko.dokkatoo.html) alias(libs.plugins.arturbosch.detekt).apply(false) @@ -110,7 +108,6 @@ apiValidation { dependencies { dokkatoo(projects.core) - dokkatoo(projects.datetime) dokkatoo(projects.either) dokkatoo(projects.models) dokkatoo(projects.result) @@ -161,3 +158,6 @@ private fun TaskContainer.registerDetektTask( xml.outputLocation = file("build/reports/detekt/$reportName.xml") } } + +private fun CharSequence.capitalized(): String = + toString().replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 045f4823c..3f96d5d90 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -48,7 +48,6 @@ kotlin { implementation(projects.core) implementation(projects.result) implementation(projects.either) - implementation(projects.datetime) implementation(projects.revenuecatui) } androidMain.dependencies { diff --git a/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/AdTrackingTestingScreen.kt b/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/AdTrackingTestingScreen.kt index bf844582e..a06d0d452 100644 --- a/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/AdTrackingTestingScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/AdTrackingTestingScreen.kt @@ -35,7 +35,8 @@ import com.revenuecat.purchases.kmp.models.AdMediatorName import com.revenuecat.purchases.kmp.models.AdOpenedData import com.revenuecat.purchases.kmp.models.AdRevenueData import com.revenuecat.purchases.kmp.models.AdRevenuePrecision -import kotlinx.datetime.Clock +import kotlin.time.Clock +import kotlin.time.ExperimentalTime enum class AdTrackingFunction(val displayName: String) { TRACK_AD_DISPLAYED("trackAdDisplayed()"), @@ -45,7 +46,7 @@ enum class AdTrackingFunction(val displayName: String) { TRACK_AD_FAILED_TO_LOAD("trackAdFailedToLoad()") } -@OptIn(ExperimentalRevenueCatApi::class) +@OptIn(ExperimentalRevenueCatApi::class, ExperimentalTime::class) @Composable fun AdTrackingTestingScreen( navigateTo: (Screen) -> Unit diff --git a/core/api/core.klib.api b/core/api/core.klib.api index cad5221f0..86f945f99 100644 --- a/core/api/core.klib.api +++ b/core/api/core.klib.api @@ -52,8 +52,6 @@ final class com.revenuecat.purchases.kmp.ktx/SuccessfulPurchase { // com.revenue } final class com.revenuecat.purchases.kmp/AdTracker { // com.revenuecat.purchases.kmp/AdTracker|null[0] - constructor () // com.revenuecat.purchases.kmp/AdTracker.|(){}[0] - final fun trackAdDisplayed(com.revenuecat.purchases.kmp.models/AdDisplayedData) // com.revenuecat.purchases.kmp/AdTracker.trackAdDisplayed|trackAdDisplayed(com.revenuecat.purchases.kmp.models.AdDisplayedData){}[0] final fun trackAdFailedToLoad(com.revenuecat.purchases.kmp.models/AdFailedToLoadData) // com.revenuecat.purchases.kmp/AdTracker.trackAdFailedToLoad|trackAdFailedToLoad(com.revenuecat.purchases.kmp.models.AdFailedToLoadData){}[0] final fun trackAdLoaded(com.revenuecat.purchases.kmp.models/AdLoadedData) // com.revenuecat.purchases.kmp/AdTracker.trackAdLoaded|trackAdLoaded(com.revenuecat.purchases.kmp.models.AdLoadedData){}[0] diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 92dd66354..38a5ffa2f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -2,7 +2,6 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING plugins { id("revenuecat-library") - alias(libs.plugins.kotlin.cocoapods) alias(libs.plugins.codingfeline.buildkonfig) } @@ -17,10 +16,11 @@ kotlin { } androidMain.dependencies { api(libs.androidx.startup) - implementation(libs.revenuecat.common) + implementation(libs.revenuecat.android) implementation(projects.mappings) } iosMain.dependencies { + implementation(projects.knCore) implementation(projects.mappings) } @@ -32,22 +32,6 @@ kotlin { implementation(libs.kotlin.test.junit) } } - - cocoapods { - version = libs.versions.revenuecat.kmp.get() - ios.deploymentTarget = libs.versions.ios.deploymentTarget.core.get() - - framework { - baseName = "Purchases" - isStatic = true - } - - pod("PurchasesHybridCommon") { - version = libs.versions.revenuecat.common.get() - extraOpts += listOf("-compiler-option", "-fmodules") - packageName = "swiftPMImport.com.revenuecat.purchases.kn.core" - } - } } android { @@ -59,7 +43,6 @@ buildkonfig { defaultConfigs { buildConfigField(STRING, "platformFlavor", "kmp") - buildConfigField(STRING, "revenuecatCommonVersion", libs.versions.revenuecat.common.get()) buildConfigField(STRING, "revenuecatKmpVersion", libs.versions.revenuecat.kmp.get()) } } diff --git a/core/core.podspec b/core/core.podspec deleted file mode 100644 index 3ac22f91d..000000000 --- a/core/core.podspec +++ /dev/null @@ -1,54 +0,0 @@ -Pod::Spec.new do |spec| - spec.name = 'core' - spec.version = '2.11.0-SNAPSHOT' - spec.homepage = '' - spec.source = { :http=> ''} - spec.authors = '' - spec.license = '' - spec.summary = '' - spec.vendored_frameworks = 'build/cocoapods/framework/Purchases.framework' - spec.libraries = 'c++' - spec.ios.deployment_target = '13.0' - spec.dependency 'PurchasesHybridCommon', '17.55.1' - - if !Dir.exist?('build/cocoapods/framework/Purchases.framework') || Dir.empty?('build/cocoapods/framework/Purchases.framework') - raise " - - Kotlin framework 'Purchases' doesn't exist yet, so a proper Xcode project can't be generated. - 'pod install' should be executed after running ':generateDummyFramework' Gradle task: - - ./gradlew :core:generateDummyFramework - - Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" - end - - spec.xcconfig = { - 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO', - } - - spec.pod_target_xcconfig = { - 'KOTLIN_PROJECT_PATH' => ':core', - 'PRODUCT_MODULE_NAME' => 'Purchases', - } - - spec.script_phases = [ - { - :name => 'Build core', - :execution_position => :before_compile, - :shell_path => '/bin/sh', - :script => <<-SCRIPT - if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then - echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" - exit 0 - fi - set -ev - REPO_ROOT="$PODS_TARGET_SRCROOT" - "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ - -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ - -Pkotlin.native.cocoapods.archs="$ARCHS" \ - -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" - SCRIPT - } - ] - -end \ No newline at end of file diff --git a/core/src/androidMain/kotlin/com/revenuecat/purchases/kmp/Purchases.android.kt b/core/src/androidMain/kotlin/com/revenuecat/purchases/kmp/Purchases.android.kt index c3d6ae1f9..0ff1b471b 100644 --- a/core/src/androidMain/kotlin/com/revenuecat/purchases/kmp/Purchases.android.kt +++ b/core/src/androidMain/kotlin/com/revenuecat/purchases/kmp/Purchases.android.kt @@ -3,8 +3,6 @@ package com.revenuecat.purchases.kmp import android.content.Intent import android.net.Uri import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI -import com.revenuecat.purchases.kmp.models.CustomPaywallImpressionParams as KmpCustomPaywallImpressionParams -import com.revenuecat.purchases.paywalls.events.CustomPaywallImpressionParams as AndroidCustomPaywallImpressionParams import com.revenuecat.purchases.PurchaseParams import com.revenuecat.purchases.common.PlatformInfo import com.revenuecat.purchases.getCustomerInfoWith @@ -12,20 +10,19 @@ import com.revenuecat.purchases.getOfferingsWith import com.revenuecat.purchases.getProductsWith import com.revenuecat.purchases.getStorefrontCountryCodeWith import com.revenuecat.purchases.getVirtualCurrenciesWith -import com.revenuecat.purchases.hybridcommon.isWebPurchaseRedemptionURL import com.revenuecat.purchases.kmp.di.AndroidProvider import com.revenuecat.purchases.kmp.di.requireActivity import com.revenuecat.purchases.kmp.di.requireApplication -import com.revenuecat.purchases.kmp.mappings.toAndroid import com.revenuecat.purchases.kmp.mappings.toAndroidBillingFeature import com.revenuecat.purchases.kmp.mappings.toAndroidCacheFetchPolicy +import com.revenuecat.purchases.kmp.mappings.toAndroidEntitlementVerificationMode import com.revenuecat.purchases.kmp.mappings.toAndroidGoogleReplacementMode import com.revenuecat.purchases.kmp.mappings.toAndroidPackage +import com.revenuecat.purchases.kmp.mappings.toAndroidPurchasesAreCompletedBy import com.revenuecat.purchases.kmp.mappings.toAndroidStore import com.revenuecat.purchases.kmp.mappings.toAndroidStoreProduct import com.revenuecat.purchases.kmp.mappings.toAndroidSubscriptionOption import com.revenuecat.purchases.kmp.mappings.toCustomerInfo -import com.revenuecat.purchases.kmp.mappings.toHybridString import com.revenuecat.purchases.kmp.mappings.toOfferings import com.revenuecat.purchases.kmp.mappings.toPurchasesError import com.revenuecat.purchases.kmp.mappings.toStore @@ -33,11 +30,6 @@ import com.revenuecat.purchases.kmp.mappings.toStoreProduct import com.revenuecat.purchases.kmp.mappings.toStoreTransaction import com.revenuecat.purchases.kmp.mappings.toVirtualCurrencies import com.revenuecat.purchases.kmp.mappings.toWebPurchaseResult -import com.revenuecat.purchases.kmp.models.AdDisplayedData -import com.revenuecat.purchases.kmp.models.AdFailedToLoadData -import com.revenuecat.purchases.kmp.models.AdLoadedData -import com.revenuecat.purchases.kmp.models.AdOpenedData -import com.revenuecat.purchases.kmp.models.AdRevenueData import com.revenuecat.purchases.kmp.models.BillingFeature import com.revenuecat.purchases.kmp.models.CacheFetchPolicy import com.revenuecat.purchases.kmp.models.CustomerInfo @@ -59,7 +51,6 @@ import com.revenuecat.purchases.kmp.models.StoreTransaction import com.revenuecat.purchases.kmp.models.Storefront import com.revenuecat.purchases.kmp.models.SubscriptionOption import com.revenuecat.purchases.kmp.models.VirtualCurrencies -import com.revenuecat.purchases.kmp.models.VirtualCurrency import com.revenuecat.purchases.kmp.models.WebPurchaseRedemption import com.revenuecat.purchases.kmp.models.WinBackOffer import com.revenuecat.purchases.kmp.strings.ConfigureStrings @@ -73,7 +64,9 @@ import com.revenuecat.purchases.syncPurchasesWith import java.net.URL import com.revenuecat.purchases.DangerousSettings as AndroidDangerousSettings import com.revenuecat.purchases.Purchases as AndroidPurchases -import com.revenuecat.purchases.hybridcommon.configure as commonConfigure +import com.revenuecat.purchases.PurchasesConfiguration as AndroidPurchasesConfiguration +import com.revenuecat.purchases.kmp.models.CustomPaywallImpressionParams as KmpCustomPaywallImpressionParams +import com.revenuecat.purchases.paywalls.events.CustomPaywallImpressionParams as AndroidCustomPaywallImpressionParams import com.revenuecat.purchases.virtualcurrencies.VirtualCurrencies as AndroidVirtualCurrencies public actual class Purchases private constructor(private val androidPurchases: AndroidPurchases) { @@ -120,21 +113,24 @@ public actual class Purchases private constructor(private val androidPurchases: @JvmStatic public actual fun configure(configuration: PurchasesConfiguration): Purchases { with(configuration) { - // Using the common configure() call allows us to pass PlatformInfo. - commonConfigure( - context = AndroidProvider.requireApplication(), - apiKey = apiKey, - appUserID = appUserId, - purchasesAreCompletedBy = purchasesAreCompletedBy.toHybridString(), - platformInfo = PlatformInfo( - flavor = BuildKonfig.platformFlavor, - version = frameworkVersion, - ), - store = (store ?: Store.PLAY_STORE).toAndroidStore(), - dangerousSettings = dangerousSettings.toAndroidDangerousSettings(), - shouldShowInAppMessagesAutomatically = showInAppMessagesAutomatically, - verificationMode = verificationMode.name, - pendingTransactionsForPrepaidPlansEnabled = pendingTransactionsForPrepaidPlansEnabled + AndroidPurchases.platformInfo = PlatformInfo( + flavor = BuildKonfig.platformFlavor, + version = frameworkVersion, + ) + AndroidPurchases.configure( + AndroidPurchasesConfiguration.Builder( + context = AndroidProvider.requireApplication(), + apiKey = apiKey, + ).appUserID(appUserId) + .purchasesAreCompletedBy(purchasesAreCompletedBy.toAndroidPurchasesAreCompletedBy()) + .store((store ?: Store.PLAY_STORE).toAndroidStore()) + .dangerousSettings(dangerousSettings.toAndroidDangerousSettings()) + .showInAppMessagesAutomatically(showInAppMessagesAutomatically) + .entitlementVerificationMode(verificationMode.toAndroidEntitlementVerificationMode()) + .pendingTransactionsForPrepaidPlansEnabled( + pendingTransactionsForPrepaidPlansEnabled ?: false + ) + .build() ) } @@ -154,7 +150,7 @@ public actual class Purchases private constructor(private val androidPurchases: AndroidDangerousSettings(autoSyncPurchases) public actual fun parseAsWebPurchaseRedemption(url: String): WebPurchaseRedemption? { - return if (isWebPurchaseRedemptionURL(url)) { + return if (AndroidPurchases.parseAsWebPurchaseRedemption(url) != null) { WebPurchaseRedemption(url) } else { null diff --git a/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/AdTracker.ios.kt b/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/AdTracker.ios.kt index e6bfac5cc..a0047460c 100644 --- a/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/AdTracker.ios.kt +++ b/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/AdTracker.ios.kt @@ -1,123 +1,62 @@ package com.revenuecat.purchases.kmp -import swiftPMImport.com.revenuecat.purchases.kn.core.IOSAPIAvailabilityChecker -import swiftPMImport.com.revenuecat.purchases.kn.core.RCCommonFunctionality -import swiftPMImport.com.revenuecat.purchases.kn.core.trackAdDisplayed -import swiftPMImport.com.revenuecat.purchases.kn.core.trackAdFailedToLoad -import swiftPMImport.com.revenuecat.purchases.kn.core.trackAdLoaded -import swiftPMImport.com.revenuecat.purchases.kn.core.trackAdOpened -import swiftPMImport.com.revenuecat.purchases.kn.core.trackAdRevenue +import com.revenuecat.purchases.kmp.mappings.toIos import com.revenuecat.purchases.kmp.models.AdDisplayedData import com.revenuecat.purchases.kmp.models.AdFailedToLoadData import com.revenuecat.purchases.kmp.models.AdLoadedData import com.revenuecat.purchases.kmp.models.AdOpenedData import com.revenuecat.purchases.kmp.models.AdRevenueData import kotlinx.cinterop.ExperimentalForeignApi -import platform.Foundation.NSNumber - -private const val KEY_NETWORK_NAME = "networkName" -private const val KEY_MEDIATOR_NAME = "mediatorName" -private const val KEY_AD_FORMAT = "adFormat" -private const val KEY_PLACEMENT = "placement" -private const val KEY_AD_UNIT_ID = "adUnitId" -private const val KEY_IMPRESSION_ID = "impressionId" -private const val KEY_REVENUE_MICROS = "revenueMicros" -private const val KEY_CURRENCY = "currency" -private const val KEY_PRECISION = "precision" -private const val KEY_MEDIATOR_ERROR_CODE = "mediatorErrorCode" +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdTracker +import swiftPMImport.com.revenuecat.purchases.kn.core.additional.AppleApiAvailability @ExperimentalRevenueCatApi @OptIn(ExperimentalForeignApi::class) -public actual class AdTracker { +public actual class AdTracker internal constructor( + private val iosAdTracker: RCAdTracker +) { public actual fun trackAdDisplayed(data: AdDisplayedData) { - if (!IOSAPIAvailabilityChecker().isAdTrackingAPIAvailable()) { + if (!AppleApiAvailability().isAdTrackingAPIAvailable()) { Purchases.logHandler.w("Purchases", "Ad tracking requires iOS 15.0+. Current API is unavailable.") return } - val adData = buildMap { - data.networkName?.let { put(KEY_NETWORK_NAME, it) } - put(KEY_MEDIATOR_NAME, data.mediatorName.value) - put(KEY_AD_FORMAT, data.adFormat.value) - put(KEY_PLACEMENT, data.placement) - put(KEY_AD_UNIT_ID, data.adUnitId) - put(KEY_IMPRESSION_ID, data.impressionId) - } - - RCCommonFunctionality.trackAdDisplayed(adData) + iosAdTracker.trackAdDisplayed(data.toIos()) } public actual fun trackAdOpened(data: AdOpenedData) { - if (!IOSAPIAvailabilityChecker().isAdTrackingAPIAvailable()) { + if (!AppleApiAvailability().isAdTrackingAPIAvailable()) { Purchases.logHandler.w("Purchases", "Ad tracking requires iOS 15.0+. Current API is unavailable.") return } - val adData = buildMap { - data.networkName?.let { put(KEY_NETWORK_NAME, it) } - put(KEY_MEDIATOR_NAME, data.mediatorName.value) - put(KEY_AD_FORMAT, data.adFormat.value) - put(KEY_PLACEMENT, data.placement) - put(KEY_AD_UNIT_ID, data.adUnitId) - put(KEY_IMPRESSION_ID, data.impressionId) - } - - RCCommonFunctionality.trackAdOpened(adData) + iosAdTracker.trackAdOpened(data.toIos()) } public actual fun trackAdRevenue(data: AdRevenueData) { - if (!IOSAPIAvailabilityChecker().isAdTrackingAPIAvailable()) { + if (!AppleApiAvailability().isAdTrackingAPIAvailable()) { Purchases.logHandler.w("Purchases", "Ad tracking requires iOS 15.0+. Current API is unavailable.") return } - val adData = buildMap { - data.networkName?.let { put(KEY_NETWORK_NAME, it) } - put(KEY_MEDIATOR_NAME, data.mediatorName.value) - put(KEY_AD_FORMAT, data.adFormat.value) - put(KEY_PLACEMENT, data.placement) - put(KEY_AD_UNIT_ID, data.adUnitId) - put(KEY_IMPRESSION_ID, data.impressionId) - put(KEY_REVENUE_MICROS, NSNumber(long = data.revenueMicros)) - put(KEY_CURRENCY, data.currency) - put(KEY_PRECISION, data.precision.value) - } - - RCCommonFunctionality.trackAdRevenue(adData) + iosAdTracker.trackAdRevenue(data.toIos()) } public actual fun trackAdLoaded(data: AdLoadedData) { - if (!IOSAPIAvailabilityChecker().isAdTrackingAPIAvailable()) { + if (!AppleApiAvailability().isAdTrackingAPIAvailable()) { Purchases.logHandler.w("Purchases", "Ad tracking requires iOS 15.0+. Current API is unavailable.") return } - val adData = buildMap { - data.networkName?.let { put(KEY_NETWORK_NAME, it) } - put(KEY_MEDIATOR_NAME, data.mediatorName.value) - put(KEY_AD_FORMAT, data.adFormat.value) - put(KEY_PLACEMENT, data.placement) - put(KEY_AD_UNIT_ID, data.adUnitId) - put(KEY_IMPRESSION_ID, data.impressionId) - } - - RCCommonFunctionality.trackAdLoaded(adData) + iosAdTracker.trackAdLoaded(data.toIos()) } public actual fun trackAdFailedToLoad(data: AdFailedToLoadData) { - if (!IOSAPIAvailabilityChecker().isAdTrackingAPIAvailable()) { + if (!AppleApiAvailability().isAdTrackingAPIAvailable()) { Purchases.logHandler.w("Purchases", "Ad tracking requires iOS 15.0+. Current API is unavailable.") return } - val adData = buildMap { - put(KEY_MEDIATOR_NAME, data.mediatorName.value) - put(KEY_AD_FORMAT, data.adFormat.value) - put(KEY_PLACEMENT, data.placement) - put(KEY_AD_UNIT_ID, data.adUnitId) - data.mediatorErrorCode?.let { put(KEY_MEDIATOR_ERROR_CODE, NSNumber(int = it)) } - } - - RCCommonFunctionality.trackAdFailedToLoad(adData) + iosAdTracker.trackAdFailedToLoad(data.toIos()) } } diff --git a/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/Purchases.ios.kt b/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/Purchases.ios.kt index 4988e3d2d..0c46114d9 100644 --- a/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/Purchases.ios.kt +++ b/core/src/iosMain/kotlin/com/revenuecat/purchases/kmp/Purchases.ios.kt @@ -1,77 +1,76 @@ package com.revenuecat.purchases.kmp -import swiftPMImport.com.revenuecat.purchases.kn.core.IOSAPIAvailabilityChecker -import swiftPMImport.com.revenuecat.purchases.kn.core.RCCommonFunctionality -import swiftPMImport.com.revenuecat.purchases.kn.core.RCCustomerInfo -import swiftPMImport.com.revenuecat.purchases.kn.core.RCIntroEligibility -import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchaseParamsBuilder -import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchasesDelegateProtocol -import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreProduct -import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreTransaction -import swiftPMImport.com.revenuecat.purchases.kn.core.RCVirtualCurrencies -import swiftPMImport.com.revenuecat.purchases.kn.core.configureWithAPIKey -import swiftPMImport.com.revenuecat.purchases.kn.core.isWebPurchaseRedemptionURL -import swiftPMImport.com.revenuecat.purchases.kn.core.parseAsWebPurchaseRedemptionWithUrlString -import swiftPMImport.com.revenuecat.purchases.kn.core.recordPurchaseForProductID -import swiftPMImport.com.revenuecat.purchases.kn.core.setAirbridgeDeviceID -import swiftPMImport.com.revenuecat.purchases.kn.core.setAirshipChannelID -import swiftPMImport.com.revenuecat.purchases.kn.core.setOnesignalUserID -import swiftPMImport.com.revenuecat.purchases.kn.core.showStoreMessagesForTypes -import swiftPMImport.com.revenuecat.purchases.kn.core.trackCustomPaywallImpression -import com.revenuecat.purchases.kmp.models.CustomPaywallImpressionParams -import com.revenuecat.purchases.kmp.ktx.mapEntriesNotNull -import com.revenuecat.purchases.kmp.mappings.buildStoreTransaction -import com.revenuecat.purchases.kmp.mappings.toCustomerInfo -import com.revenuecat.purchases.kmp.mappings.toHybridString -import com.revenuecat.purchases.kmp.mappings.toIntroEligibilityStatus -import com.revenuecat.purchases.kmp.mappings.toIosCacheFetchPolicy -import com.revenuecat.purchases.kmp.mappings.toIosPackage -import com.revenuecat.purchases.kmp.mappings.toIosPromotionalOffer -import com.revenuecat.purchases.kmp.mappings.toIosStoreProduct -import com.revenuecat.purchases.kmp.mappings.toIosStoreProductDiscount -import com.revenuecat.purchases.kmp.mappings.toIosWinBackOffer -import com.revenuecat.purchases.kmp.mappings.toOfferings -import com.revenuecat.purchases.kmp.mappings.toPromotionalOffer -import com.revenuecat.purchases.kmp.mappings.toPurchasesErrorOrThrow -import com.revenuecat.purchases.kmp.mappings.toStoreProduct -import com.revenuecat.purchases.kmp.mappings.toStoreTransaction -import com.revenuecat.purchases.kmp.mappings.toStorefront -import com.revenuecat.purchases.kmp.mappings.toVirtualCurrencies -import com.revenuecat.purchases.kmp.mappings.toWinBackOffer -import com.revenuecat.purchases.kmp.models.AdDisplayedData -import com.revenuecat.purchases.kmp.models.AdFailedToLoadData -import com.revenuecat.purchases.kmp.models.AdLoadedData -import com.revenuecat.purchases.kmp.models.AdOpenedData -import com.revenuecat.purchases.kmp.models.AdRevenueData -import com.revenuecat.purchases.kmp.models.BillingFeature -import com.revenuecat.purchases.kmp.models.CacheFetchPolicy -import com.revenuecat.purchases.kmp.models.CustomerInfo -import com.revenuecat.purchases.kmp.models.DangerousSettings -import com.revenuecat.purchases.kmp.models.IntroEligibilityStatus -import com.revenuecat.purchases.kmp.models.Offerings -import com.revenuecat.purchases.kmp.models.Package -import com.revenuecat.purchases.kmp.models.PromotionalOffer -import com.revenuecat.purchases.kmp.models.PurchasesError -import com.revenuecat.purchases.kmp.models.PurchasesErrorCode -import com.revenuecat.purchases.kmp.models.RedeemWebPurchaseListener -import com.revenuecat.purchases.kmp.models.ReplacementMode -import com.revenuecat.purchases.kmp.models.Store -import com.revenuecat.purchases.kmp.models.StoreMessageType -import com.revenuecat.purchases.kmp.models.StoreProduct -import com.revenuecat.purchases.kmp.models.StoreProductDiscount -import com.revenuecat.purchases.kmp.models.StoreTransaction -import com.revenuecat.purchases.kmp.models.Storefront -import com.revenuecat.purchases.kmp.models.SubscriptionOption -import com.revenuecat.purchases.kmp.models.VirtualCurrencies -import com.revenuecat.purchases.kmp.models.VirtualCurrency -import com.revenuecat.purchases.kmp.models.WebPurchaseRedemption -import com.revenuecat.purchases.kmp.models.WinBackOffer -import com.revenuecat.purchases.kmp.strings.ConfigureStrings -import platform.Foundation.NSError -import platform.Foundation.NSURL -import swiftPMImport.com.revenuecat.purchases.kn.core.RCDangerousSettings as IosDangerousSettings -import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchases as IosPurchases -import swiftPMImport.com.revenuecat.purchases.kn.core.RCWinBackOffer as NativeIosWinBackOffer + import com.revenuecat.purchases.kmp.ktx.mapEntriesNotNull + import com.revenuecat.purchases.kmp.mappings.toCustomerInfo + import com.revenuecat.purchases.kmp.mappings.toIntroEligibilityStatus + import com.revenuecat.purchases.kmp.mappings.toIosCacheFetchPolicy + import com.revenuecat.purchases.kmp.mappings.toIosEntitlementVerificationMode + import com.revenuecat.purchases.kmp.mappings.toIosPackage + import com.revenuecat.purchases.kmp.mappings.toIosPromotionalOffer + import com.revenuecat.purchases.kmp.mappings.toIosPurchasesAreCompletedBy + import com.revenuecat.purchases.kmp.mappings.toIosStoreKitVersion + import com.revenuecat.purchases.kmp.mappings.toIosStoreMessageTypes + import com.revenuecat.purchases.kmp.mappings.toIosStoreProduct + import com.revenuecat.purchases.kmp.mappings.toIosStoreProductDiscount + import com.revenuecat.purchases.kmp.mappings.toIosWinBackOffer + import com.revenuecat.purchases.kmp.mappings.toOfferings + import com.revenuecat.purchases.kmp.mappings.toPromotionalOffer + import com.revenuecat.purchases.kmp.mappings.toPurchasesErrorOrThrow + import com.revenuecat.purchases.kmp.mappings.toStoreProduct + import com.revenuecat.purchases.kmp.mappings.toStoreTransaction + import com.revenuecat.purchases.kmp.mappings.toStorefront + import com.revenuecat.purchases.kmp.mappings.toVirtualCurrencies + import com.revenuecat.purchases.kmp.mappings.toWinBackOffer + import com.revenuecat.purchases.kmp.models.BillingFeature + import com.revenuecat.purchases.kmp.models.CacheFetchPolicy + import com.revenuecat.purchases.kmp.models.CustomPaywallImpressionParams + import com.revenuecat.purchases.kmp.models.CustomerInfo + import com.revenuecat.purchases.kmp.models.DangerousSettings + import com.revenuecat.purchases.kmp.models.IntroEligibilityStatus + import com.revenuecat.purchases.kmp.models.Offerings + import com.revenuecat.purchases.kmp.models.Package + import com.revenuecat.purchases.kmp.models.PromotionalOffer + import com.revenuecat.purchases.kmp.models.PurchasesError + import com.revenuecat.purchases.kmp.models.PurchasesErrorCode + import com.revenuecat.purchases.kmp.models.RedeemWebPurchaseListener + import com.revenuecat.purchases.kmp.models.ReplacementMode + import com.revenuecat.purchases.kmp.models.Store + import com.revenuecat.purchases.kmp.models.StoreMessageType + import com.revenuecat.purchases.kmp.models.StoreProduct + import com.revenuecat.purchases.kmp.models.StoreProductDiscount + import com.revenuecat.purchases.kmp.models.StoreTransaction + import com.revenuecat.purchases.kmp.models.Storefront + import com.revenuecat.purchases.kmp.models.SubscriptionOption + import com.revenuecat.purchases.kmp.models.VirtualCurrencies + import com.revenuecat.purchases.kmp.models.WebPurchaseRedemption + import com.revenuecat.purchases.kmp.models.WinBackOffer + import com.revenuecat.purchases.kmp.strings.ConfigureStrings + import platform.Foundation.NSError + import platform.Foundation.NSURL + import platform.Foundation.NSUserDefaults + import swiftPMImport.com.revenuecat.purchases.kn.core.RCConfiguration + import swiftPMImport.com.revenuecat.purchases.kn.core.RCCustomPaywallImpressionParams + import swiftPMImport.com.revenuecat.purchases.kn.core.RCCustomerInfo + import swiftPMImport.com.revenuecat.purchases.kn.core.RCIntroEligibility + import swiftPMImport.com.revenuecat.purchases.kn.core.RCPlatformInfo + import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchaseParamsBuilder + import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchasesDelegateProtocol + import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreProduct + import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreTransaction + import swiftPMImport.com.revenuecat.purchases.kn.core.RCVirtualCurrencies + import swiftPMImport.com.revenuecat.purchases.kn.core.additional.AppleApiAvailability + import swiftPMImport.com.revenuecat.purchases.kn.core.configureWithConfiguration + import swiftPMImport.com.revenuecat.purchases.kn.core.enableAdServicesAttributionTokenCollection + import swiftPMImport.com.revenuecat.purchases.kn.core.parseAsWebPurchaseRedemption + import swiftPMImport.com.revenuecat.purchases.kn.core.recordPurchaseForProductID + import swiftPMImport.com.revenuecat.purchases.kn.core.setAirbridgeDeviceID + import swiftPMImport.com.revenuecat.purchases.kn.core.setAirshipChannelID + import swiftPMImport.com.revenuecat.purchases.kn.core.setOnesignalUserID + import swiftPMImport.com.revenuecat.purchases.kn.core.showStoreMessagesForTypes + import swiftPMImport.com.revenuecat.purchases.kn.core.trackCustomPaywallImpression + import swiftPMImport.com.revenuecat.purchases.kn.core.RCDangerousSettings as IosDangerousSettings + import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchases as IosPurchases + import swiftPMImport.com.revenuecat.purchases.kn.core.RCWinBackOffer as NativeIosWinBackOffer public actual class Purchases private constructor(private val iosPurchases: IosPurchases) { public actual companion object { @@ -106,25 +105,33 @@ public actual class Purchases private constructor(private val iosPurchases: IosP public actual fun configure( configuration: PurchasesConfiguration - ): Purchases { - checkCommonVersion() - - return with(configuration) { - IosPurchases.configureWithAPIKey( - apiKey = apiKey, - appUserID = appUserId, - purchasesAreCompletedBy = purchasesAreCompletedBy.toHybridString(), - userDefaultsSuiteName = userDefaultsSuiteName, - platformFlavor = BuildKonfig.platformFlavor, - platformFlavorVersion = frameworkVersion, - storeKitVersion = storeKitVersion.toHybridString(), - dangerousSettings = dangerousSettings.toIosDangerousSettings(), - shouldShowInAppMessagesAutomatically = showInAppMessagesAutomatically, - verificationMode = verificationMode.name, - ) - }.let { Purchases(it) } - .also { _sharedInstance = it } - } + ): Purchases = with(configuration) { + IosPurchases.configureWithConfiguration( + configuration = RCConfiguration.builderWithAPIKey(apiKey) + .withAppUserID(appUserId) + .apply { + purchasesAreCompletedBy.toIosPurchasesAreCompletedBy().also { + withPurchasesAreCompletedBy( + purchasesAreCompletedBy = it.purchasesAreCompletedBy, + storeKitVersion = it.storeKitVersion + ) + } + } + .withUserDefaults(NSUserDefaults(suiteName = userDefaultsSuiteName)) + .withPlatformInfo( + RCPlatformInfo( + flavor = BuildKonfig.platformFlavor, + version = frameworkVersion + ) + ) + .withStoreKitVersion(storeKitVersion.toIosStoreKitVersion()) + .withDangerousSettings(dangerousSettings.toIosDangerousSettings()) + .withShowStoreMessagesAutomatically(showInAppMessagesAutomatically) + .withEntitlementVerificationMode(verificationMode.toIosEntitlementVerificationMode()) + .build() + ) + }.let { Purchases(it) } + .also { _sharedInstance = it } public actual fun canMakePayments( features: List, @@ -133,20 +140,11 @@ public actual class Purchases private constructor(private val iosPurchases: IosP callback(IosPurchases.canMakePayments()) } - private fun checkCommonVersion() { - val expected = BuildKonfig.revenuecatCommonVersion - val actual = RCCommonFunctionality.hybridCommonVersion() - check(actual == expected) { - "Unexpected version of PurchasesHybridCommon ('$actual'). Make sure to use " + - "'$expected' exactly." - } - } - private fun DangerousSettings.toIosDangerousSettings(): IosDangerousSettings = IosDangerousSettings(autoSyncPurchases) public actual fun parseAsWebPurchaseRedemption(url: String): WebPurchaseRedemption? { - return if (RCCommonFunctionality.isWebPurchaseRedemptionURL(url)) { + return if (IosPurchases.parseAsWebPurchaseRedemption(NSURL(string = url)) != null) { WebPurchaseRedemption(url) } else { null @@ -342,40 +340,23 @@ public actual class Purchases private constructor(private val iosPurchases: IosP onError: (error: PurchasesError) -> Unit, onSuccess: (storeTransaction: StoreTransaction) -> Unit, ) { - RCCommonFunctionality.recordPurchaseForProductID( - productID, - completion = { storeTransactionMap, error -> - if (error != null) { - onError(error.error().toPurchasesErrorOrThrow()) - return@recordPurchaseForProductID - } - - if (storeTransactionMap == null) { - onError( - PurchasesError( - code = PurchasesErrorCode.UnknownError, - underlyingErrorMessage = - "Expected storeTransactionMap to be non-null when error is non-null." - ) - ) - } else { - val storeTransactionMappingResult = buildStoreTransaction( - storeTransactionMap = storeTransactionMap + iosPurchases.recordPurchaseForProductID(productID) { storeTransaction, error -> + if (error != null) { + onError(error.toPurchasesErrorOrThrow()) + return@recordPurchaseForProductID + } + if (storeTransaction == null) { + onError( + PurchasesError( + code = PurchasesErrorCode.UnknownError, + underlyingErrorMessage = + "Expected a non-null RCStoreTransaction when error is non-null." ) - storeTransactionMappingResult.onSuccess { - onSuccess(it) - } - storeTransactionMappingResult.onFailure { - onError( - PurchasesError( - code = PurchasesErrorCode.UnknownError, - underlyingErrorMessage = it.message - ) - ) - } - } + ) + } else { + onSuccess(storeTransaction.toStoreTransaction()) } - ) + } } public actual fun checkTrialOrIntroPriceEligibility( @@ -403,7 +384,7 @@ public actual class Purchases private constructor(private val iosPurchases: IosP // API availability checks must be performed here at the KMP level, since the KMP/ObjC/Swift // interoperability drops the @available(osVersion) requirements, and you can technically // call functions with an @available from any OS version in KMP - if (!IOSAPIAvailabilityChecker().isWinBackOfferAPIAvailable()) { + if (!AppleApiAvailability().isWinBackOfferAPIAvailable()) { onError( PurchasesError( PurchasesErrorCode.UnsupportedError, @@ -449,7 +430,7 @@ public actual class Purchases private constructor(private val iosPurchases: IosP // API availability checks must be performed here at the KMP level, since the KMP/ObjC/Swift // interoperability drops the @available(osVersion) requirements, and you can technically // call functions with an @available from any OS version in KMP - if (!IOSAPIAvailabilityChecker().isWinBackOfferAPIAvailable()) { + if (!AppleApiAvailability().isWinBackOfferAPIAvailable()) { onError( PurchasesError( PurchasesErrorCode.UnsupportedError, @@ -496,7 +477,7 @@ public actual class Purchases private constructor(private val iosPurchases: IosP // API availability checks must be performed here at the KMP level, since the KMP/ObjC/Swift // interoperability drops the @available(osVersion) requirements, and you can technically // call functions with an @available from any OS version in KMP - if (!IOSAPIAvailabilityChecker().isWinBackOfferAPIAvailable()) { + if (!AppleApiAvailability().isWinBackOfferAPIAvailable()) { onError( PurchasesError( PurchasesErrorCode.UnsupportedError, @@ -553,7 +534,7 @@ public actual class Purchases private constructor(private val iosPurchases: IosP // API availability checks must be performed here at the KMP level, since the KMP/ObjC/Swift // interoperability drops the @available(osVersion) requirements, and you can technically // call functions with an @available from any OS version in KMP - if (!IOSAPIAvailabilityChecker().isWinBackOfferAPIAvailable()) { + if (!AppleApiAvailability().isWinBackOfferAPIAvailable()) { onError( PurchasesError( PurchasesErrorCode.UnsupportedError, @@ -638,17 +619,9 @@ public actual class Purchases private constructor(private val iosPurchases: IosP public actual fun showInAppMessagesIfNeeded( messageTypes: List, - ): Unit = RCCommonFunctionality.showStoreMessagesForTypes( - rawValues = messageTypes.mapTo(mutableSetOf()) { type -> - when (type) { - StoreMessageType.BILLING_ISSUES -> 0 - StoreMessageType.PRICE_INCREASE_CONSENT -> 1 - StoreMessageType.GENERIC -> 2 - StoreMessageType.WIN_BACK_OFFER -> 3 - } - }, - completion = { } - ) + ) { + iosPurchases.showStoreMessagesForTypes(messageTypes.toIosStoreMessageTypes()) {} + } public actual fun invalidateCustomerInfoCache(): Unit = iosPurchases.invalidateCustomerInfoCache() @@ -675,10 +648,10 @@ public actual class Purchases private constructor(private val iosPurchases: IosP iosPurchases.setOnesignalID(onesignalID) public actual fun setOnesignalUserID(onesignalUserID: String?): Unit = - RCCommonFunctionality.setOnesignalUserID(onesignalUserID) + iosPurchases.attribution().setOnesignalUserID(onesignalUserID) public actual fun setAirshipChannelID(airshipChannelID: String?): Unit = - RCCommonFunctionality.setAirshipChannelID(airshipChannelID) + iosPurchases.attribution().setAirshipChannelID(airshipChannelID) public actual fun setFirebaseAppInstanceID(firebaseAppInstanceID: String?): Unit = iosPurchases.setFirebaseAppInstanceID(firebaseAppInstanceID) @@ -693,7 +666,7 @@ public actual class Purchases private constructor(private val iosPurchases: IosP iosPurchases.setAppsflyerID(appsflyerID) public actual fun setAirbridgeDeviceID(airbridgeDeviceID: String?): Unit = - RCCommonFunctionality.setAirbridgeDeviceID(airbridgeDeviceID) + iosPurchases.attribution().setAirbridgeDeviceID(airbridgeDeviceID) public actual fun setFBAnonymousID(fbAnonymousID: String?): Unit = iosPurchases.setFBAnonymousID(fbAnonymousID) @@ -723,8 +696,8 @@ public actual class Purchases private constructor(private val iosPurchases: IosP iosPurchases.setCreative(creative) public actual fun presentCodeRedemptionSheet() { - if (IOSAPIAvailabilityChecker().isCodeRedemptionSheetAPIAvailable()) - RCCommonFunctionality.presentCodeRedemptionSheet() + if (AppleApiAvailability().isCodeRedemptionSheetAPIAvailable()) + iosPurchases.presentCodeRedemptionSheet() else logHandler.d( tag = "Purchases", msg = "`presentCodeRedemptionSheet()` is only available on iOS 14.0 and up." @@ -732,8 +705,8 @@ public actual class Purchases private constructor(private val iosPurchases: IosP } public actual fun enableAdServicesAttributionTokenCollection() { - if (IOSAPIAvailabilityChecker().isEnableAdServicesAttributionTokenCollectionAPIAvailable()) - RCCommonFunctionality.enableAdServicesAttributionTokenCollection() + if (AppleApiAvailability().isEnableAdServicesAttributionTokenCollectionAPIAvailable()) + iosPurchases.attribution().enableAdServicesAttributionTokenCollection() else logHandler.d( tag = "Purchases", msg = "`enableAdServicesAttributionTokenCollection()` is only available on iOS 14.3 " + @@ -745,8 +718,8 @@ public actual class Purchases private constructor(private val iosPurchases: IosP webPurchaseRedemption: WebPurchaseRedemption, listener: RedeemWebPurchaseListener, ) { - val nativeWebPurchaseRedemption = RCCommonFunctionality.parseAsWebPurchaseRedemptionWithUrlString( - webPurchaseRedemption.redemptionUrl, + val nativeWebPurchaseRedemption = IosPurchases.parseAsWebPurchaseRedemption( + url = NSURL(string = webPurchaseRedemption.redemptionUrl) ) if (nativeWebPurchaseRedemption == null) { listener.handleResult(RedeemWebPurchaseListener.Result.Error( @@ -814,19 +787,21 @@ public actual class Purchases private constructor(private val iosPurchases: IosP public actual fun trackCustomPaywallImpression( params: CustomPaywallImpressionParams, ) { - if (!IOSAPIAvailabilityChecker().isCustomPaywallTrackingAPIAvailable()) { - Purchases.logHandler.w( + if (!AppleApiAvailability().isCustomPaywallTrackingAPIAvailable()) { + logHandler.w( "Purchases", "Custom paywall tracking requires iOS 15.0+. Current API is unavailable." ) return } - val data = mutableMapOf() - params.paywallId?.let { data["paywallId"] = it } - params.offeringId?.let { data["offeringId"] = it } - RCCommonFunctionality.trackCustomPaywallImpression(data) + iosPurchases.trackCustomPaywallImpression( + RCCustomPaywallImpressionParams( + paywallId = params.paywallId, + offeringId = params.offeringId + ) + ) } @ExperimentalRevenueCatApi - public actual val adTracker: AdTracker by lazy { AdTracker() } + public actual val adTracker: AdTracker by lazy { AdTracker(iosPurchases.adTracker()) } } diff --git a/core/src/commonTest/kotlin/com/revenuecat/purchases/kmp/PrintLnLogHandler.kt b/core/src/iosTest/kotlin/com/revenuecat/purchases/kmp/PrintLnLogHandler.kt similarity index 100% rename from core/src/commonTest/kotlin/com/revenuecat/purchases/kmp/PrintLnLogHandler.kt rename to core/src/iosTest/kotlin/com/revenuecat/purchases/kmp/PrintLnLogHandler.kt diff --git a/datetime/.gitignore b/datetime/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/datetime/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/datetime/api/datetime.api b/datetime/api/datetime.api deleted file mode 100644 index da12561c1..000000000 --- a/datetime/api/datetime.api +++ /dev/null @@ -1,21 +0,0 @@ -public final class com/revenuecat/purchases/kmp/datetime/CustomerInfoKt { - public static final fun getAllExpirationInstants (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)Ljava/util/Map; - public static final fun getAllPurchaseInstants (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)Ljava/util/Map; - public static final fun getFirstSeenInstant (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)Lkotlinx/datetime/Instant; - public static final fun getLatestExpirationInstant (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)Lkotlinx/datetime/Instant; - public static final fun getOriginalPurchaseInstant (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)Lkotlinx/datetime/Instant; - public static final fun getRequestInstant (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)Lkotlinx/datetime/Instant; -} - -public final class com/revenuecat/purchases/kmp/datetime/EntitlementInfoKt { - public static final fun getBillingIssueDetectedAtInstant (Lcom/revenuecat/purchases/kmp/models/EntitlementInfo;)Lkotlinx/datetime/Instant; - public static final fun getExpirationInstant (Lcom/revenuecat/purchases/kmp/models/EntitlementInfo;)Lkotlinx/datetime/Instant; - public static final fun getLatestPurchaseInstant (Lcom/revenuecat/purchases/kmp/models/EntitlementInfo;)Lkotlinx/datetime/Instant; - public static final fun getOriginalPurchaseInstant (Lcom/revenuecat/purchases/kmp/models/EntitlementInfo;)Lkotlinx/datetime/Instant; - public static final fun getUnsubscribeDetectedAtInstant (Lcom/revenuecat/purchases/kmp/models/EntitlementInfo;)Lkotlinx/datetime/Instant; -} - -public final class com/revenuecat/purchases/kmp/datetime/TransactionKt { - public static final fun getPurchaseInstant (Lcom/revenuecat/purchases/kmp/models/Transaction;)Lkotlinx/datetime/Instant; -} - diff --git a/datetime/api/datetime.klib.api b/datetime/api/datetime.klib.api deleted file mode 100644 index d3565bcfa..000000000 --- a/datetime/api/datetime.klib.api +++ /dev/null @@ -1,32 +0,0 @@ -// Klib ABI Dump -// Targets: [iosArm64, iosSimulatorArm64, iosX64] -// Rendering settings: -// - Signature version: 2 -// - Show manifest properties: true -// - Show declarations: true - -// Library unique name: -final val com.revenuecat.purchases.kmp.datetime/allExpirationInstants // com.revenuecat.purchases.kmp.datetime/allExpirationInstants|@com.revenuecat.purchases.kmp.models.CustomerInfo{}allExpirationInstants[0] - final fun (com.revenuecat.purchases.kmp.models/CustomerInfo).(): kotlin.collections/Map // com.revenuecat.purchases.kmp.datetime/allExpirationInstants.|@com.revenuecat.purchases.kmp.models.CustomerInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/allPurchaseInstants // com.revenuecat.purchases.kmp.datetime/allPurchaseInstants|@com.revenuecat.purchases.kmp.models.CustomerInfo{}allPurchaseInstants[0] - final fun (com.revenuecat.purchases.kmp.models/CustomerInfo).(): kotlin.collections/Map // com.revenuecat.purchases.kmp.datetime/allPurchaseInstants.|@com.revenuecat.purchases.kmp.models.CustomerInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/billingIssueDetectedAtInstant // com.revenuecat.purchases.kmp.datetime/billingIssueDetectedAtInstant|@com.revenuecat.purchases.kmp.models.EntitlementInfo{}billingIssueDetectedAtInstant[0] - final fun (com.revenuecat.purchases.kmp.models/EntitlementInfo).(): kotlinx.datetime/Instant? // com.revenuecat.purchases.kmp.datetime/billingIssueDetectedAtInstant.|@com.revenuecat.purchases.kmp.models.EntitlementInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/expirationInstant // com.revenuecat.purchases.kmp.datetime/expirationInstant|@com.revenuecat.purchases.kmp.models.EntitlementInfo{}expirationInstant[0] - final fun (com.revenuecat.purchases.kmp.models/EntitlementInfo).(): kotlinx.datetime/Instant? // com.revenuecat.purchases.kmp.datetime/expirationInstant.|@com.revenuecat.purchases.kmp.models.EntitlementInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/firstSeenInstant // com.revenuecat.purchases.kmp.datetime/firstSeenInstant|@com.revenuecat.purchases.kmp.models.CustomerInfo{}firstSeenInstant[0] - final fun (com.revenuecat.purchases.kmp.models/CustomerInfo).(): kotlinx.datetime/Instant // com.revenuecat.purchases.kmp.datetime/firstSeenInstant.|@com.revenuecat.purchases.kmp.models.CustomerInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/latestExpirationInstant // com.revenuecat.purchases.kmp.datetime/latestExpirationInstant|@com.revenuecat.purchases.kmp.models.CustomerInfo{}latestExpirationInstant[0] - final fun (com.revenuecat.purchases.kmp.models/CustomerInfo).(): kotlinx.datetime/Instant? // com.revenuecat.purchases.kmp.datetime/latestExpirationInstant.|@com.revenuecat.purchases.kmp.models.CustomerInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/latestPurchaseInstant // com.revenuecat.purchases.kmp.datetime/latestPurchaseInstant|@com.revenuecat.purchases.kmp.models.EntitlementInfo{}latestPurchaseInstant[0] - final fun (com.revenuecat.purchases.kmp.models/EntitlementInfo).(): kotlinx.datetime/Instant? // com.revenuecat.purchases.kmp.datetime/latestPurchaseInstant.|@com.revenuecat.purchases.kmp.models.EntitlementInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/originalPurchaseInstant // com.revenuecat.purchases.kmp.datetime/originalPurchaseInstant|@com.revenuecat.purchases.kmp.models.CustomerInfo{}originalPurchaseInstant[0] - final fun (com.revenuecat.purchases.kmp.models/CustomerInfo).(): kotlinx.datetime/Instant? // com.revenuecat.purchases.kmp.datetime/originalPurchaseInstant.|@com.revenuecat.purchases.kmp.models.CustomerInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/originalPurchaseInstant // com.revenuecat.purchases.kmp.datetime/originalPurchaseInstant|@com.revenuecat.purchases.kmp.models.EntitlementInfo{}originalPurchaseInstant[0] - final fun (com.revenuecat.purchases.kmp.models/EntitlementInfo).(): kotlinx.datetime/Instant? // com.revenuecat.purchases.kmp.datetime/originalPurchaseInstant.|@com.revenuecat.purchases.kmp.models.EntitlementInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/purchaseInstant // com.revenuecat.purchases.kmp.datetime/purchaseInstant|@com.revenuecat.purchases.kmp.models.Transaction{}purchaseInstant[0] - final fun (com.revenuecat.purchases.kmp.models/Transaction).(): kotlinx.datetime/Instant // com.revenuecat.purchases.kmp.datetime/purchaseInstant.|@com.revenuecat.purchases.kmp.models.Transaction(){}[0] -final val com.revenuecat.purchases.kmp.datetime/requestInstant // com.revenuecat.purchases.kmp.datetime/requestInstant|@com.revenuecat.purchases.kmp.models.CustomerInfo{}requestInstant[0] - final fun (com.revenuecat.purchases.kmp.models/CustomerInfo).(): kotlinx.datetime/Instant // com.revenuecat.purchases.kmp.datetime/requestInstant.|@com.revenuecat.purchases.kmp.models.CustomerInfo(){}[0] -final val com.revenuecat.purchases.kmp.datetime/unsubscribeDetectedAtInstant // com.revenuecat.purchases.kmp.datetime/unsubscribeDetectedAtInstant|@com.revenuecat.purchases.kmp.models.EntitlementInfo{}unsubscribeDetectedAtInstant[0] - final fun (com.revenuecat.purchases.kmp.models/EntitlementInfo).(): kotlinx.datetime/Instant? // com.revenuecat.purchases.kmp.datetime/unsubscribeDetectedAtInstant.|@com.revenuecat.purchases.kmp.models.EntitlementInfo(){}[0] diff --git a/datetime/build.gradle.kts b/datetime/build.gradle.kts deleted file mode 100644 index 483c99941..000000000 --- a/datetime/build.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id("revenuecat-library") -} - -revenueCat { - dokka = true -} - -kotlin { - sourceSets { - commonMain.dependencies { - api(libs.kotlinx.datetime) - implementation(projects.core) - } - } -} - -android { - namespace = "com.revenuecat.purchases.kmp.datetime" -} diff --git a/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/CustomerInfo.kt b/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/CustomerInfo.kt deleted file mode 100644 index 40ae328c5..000000000 --- a/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/CustomerInfo.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.revenuecat.purchases.kmp.datetime - -import com.revenuecat.purchases.kmp.Purchases -import com.revenuecat.purchases.kmp.models.CustomerInfo -import kotlinx.datetime.Instant - -/** - * Map of productIds to expiration instants. - * * For Google subscriptions, productIds are `subscriptionId:basePlanId`. - * * For Amazon subscriptions, productsIds are `termSku`. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("allExpirationDates"), -) -public val CustomerInfo.allExpirationInstants: Map - get() = allExpirationDateMillis.mapValues { (_, millis) -> - millis?.let { Instant.fromEpochMilliseconds(it) } - } - -/** - * Map of productIds to purchase instants. - * * For Google subscriptions, productIds are `subscriptionId:basePlanId`. - * * For Google and Amazon INAPPs, productsIds are simply `productId`. - * * For Amazon subscriptions, productsIds are `termSku`. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("allPurchaseDates"), -) -public val CustomerInfo.allPurchaseInstants: Map - get() = allPurchaseDateMillis.mapValues { (_, millis) -> - millis?.let { Instant.fromEpochMilliseconds(it) } - } - -/** - * The instant this user was first seen in RevenueCat. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("firstSeen"), -) -public val CustomerInfo.firstSeenInstant: Instant - get() = Instant.fromEpochMilliseconds(firstSeenMillis) - -/** - * The latest expiration instant of all purchased productIds. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("latestExpirationDate"), -) -public val CustomerInfo.latestExpirationInstant: Instant? - get() = latestExpirationDateMillis?.let { Instant.fromEpochMilliseconds(it) } - -/** - * The purchase instant for the version of the application when the user bought the app. Use this - * for grandfathering users when migrating to subscriptions. This can be null, see - * [Purchases.restorePurchases]. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("originalPurchaseDate"), -) -public val CustomerInfo.originalPurchaseInstant: Instant? - get() = originalPurchaseDateMillis?.let { Instant.fromEpochMilliseconds(it) } - -/** - * The instant when this info was requested. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("requestDate"), -) -public val CustomerInfo.requestInstant: Instant - get() = Instant.fromEpochMilliseconds(requestDateMillis) diff --git a/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/EntitlementInfo.kt b/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/EntitlementInfo.kt deleted file mode 100644 index eb5c707f8..000000000 --- a/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/EntitlementInfo.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.revenuecat.purchases.kmp.datetime - -import com.revenuecat.purchases.kmp.models.EntitlementInfo -import com.revenuecat.purchases.kmp.models.PeriodType -import kotlinx.datetime.Instant - - -/** - * Nullable on iOS only not on Android. The latest purchase or renewal instant for the entitlement. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("latestPurchaseDate"), -) -public val EntitlementInfo.latestPurchaseInstant: Instant? - get() = latestPurchaseDateMillis?.let { Instant.fromEpochMilliseconds(it) } - -/** - * Nullable on iOS only not on Android. The first instant this entitlement was purchased. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("originalPurchaseDate"), -) -public val EntitlementInfo.originalPurchaseInstant: Instant? - get() = originalPurchaseDateMillis?.let { Instant.fromEpochMilliseconds(it) } - -/** - * The expiration instant for the entitlement, can be `null` for lifetime access. If the - * [periodType] is [PeriodType.TRIAL], this is the trial expiration instant. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("expirationDate"), -) -public val EntitlementInfo.expirationInstant: Instant? - get() = expirationDateMillis?.let { Instant.fromEpochMilliseconds(it) } - -/** - * The instant an unsubscribe was detected. Can be `null`. - * - * Note: Entitlement may still be active even if user has unsubscribed. Check the [isActive] - * property. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("unsubscribeDetectedAt"), -) -public val EntitlementInfo.unsubscribeDetectedAtInstant: Instant? - get() = unsubscribeDetectedAtMillis?.let { Instant.fromEpochMilliseconds(it) } - -/** - * The instant a billing issue was detected. Can be `null` if there is no billing issue or an issue - * has been resolved. Note: Entitlement may still be active even if there is a billing issue. - * Check the [isActive] property. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("billingIssueDetectedAt"), -) -public val EntitlementInfo.billingIssueDetectedAtInstant: Instant? - get() = billingIssueDetectedAtMillis?.let { Instant.fromEpochMilliseconds(it) } diff --git a/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/Transaction.kt b/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/Transaction.kt deleted file mode 100644 index 4850c1e9f..000000000 --- a/datetime/src/commonMain/kotlin/com/revenuecat/purchases/kmp/datetime/Transaction.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.revenuecat.purchases.kmp.datetime - -import com.revenuecat.purchases.kmp.models.Transaction -import kotlinx.datetime.Instant - -/** - * The purchase instant. - */ -@Deprecated( - message = "Instant properties have been moved to member fields. The entire " + - "purchases-kmp-datetime module is deprecated and can be removed from your Gradle " + - "files.", - replaceWith = ReplaceWith("purchaseDate"), -) -public val Transaction.purchaseInstant: Instant - get() = Instant.fromEpochMilliseconds(purchaseDateMillis) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 097b8685b..959200857 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -24,7 +24,6 @@ FILES_WITH_SDK_VERSION = { desc "Bumps version, edits changelog, and creates pull request" lane :bump do |options| - phc_version = get_phc_version bump_version_update_changelog_create_pr( current_version: current_version_number, changelog_latest_path: PATH_CHANGELOG_LATEST, @@ -36,16 +35,10 @@ lane :bump do |options| editor: options[:editor], next_version: options[:next_version], automatic_release: options[:automatic_release], - hybrid_common_version: phc_version, versions_file_path: PATH_VERSIONS, - is_prerelease: options[:is_prerelease], - append_phc_version_if_next_version_is_not_prerelease: true - ) - update_hybrids_versions_file( - versions_file_path: PATH_VERSIONS, - new_sdk_version: current_version_number, - hybrid_common_version: phc_version + is_prerelease: options[:is_prerelease] ) + update_versions_file(current_version_number) commit_current_changes(commit_message: 'Updates VERSIONS.md') push_to_git_remote(set_upstream: true) end @@ -68,62 +61,6 @@ lane :automatic_bump do |options| bump(options) end -desc "Update purchases-hybrid-common dependency" -lane :update_hybrid_common do |options| - if options[:dry_run] - dry_run = true - end - if options[:version] - new_phc_version = options[:version] - else - UI.user_error!("Missing `version` argument") - end - - current_phc_version = get_phc_version - - UI.message("ℹ️ Current Purchases Hybrid Common version: #{current_phc_version}") - UI.message("ℹ️ Setting Purchases Hybrid Common version: #{new_phc_version}") - files_to_update = { - "gradle/libs.versions.toml" => ["revenuecat-common = \"{x}\""], - "core/core.podspec" => ["spec.dependency 'PurchasesHybridCommon', '{x}'"], - "mappings/mappings.podspec" => ["spec.dependency 'PurchasesHybridCommon', '{x}'"], - "models/models.podspec" => ["spec.dependency 'PurchasesHybridCommon', '{x}'"], - "revenuecatui/revenuecatui.podspec" => ["spec.dependency 'PurchasesHybridCommonUI', '{x}'"], - "iosApp/Podfile" => [" pod 'PurchasesHybridCommon', '{x}'", " pod 'PurchasesHybridCommonUI', '{x}'"], - } - - if dry_run - UI.message("ℹ️ Nothing more to do, dry_run: true") - next - end - - bump_phc_version( - repo_name: REPO_NAME, - files_to_update: files_to_update, - current_version: current_phc_version, - next_version: new_phc_version, - open_pr: options[:open_pr] || false, - automatic_release: options[:automatic_release] || false - ) - # Making sure Podfile.lock is updated. - podfile = "iosApp/Podfile" - cocoapods( - podfile: podfile, - repo_update: true - ) - if options[:open_pr] - git_commit( - path: ["#{podfile}.lock"], - message: "Updates Podfile.lock", - skip_git_hooks: true - ) - push_to_git_remote( - tags: false, - no_verify: true - ) - end -end - desc "Opens a PR updating the SDK version to the next minor, appending -SNAPSHOT." lane :prepare_next_snapshot_version do |options| create_next_snapshot_version( @@ -179,40 +116,67 @@ lane :github_release do |options| ) end -def get_phc_version - android_phc_version = File.read("../gradle/libs.versions.toml") - .match("revenuecat-common = \"(.*)\"") - .captures[0] - UI.user_error!("Android PHC version not found.") if android_phc_version.nil? - - ios_phc_version = File.read("../core/core.podspec") - .match("spec.dependency 'PurchasesHybridCommon', '(.*)'") +def current_version_number + File.read("../gradle/libs.versions.toml") + .match('revenuecat-kmp = "(.*)"') .captures[0] - UI.user_error!("iOS PHC version not found.") if ios_phc_version.nil? +end - ios_phc_ui_version = File.read("../revenuecatui/revenuecatui.podspec") - .match("spec.dependency 'PurchasesHybridCommonUI', '(.*)'") +def get_android_version + version = File.read("../gradle/libs.versions.toml") + .match('revenuecat-android = "(.*)"') .captures[0] - UI.user_error!("iOS PHC UI version not found.") if ios_phc_ui_version.nil? - - versions_mismatch_error = "Found multiple PHC (UI) versions. Please make sure to use only 1." - versions_match = android_phc_version == ios_phc_version && ios_phc_version == ios_phc_ui_version - UI.user_error!(versions_mismatch_error) unless versions_match - - android_phc_version - end + UI.user_error!("Android version not found in libs.versions.toml") if version.nil? + version +end - def current_version_number - File.read("../gradle/libs.versions.toml") - .match("revenuecat-kmp = \"(.*)\"") - .captures[0] - end +def get_ios_version + version = File.read("../upstream/purchases-ios/.version").strip + UI.user_error!("iOS version not found in upstream/purchases-ios/.version") if version.nil? || version.empty? + version +end - def check_no_git_tag_exists(version_number) - if git_tag_exists(tag: version_number, remote: true, remote_name: 'origin') - raise "git tag with version #{version_number} already exists!" - end +def get_billing_client_version(android_version) + require 'base64' + response = github_api( + server_url: 'https://api.github.com', + api_token: ENV["GITHUB_TOKEN"], + http_method: 'GET', + path: "/repos/RevenueCat/purchases-android/contents/gradle/libs.versions.toml?ref=#{android_version}" + ) + contents = Base64.decode64(response[:json]['content']) + matches = contents.match('bc8 = "(.*)"') + UI.user_error!("Could not find Play Billing Library version for purchases-android #{android_version}") unless matches + matches.captures[0] +end + +def update_versions_file(sdk_version) + android_version = get_android_version + ios_version = get_ios_version + billing_version = get_billing_client_version(android_version) + + UI.message("Android version: #{android_version}") + UI.message("iOS version: #{ios_version}") + UI.message("Play Billing Library version: #{billing_version}") + + versions_path = File.join('..', PATH_VERSIONS) + lines = File.readlines(versions_path) + new_line = [ + sdk_version, + "[#{ios_version}](https://github.com/RevenueCat/purchases-ios/releases/tag/#{ios_version})", + "[#{android_version}](https://github.com/RevenueCat/purchases-android/releases/tag/#{android_version})", + "N/A", + "[#{billing_version}](https://developer.android.com/google/play/billing/release-notes)" + ].join(' | ') + lines.insert(2, "| #{new_line} |\n") + File.write(versions_path, lines.join) +end + +def check_no_git_tag_exists(version_number) + if git_tag_exists(tag: version_number, remote: true, remote_name: 'origin') + raise "git tag with version #{version_number} already exists!" end +end # Publishes the SDK. If the current version is a SNAPSHOT version, it will be published # to Sonatype's snapshots repository, otherwise it will be published to Maven Central. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f26c73b48..1892aa0f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,16 +3,16 @@ agp = "8.13.2" android-compileSdk = "35" android-minSdk = "21" android-targetSdk = "35" -compose = "1.8.0" +compose = "1.9.3" detekt = "1.23.6" dokkatoo = "2.3.1" ios-deploymentTarget-core = "13.0" ios-deploymentTarget-ui = "15.0" java = "1.8" junit = "5.10.0" -kotlin = "2.1.21" +kotlin = "2.3.20" mavenPublish = "0.33.0" -revenuecat-common = "17.55.1" +revenuecat-android = "9.26.1" revenuecat-kmp = "2.11.0-SNAPSHOT" [libraries] @@ -28,14 +28,14 @@ arrow-core = { module = "io.arrow-kt:arrow-core", version = "1.2.4" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-core = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit-platformLauncher = { module = "org.junit.platform:junit-platform-launcher" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-test-assertions = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } kotlin-test-annotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } kotlin-test-core = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } -kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" } -revenuecat-common = { module = "com.revenuecat.purchases:purchases-hybrid-common", version.ref = "revenuecat-common" } -revenuecat-commonUi = { module = "com.revenuecat.purchases:purchases-hybrid-common-ui", version.ref = "revenuecat-common" } +revenuecat-android = { module = "com.revenuecat.purchases:purchases", version.ref = "revenuecat-android" } +revenuecatUi-android = { module = "com.revenuecat.purchases:purchases-ui", version.ref = "revenuecat-android" } [plugins] adamko-dokkatoo-html = { id = "dev.adamko.dokkatoo-html", version.ref = "dokkatoo" } @@ -46,7 +46,6 @@ codingfeline-buildkonfig = { id = "com.codingfeline.buildkonfig", version = "0.1 iurysouza-modulegraph = { id = "dev.iurysouza.modulegraph", version = "0.10.0" } jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlin-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinx-binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.17.0" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aaaabb3cb..c61a118f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/iosApp/Podfile b/iosApp/Podfile deleted file mode 100644 index f73aa0f08..000000000 --- a/iosApp/Podfile +++ /dev/null @@ -1,11 +0,0 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -target 'iosApp' do - # Comment the next line if you don't want to use dynamic frameworks - # use_frameworks! - - # Pods for iosApp - pod 'PurchasesHybridCommon', '17.55.1' - pod 'PurchasesHybridCommonUI', '17.55.1' -end diff --git a/iosApp/Podfile.lock b/iosApp/Podfile.lock deleted file mode 100644 index 4fc5d655d..000000000 --- a/iosApp/Podfile.lock +++ /dev/null @@ -1,30 +0,0 @@ -PODS: - - PurchasesHybridCommon (17.51.1): - - RevenueCat (= 5.65.0) - - PurchasesHybridCommonUI (17.51.1): - - PurchasesHybridCommon (= 17.51.1) - - RevenueCatUI (= 5.65.0) - - RevenueCat (5.65.0) - - RevenueCatUI (5.65.0): - - RevenueCat (= 5.65.0) - -DEPENDENCIES: - - PurchasesHybridCommon (= 17.51.1) - - PurchasesHybridCommonUI (= 17.51.1) - -SPEC REPOS: - trunk: - - PurchasesHybridCommon - - PurchasesHybridCommonUI - - RevenueCat - - RevenueCatUI - -SPEC CHECKSUMS: - PurchasesHybridCommon: 204de8c7d0b182240ac708cf6a05881bd99ceca9 - PurchasesHybridCommonUI: a4cb430ad365d27258653ce1bafd4dd5b130d7e4 - RevenueCat: 9a37739bdf42af84dd87e023db340de7e0f9d874 - RevenueCatUI: 1a503c1713ab8f598a0eb55e9954de7e3616aa5b - -PODFILE CHECKSUM: 0d2197b4fbe127ac69e58d0586b1916aa660770a - -COCOAPODS: 1.16.2 diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 1384f7e39..72c82946b 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; - 0E12A6F5E3C5A368A45100AA /* libPods-iosApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7970694DAED6D108F4AB7D20 /* libPods-iosApp.a */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; FD277EB72D2C6C5B00E392B7 /* SKConfig.storekit in Resources */ = {isa = PBXBuildFile; fileRef = FD277EB62D2C6C5B00E392B7 /* SKConfig.storekit */; }; @@ -22,10 +21,7 @@ 7555FF7B242A565900829871 /* purchases-kmp Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "purchases-kmp Sample.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7970694DAED6D108F4AB7D20 /* libPods-iosApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iosApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - ADB7642423335C54609E9121 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; - E4E97109987A6338537554BE /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; FD277EB62D2C6C5B00E392B7 /* SKConfig.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SKConfig.storekit; sourceTree = ""; }; /* End PBXFileReference section */ @@ -34,7 +30,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0E12A6F5E3C5A368A45100AA /* libPods-iosApp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -49,31 +44,12 @@ path = "Preview Content"; sourceTree = ""; }; - 38C5C97CCB7C1B4002B5B2EE /* Pods */ = { - isa = PBXGroup; - children = ( - ADB7642423335C54609E9121 /* Pods-iosApp.debug.xcconfig */, - E4E97109987A6338537554BE /* Pods-iosApp.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 42799AB246E5F90AF97AA0EF /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7970694DAED6D108F4AB7D20 /* libPods-iosApp.a */, - ); - name = Frameworks; - sourceTree = ""; - }; 7555FF72242A565900829871 = { isa = PBXGroup; children = ( AB1DB47929225F7C00F7AF9C /* Configuration */, 7555FF7D242A565900829871 /* iosApp */, 7555FF7C242A565900829871 /* Products */, - 42799AB246E5F90AF97AA0EF /* Frameworks */, - 38C5C97CCB7C1B4002B5B2EE /* Pods */, ); sourceTree = ""; }; @@ -113,12 +89,10 @@ isa = PBXNativeTarget; buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; buildPhases = ( - E546C444910C3630A98ADF58 /* [CP] Check Pods Manifest.lock */, F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, 7555FF77242A565900829871 /* Sources */, B92378962B6B1156000C7307 /* Frameworks */, 7555FF79242A565900829871 /* Resources */, - BADF33751A0CFC72A3F9017A /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -176,45 +150,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - BADF33751A0CFC72A3F9017A /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - E546C444910C3630A98ADF58 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -368,7 +303,6 @@ }; 7555FFA6242A565B00829871 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = ADB7642423335C54609E9121 /* Pods-iosApp.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; @@ -401,7 +335,6 @@ }; 7555FFA7242A565B00829871 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E4E97109987A6338537554BE /* Pods-iosApp.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme index 226434528..645b6baf2 100644 --- a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme +++ b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme @@ -1,11 +1,9 @@ + buildImplicitDependencies = "YES"> - - + + - - - - diff --git a/iosApp/iosApp.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 4b6f2d363..000000000 --- a/iosApp/iosApp.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/kn-core/.gitignore b/kn-core/.gitignore new file mode 100644 index 000000000..d74241ef4 --- /dev/null +++ b/kn-core/.gitignore @@ -0,0 +1,2 @@ +/build +.build diff --git a/kn-core/api/kn-core.api b/kn-core/api/kn-core.api new file mode 100644 index 000000000..e69de29bb diff --git a/kn-core/api/kn-core.klib.api b/kn-core/api/kn-core.klib.api new file mode 100644 index 000000000..1ec12c359 --- /dev/null +++ b/kn-core/api/kn-core.klib.api @@ -0,0 +1,8 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: diff --git a/kn-core/build.gradle.kts b/kn-core/build.gradle.kts new file mode 100644 index 000000000..c27f4c06a --- /dev/null +++ b/kn-core/build.gradle.kts @@ -0,0 +1,33 @@ +import com.revenuecat.purchases.kmp.buildlogic.swift.swiftPackage + +plugins { + id("revenuecat-library") +} + +kotlin { + sourceSets { + iosMain.dependencies { + swiftPackage( + path = rootProject.file("upstream/purchases-ios"), + target = "RevenueCat", + packageName = "swiftPMImport.com.revenuecat.purchases.kn.core", + customDeclarations = """ + // Force cinterop binding generation for types otherwise not in the public API + static inline int __forceBindings( + enum RCStoreMessageType _1 + ) { return 0; } + """.trimIndent() + ) + + swiftPackage( + path = file("src/swift"), + target = "AdditionalSwift", + packageName = "swiftPMImport.com.revenuecat.purchases.kn.core.additional" + ) + } + } +} + +android { + namespace = "com.revenuecat.purchases.kn.core" +} diff --git a/kn-core/src/iosMain/kotlin/com/revenuecat/purchases/kn/core/Dummy.kt b/kn-core/src/iosMain/kotlin/com/revenuecat/purchases/kn/core/Dummy.kt new file mode 100644 index 000000000..40990e7de --- /dev/null +++ b/kn-core/src/iosMain/kotlin/com/revenuecat/purchases/kn/core/Dummy.kt @@ -0,0 +1,8 @@ +@file:Suppress("unused") + +package com.revenuecat.purchases.kn.core + +/** + * We need at least 1 Kotlin source file for the kn-core module to be published. + */ +internal object Dummy diff --git a/kn-core/src/swift/Package.swift b/kn-core/src/swift/Package.swift new file mode 100644 index 000000000..63571d3de --- /dev/null +++ b/kn-core/src/swift/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "AdditionalSwift", + platforms: [ + .macOS(.v10_15), + .watchOS("6.2"), + .tvOS(.v13), + .iOS(.v13), + .visionOS(.v1) + ], + products: [ + .library(name: "AdditionalSwift", targets: ["AdditionalSwift"]) + ], + targets: [ + .target(name: "AdditionalSwift") + ] +) + diff --git a/kn-core/src/swift/Sources/AdditionalSwift/AppleApiAvailability.swift b/kn-core/src/swift/Sources/AdditionalSwift/AppleApiAvailability.swift new file mode 100644 index 000000000..8997c4651 --- /dev/null +++ b/kn-core/src/swift/Sources/AdditionalSwift/AppleApiAvailability.swift @@ -0,0 +1,75 @@ +import Foundation + +/// Provides runtime availability checks for Apple platform APIs. +@objc +public class AppleApiAvailability: NSObject { + /// Determines if the Win-Back Offer APIs are available on the current device. + /// + /// Note: This only checks if the APIs are available in the current OS version, + /// not if the SDK is using StoreKit 2, which is required for the APIs. + /// + /// - Returns: `true` if the Win-Back Offer APIs are available, `false` otherwise. + @objc + public func isWinBackOfferAPIAvailable() -> Bool { + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + return true + } else { + return false + } + } + + /// Determines if the enableAdServicesAttributionTokenCollection API is available on the current device. + /// + /// - Returns: `true` if the ``CommonFunctionality/enableAdServicesAttributionTokenCollection()`` API is available, + /// `false` otherwise. + @objc + public func isEnableAdServicesAttributionTokenCollectionAPIAvailable() -> Bool { + #if os(tvOS) || os(watchOS) + return false + #else + if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { + return true + } else { + return false + } + #endif + } + + /// Determines if the `presentCodeRedemptionSheet` API is available on the current device. + /// + /// - Returns: `true` if `presentCodeRedemptionSheet` is available, `false` otherwise. + @objc + public func isCodeRedemptionSheetAPIAvailable() -> Bool { + #if os(tvOS) || os(watchOS) || os(macOS) || targetEnvironment(macCatalyst) + return false + #else + if #available(iOS 14.0, visionOS 1.0, *) { + return true + } else { + return false + } + #endif + } + + @objc + public func isAdTrackingAPIAvailable() -> Bool { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + return true + } else { + return false + } + } + + /// Determines if the Custom Paywall Tracking APIs are available on the current device. + /// + /// - Returns: `true` if the Custom Paywall Tracking APIs (trackCustomPaywallImpression) are available, + /// `false` otherwise. + @objc + public func isCustomPaywallTrackingAPIAvailable() -> Bool { + if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { + return true + } else { + return false + } + } +} diff --git a/kn-ui/.gitignore b/kn-ui/.gitignore new file mode 100644 index 000000000..d74241ef4 --- /dev/null +++ b/kn-ui/.gitignore @@ -0,0 +1,2 @@ +/build +.build diff --git a/kn-ui/api/kn-ui.api b/kn-ui/api/kn-ui.api new file mode 100644 index 000000000..e69de29bb diff --git a/kn-ui/api/kn-ui.klib.api b/kn-ui/api/kn-ui.klib.api new file mode 100644 index 000000000..fc9165d7a --- /dev/null +++ b/kn-ui/api/kn-ui.klib.api @@ -0,0 +1,19 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final val swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_array$stableprop // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_array$stableprop|#static{}swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_array$stableprop[0] +final val swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_drawable$stableprop // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_drawable$stableprop|#static{}swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_drawable$stableprop[0] +final val swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_font$stableprop // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_font$stableprop|#static{}swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_font$stableprop[0] +final val swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_plurals$stableprop // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_plurals$stableprop|#static{}swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_plurals$stableprop[0] +final val swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_string$stableprop // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_string$stableprop|#static{}swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_string$stableprop[0] + +final fun swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_array$stableprop_getter(): kotlin/Int // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_array$stableprop_getter|swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_array$stableprop_getter(){}[0] +final fun swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_drawable$stableprop_getter(): kotlin/Int // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_drawable$stableprop_getter|swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_drawable$stableprop_getter(){}[0] +final fun swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_font$stableprop_getter(): kotlin/Int // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_font$stableprop_getter|swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_font$stableprop_getter(){}[0] +final fun swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_plurals$stableprop_getter(): kotlin/Int // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_plurals$stableprop_getter|swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_plurals$stableprop_getter(){}[0] +final fun swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_string$stableprop_getter(): kotlin/Int // swiftPMImport.com.revenuecat.purchases.kn.ui.resources/swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_string$stableprop_getter|swiftPMImport_com_revenuecat_purchases_kn_ui_resources_Res_string$stableprop_getter(){}[0] diff --git a/kn-ui/build.gradle.kts b/kn-ui/build.gradle.kts new file mode 100644 index 000000000..516f0861e --- /dev/null +++ b/kn-ui/build.gradle.kts @@ -0,0 +1,32 @@ +import com.revenuecat.purchases.kmp.buildlogic.swift.model.SwiftSettings +import com.revenuecat.purchases.kmp.buildlogic.swift.swiftPackage + +plugins { + id("revenuecat-library") + alias(libs.plugins.jetbrains.compose) + alias(libs.plugins.compose.compiler) +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + } + + iosMain.dependencies { + swiftPackage( + path = rootProject.file("upstream/purchases-ios"), + target = "RevenueCatUI", + packageName = "swiftPMImport.com.revenuecat.purchases.kn.ui", + swiftSettings = SwiftSettings { + define("COMPOSE_RESOURCES") + } + ) + } + } +} + +android { + namespace = "com.revenuecat.purchases.kn.ui" +} diff --git a/kn-ui/src/iosMain/kotlin/com/revenuecat/purchases/kn/ui/Dummy.kt b/kn-ui/src/iosMain/kotlin/com/revenuecat/purchases/kn/ui/Dummy.kt new file mode 100644 index 000000000..7040eeee0 --- /dev/null +++ b/kn-ui/src/iosMain/kotlin/com/revenuecat/purchases/kn/ui/Dummy.kt @@ -0,0 +1,8 @@ +@file:Suppress("unused") + +package com.revenuecat.purchases.kn.ui + +/** + * We need at least 1 Kotlin source file for the kn-ui module to be published. + */ +internal object Dummy diff --git a/mappings/build.gradle.kts b/mappings/build.gradle.kts index 979db9d1a..49e78612d 100644 --- a/mappings/build.gradle.kts +++ b/mappings/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("revenuecat-library") - alias(libs.plugins.kotlin.cocoapods) } kotlin { @@ -9,27 +8,15 @@ kotlin { api(projects.models) } androidMain.dependencies { - api(libs.revenuecat.common) + api(libs.revenuecat.android) } - commonTest.dependencies { - implementation(libs.kotlin.test.annotations) - implementation(libs.kotlin.test.assertions) + iosMain.dependencies { + implementation(projects.knCore) } androidUnitTest.dependencies { implementation(libs.kotlin.test.junit) } } - - cocoapods { - version = libs.versions.revenuecat.kmp.get() - ios.deploymentTarget = libs.versions.ios.deploymentTarget.core.get() - - pod("PurchasesHybridCommon") { - version = libs.versions.revenuecat.common.get() - extraOpts += listOf("-compiler-option", "-fmodules") - packageName = "swiftPMImport.com.revenuecat.purchases.kn.core" - } - } } android { diff --git a/mappings/mappings.podspec b/mappings/mappings.podspec deleted file mode 100644 index 24fca30e6..000000000 --- a/mappings/mappings.podspec +++ /dev/null @@ -1,54 +0,0 @@ -Pod::Spec.new do |spec| - spec.name = 'mappings' - spec.version = '2.11.0-SNAPSHOT' - spec.homepage = '' - spec.source = { :http=> ''} - spec.authors = '' - spec.license = '' - spec.summary = '' - spec.vendored_frameworks = 'build/cocoapods/framework/mappings.framework' - spec.libraries = 'c++' - spec.ios.deployment_target = '13.0' - spec.dependency 'PurchasesHybridCommon', '17.55.1' - - if !Dir.exist?('build/cocoapods/framework/mappings.framework') || Dir.empty?('build/cocoapods/framework/mappings.framework') - raise " - - Kotlin framework 'mappings' doesn't exist yet, so a proper Xcode project can't be generated. - 'pod install' should be executed after running ':generateDummyFramework' Gradle task: - - ./gradlew :mappings:generateDummyFramework - - Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" - end - - spec.xcconfig = { - 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO', - } - - spec.pod_target_xcconfig = { - 'KOTLIN_PROJECT_PATH' => ':mappings', - 'PRODUCT_MODULE_NAME' => 'mappings', - } - - spec.script_phases = [ - { - :name => 'Build mappings', - :execution_position => :before_compile, - :shell_path => '/bin/sh', - :script => <<-SCRIPT - if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then - echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" - exit 0 - fi - set -ev - REPO_ROOT="$PODS_TARGET_SRCROOT" - "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ - -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ - -Pkotlin.native.cocoapods.archs="$ARCHS" \ - -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" - SCRIPT - } - ] - -end \ No newline at end of file diff --git a/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/EntitlementVerificationMode.android.kt b/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/EntitlementVerificationMode.android.kt new file mode 100644 index 000000000..2bba0b70b --- /dev/null +++ b/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/EntitlementVerificationMode.android.kt @@ -0,0 +1,10 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.models.EntitlementVerificationMode +import com.revenuecat.purchases.EntitlementVerificationMode as AndroidEntitlementVerificationMode + +public fun EntitlementVerificationMode.toAndroidEntitlementVerificationMode(): AndroidEntitlementVerificationMode = + when (this) { + EntitlementVerificationMode.DISABLED -> AndroidEntitlementVerificationMode.DISABLED + EntitlementVerificationMode.INFORMATIONAL -> AndroidEntitlementVerificationMode.INFORMATIONAL + } diff --git a/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.android.kt b/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.android.kt new file mode 100644 index 000000000..3c22eb220 --- /dev/null +++ b/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.android.kt @@ -0,0 +1,10 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.models.PurchasesAreCompletedBy +import com.revenuecat.purchases.PurchasesAreCompletedBy as AndroidPurchasesAreCompletedBy + +public fun PurchasesAreCompletedBy.toAndroidPurchasesAreCompletedBy(): AndroidPurchasesAreCompletedBy = + when (this) { + is PurchasesAreCompletedBy.RevenueCat -> AndroidPurchasesAreCompletedBy.REVENUECAT + is PurchasesAreCompletedBy.MyApp -> AndroidPurchasesAreCompletedBy.MY_APP + } diff --git a/mappings/src/commonMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.kt b/mappings/src/commonMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.kt deleted file mode 100644 index ed2ae309f..000000000 --- a/mappings/src/commonMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.revenuecat.purchases.kmp.mappings - -import com.revenuecat.purchases.kmp.models.PurchasesAreCompletedBy - -/** - * Converts an instance of `PurchasesAreCompletedBy` to its corresponding string representation - * suitable for usage with the PurchasesHybridCommon library. - * - * @return A `String` that represents the type of `PurchasesAreCompletedBy`: - * - Returns `"REVENUECAT"` if the instance is of type `PurchasesAreCompletedBy.RevenueCat`. - * - Returns `"MY_APP"` if the instance is of type `PurchasesAreCompletedBy.MyApp`. - * - */ -public fun PurchasesAreCompletedBy.toHybridString(): String = - when(this) { - is PurchasesAreCompletedBy.RevenueCat -> "REVENUECAT" - is PurchasesAreCompletedBy.MyApp -> "MY_APP" - } diff --git a/mappings/src/commonTest/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedByTests.kt b/mappings/src/commonTest/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedByTests.kt deleted file mode 100644 index bd2a2ca93..000000000 --- a/mappings/src/commonTest/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedByTests.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.revenuecat.purchases.kmp.mappings - -import com.revenuecat.purchases.kmp.models.PurchasesAreCompletedBy -import com.revenuecat.purchases.kmp.models.StoreKitVersion -import kotlin.test.Test -import kotlin.test.assertEquals - -class PurchasesAreCompletedByTests { - - @Test - fun `toHybridString returns the correct values`() { - val purchasesAreCompletedByMyApp = PurchasesAreCompletedBy.MyApp(StoreKitVersion.DEFAULT) - val myAppHybridString = purchasesAreCompletedByMyApp.toHybridString() - - val purchasesAreCompletedByRevenueCat = PurchasesAreCompletedBy.RevenueCat - val revenuecatHybridString = purchasesAreCompletedByRevenueCat.toHybridString() - - assertEquals("MY_APP", myAppHybridString) - assertEquals("REVENUECAT", revenuecatHybridString) - } -} diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/AdEventTypes.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/AdEventTypes.ios.kt new file mode 100644 index 000000000..f957f7f28 --- /dev/null +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/AdEventTypes.ios.kt @@ -0,0 +1,94 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.ExperimentalRevenueCatApi +import com.revenuecat.purchases.kmp.models.AdDisplayedData +import com.revenuecat.purchases.kmp.models.AdFailedToLoadData +import com.revenuecat.purchases.kmp.models.AdFormat +import com.revenuecat.purchases.kmp.models.AdLoadedData +import com.revenuecat.purchases.kmp.models.AdMediatorName +import com.revenuecat.purchases.kmp.models.AdOpenedData +import com.revenuecat.purchases.kmp.models.AdRevenueData +import com.revenuecat.purchases.kmp.models.AdRevenuePrecision +import platform.Foundation.NSNumber +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdDisplayed +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdFailedToLoad +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdFormat +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdLoaded +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdOpened +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdRevenue +import swiftPMImport.com.revenuecat.purchases.kn.core.RCAdRevenuePrecision +import swiftPMImport.com.revenuecat.purchases.kn.core.RCMediatorName + +@ExperimentalRevenueCatApi +public fun AdMediatorName.toIos(): RCMediatorName = + RCMediatorName(rawValue = value) + +@ExperimentalRevenueCatApi +public fun AdRevenuePrecision.toIos(): RCAdRevenuePrecision = + RCAdRevenuePrecision(rawValue = value) + +@ExperimentalRevenueCatApi +public fun AdFormat.toIos(): RCAdFormat = + RCAdFormat(rawValue = value) + +@ExperimentalRevenueCatApi +public fun AdDisplayedData.toIos(): RCAdDisplayed { + return RCAdDisplayed( + networkName = networkName, + mediatorName = mediatorName.toIos(), + adFormat = adFormat.toIos(), + placement = placement, + adUnitId = adUnitId, + impressionId = impressionId, + ) +} + +@ExperimentalRevenueCatApi +public fun AdOpenedData.toIos(): RCAdOpened { + return RCAdOpened( + networkName = networkName, + mediatorName = mediatorName.toIos(), + adFormat = adFormat.toIos(), + placement = placement, + adUnitId = adUnitId, + impressionId = impressionId, + ) +} + +@ExperimentalRevenueCatApi +public fun AdRevenueData.toIos(): RCAdRevenue { + return RCAdRevenue( + networkName = networkName, + mediatorName = mediatorName.toIos(), + adFormat = adFormat.toIos(), + placement = placement, + adUnitId = adUnitId, + impressionId = impressionId, + revenueMicros = revenueMicros, + currency = currency, + precision = precision.toIos(), + ) +} + +@ExperimentalRevenueCatApi +public fun AdLoadedData.toIos(): RCAdLoaded { + return RCAdLoaded( + networkName = networkName, + mediatorName = mediatorName.toIos(), + adFormat = adFormat.toIos(), + placement = placement, + adUnitId = adUnitId, + impressionId = impressionId, + ) +} + +@ExperimentalRevenueCatApi +public fun AdFailedToLoadData.toIos(): RCAdFailedToLoad { + return RCAdFailedToLoad( + mediatorName = mediatorName.toIos(), + adFormat = adFormat.toIos(), + placement = placement, + adUnitId = adUnitId, + mediatorErrorCode = mediatorErrorCode?.let { NSNumber(it) }, + ) +} diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/EntitlementVerificationMode.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/EntitlementVerificationMode.ios.kt new file mode 100644 index 000000000..a00e4a398 --- /dev/null +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/EntitlementVerificationMode.ios.kt @@ -0,0 +1,12 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.models.EntitlementVerificationMode +import swiftPMImport.com.revenuecat.purchases.kn.core.RCEntitlementVerificationMode +import swiftPMImport.com.revenuecat.purchases.kn.core.RCEntitlementVerificationModeDisabled +import swiftPMImport.com.revenuecat.purchases.kn.core.RCEntitlementVerificationModeInformational + +public fun EntitlementVerificationMode.toIosEntitlementVerificationMode(): RCEntitlementVerificationMode = + when (this) { + EntitlementVerificationMode.DISABLED -> RCEntitlementVerificationModeDisabled + EntitlementVerificationMode.INFORMATIONAL -> RCEntitlementVerificationModeInformational + } diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/Price.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/Price.ios.kt index 8308a2148..7c4f9b1b2 100644 --- a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/Price.ios.kt +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/Price.ios.kt @@ -4,12 +4,12 @@ import com.revenuecat.purchases.kmp.models.Price import platform.Foundation.NSDecimalNumber import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreProduct import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreProductDiscount -import swiftPMImport.com.revenuecat.purchases.kn.core.priceAmount +import swiftPMImport.com.revenuecat.purchases.kn.core.price internal fun RCStoreProduct.toPrice(): Price = Price( formatted = localizedPriceString(), - amountMicros = priceAmount().decimalNumberByMultiplyingByPowerOf10(6).longValue, + amountMicros = price().decimalNumberByMultiplyingByPowerOf10(6).longValue, currencyCode = currencyCodeOrUsd(), ) @@ -19,7 +19,7 @@ internal fun RCStoreProduct.currencyCodeOrUsd(): String = internal fun RCStoreProductDiscount.toPrice(): Price = Price( formatted = localizedPriceString(), - amountMicros = priceAmount().decimalNumberByMultiplyingByPowerOf10(6).longValue, + amountMicros = price().decimalNumberByMultiplyingByPowerOf10(6).longValue, currencyCode = currencyCodeOrUsd(), ) diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.ios.kt new file mode 100644 index 000000000..3a02bba69 --- /dev/null +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/PurchasesAreCompletedBy.ios.kt @@ -0,0 +1,28 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.models.PurchasesAreCompletedBy +import com.revenuecat.purchases.kmp.models.StoreKitVersion +import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchasesAreCompletedBy +import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchasesAreCompletedByMyApp +import swiftPMImport.com.revenuecat.purchases.kn.core.RCPurchasesAreCompletedByRevenueCat +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreKitVersion +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreKitVersion1 +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreKitVersion2 + + +public fun PurchasesAreCompletedBy.toIosPurchasesAreCompletedBy(): IosPurchasesAreCompletedBy = + IosPurchasesAreCompletedBy( + purchasesAreCompletedBy = when (this) { + is PurchasesAreCompletedBy.RevenueCat -> RCPurchasesAreCompletedByRevenueCat + is PurchasesAreCompletedBy.MyApp -> RCPurchasesAreCompletedByMyApp + }, + storeKitVersion = when (this) { + is PurchasesAreCompletedBy.RevenueCat -> RCStoreKitVersion2 + is PurchasesAreCompletedBy.MyApp -> storeKitVersion.toIosStoreKitVersion() + } + ) + +public data class IosPurchasesAreCompletedBy( + val purchasesAreCompletedBy: RCPurchasesAreCompletedBy, + val storeKitVersion: RCStoreKitVersion +) diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreKitVersion.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreKitVersion.ios.kt index 3e008bf72..074685460 100644 --- a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreKitVersion.ios.kt +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreKitVersion.ios.kt @@ -1,10 +1,13 @@ package com.revenuecat.purchases.kmp.mappings import com.revenuecat.purchases.kmp.models.StoreKitVersion +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreKitVersion +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreKitVersion1 +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreKitVersion2 -public fun StoreKitVersion.toHybridString(): String = - when(this) { - StoreKitVersion.STOREKIT_1 -> "STOREKIT_1" - StoreKitVersion.STOREKIT_2 -> "STOREKIT_2" - StoreKitVersion.DEFAULT -> "DEFAULT" +public fun StoreKitVersion.toIosStoreKitVersion(): RCStoreKitVersion = + when (this) { + StoreKitVersion.STOREKIT_1 -> RCStoreKitVersion1 + StoreKitVersion.STOREKIT_2 -> RCStoreKitVersion2 + StoreKitVersion.DEFAULT -> RCStoreKitVersion2 } diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreMessageType.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreMessageType.ios.kt new file mode 100644 index 000000000..196ea3557 --- /dev/null +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreMessageType.ios.kt @@ -0,0 +1,19 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.models.StoreMessageType +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreMessageType +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreMessageTypeBillingIssue +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreMessageTypeGeneric +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreMessageTypePriceIncreaseConsent +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreMessageTypeWinBackOffer + +public fun Collection.toIosStoreMessageTypes(): Set = + mapTo(mutableSetOf()) { it.toIosStoreMessageType() } + +public fun StoreMessageType.toIosStoreMessageType(): RCStoreMessageType = + when (this) { + StoreMessageType.GENERIC -> RCStoreMessageTypeGeneric + StoreMessageType.BILLING_ISSUES -> RCStoreMessageTypeBillingIssue + StoreMessageType.PRICE_INCREASE_CONSENT -> RCStoreMessageTypePriceIncreaseConsent + StoreMessageType.WIN_BACK_OFFER -> RCStoreMessageTypeWinBackOffer + } diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreProduct.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreProduct.ios.kt index 8d6d95c24..3ecaa5d03 100644 --- a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreProduct.ios.kt +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreProduct.ios.kt @@ -10,12 +10,12 @@ import com.revenuecat.purchases.kmp.models.StoreProduct import com.revenuecat.purchases.kmp.models.StoreProductDiscount import com.revenuecat.purchases.kmp.models.SubscriptionOption import com.revenuecat.purchases.kmp.models.SubscriptionOptions -import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerMonthAmount -import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerMonthString -import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerWeekAmount -import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerWeekString -import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerYearAmount -import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerYearString +import swiftPMImport.com.revenuecat.purchases.kn.core.localizedPricePerMonth +import swiftPMImport.com.revenuecat.purchases.kn.core.localizedPricePerWeek +import swiftPMImport.com.revenuecat.purchases.kn.core.localizedPricePerYear +import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerMonth +import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerWeek +import swiftPMImport.com.revenuecat.purchases.kn.core.pricePerYear import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreProduct as NativeIosStoreProduct import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreProductDiscount as IosStoreProductDiscount @@ -46,22 +46,22 @@ private class IosStoreProduct(val wrapped: NativeIosStoreProduct): StoreProduct override val pricePerWeek: Price? by lazy { priceOrNull( currencyCode = price.currencyCode, - formatted = wrapped.pricePerWeekString(), - amountDecimal = wrapped.pricePerWeekAmount(), + formatted = wrapped.localizedPricePerWeek(), + amountDecimal = wrapped.pricePerWeek(), ) } override val pricePerMonth: Price? by lazy { priceOrNull( currencyCode = price.currencyCode, - formatted = wrapped.pricePerMonthString(), - amountDecimal = wrapped.pricePerMonthAmount(), + formatted = wrapped.localizedPricePerMonth(), + amountDecimal = wrapped.pricePerMonth(), ) } override val pricePerYear: Price? by lazy { priceOrNull( currencyCode = price.currencyCode, - formatted = wrapped.pricePerYearString(), - amountDecimal = wrapped.pricePerYearAmount(), + formatted = wrapped.localizedPricePerYear(), + amountDecimal = wrapped.pricePerYear(), ) } } diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreTransaction.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreTransaction.ios.kt index 29b3a66a9..13345e7d0 100644 --- a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreTransaction.ios.kt +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/StoreTransaction.ios.kt @@ -10,29 +10,3 @@ public fun IosStoreTransaction.toStoreTransaction(): StoreTransaction = productIds = listOf(productIdentifier()), purchaseTime = purchaseDate().toEpochMilliseconds(), ) - -public fun buildStoreTransaction(storeTransactionMap: Map): Result { - val transactionId = storeTransactionMap["transactionIdentifier"] as? String - val productId = storeTransactionMap["productIdentifier"] as? String - val purchaseTime = (storeTransactionMap["purchaseDateMillis"] as? Number)?.toLong() - - if(transactionId == null) { - return Result.failure(IllegalArgumentException("Expected a non-null transactionIdentifier")) - } - - if(productId == null) { - return Result.failure(IllegalArgumentException("Expected a non-null productIdentifier")) - } - - if(purchaseTime == null) { - return Result.failure(IllegalArgumentException("Expected a non-null purchaseDateMillis")) - } - - return Result.success( - StoreTransaction( - transactionId = transactionId, - productIds = listOf(productId), - purchaseTime = purchaseTime - ) - ) -} diff --git a/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/StoreKitVersionTests.ios.kt b/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/StoreKitVersionTests.ios.kt deleted file mode 100644 index bbaccc162..000000000 --- a/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/StoreKitVersionTests.ios.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.revenuecat.purchases.kmp.mappings - -import com.revenuecat.purchases.kmp.models.StoreKitVersion -import kotlin.test.Test -import kotlin.test.assertEquals - -class StoreKitVersionTests { - - @Test - fun `toHybridString returns the correct values`() { - val storeKit1 = StoreKitVersion.STOREKIT_1 - val storeKit1String = storeKit1.toHybridString() - - val storeKit2 = StoreKitVersion.STOREKIT_2 - val storeKit2String = storeKit2.toHybridString() - - val storeKitDefault = StoreKitVersion.DEFAULT - val storeKitDefaultString = storeKitDefault.toHybridString() - - assertEquals("STOREKIT_1", storeKit1String) - assertEquals("STOREKIT_2", storeKit2String) - assertEquals("DEFAULT", storeKitDefaultString) - } -} diff --git a/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/StoreTransactionTests.ios.kt b/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/StoreTransactionTests.ios.kt deleted file mode 100644 index 28921b7ba..000000000 --- a/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/StoreTransactionTests.ios.kt +++ /dev/null @@ -1,123 +0,0 @@ -package com.revenuecat.purchases.kmp.mappings - - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class StoreTransactionTests { - - @Test - fun `fromMap correctly parses map with all fields`() { - val transactionId = "someTransactionIdentifier" - val productId = "someProductIdentifier" - val purchaseDateMillis = 1627689600000.0 - val purchaseDate = "2021-07-31T00:00:00Z" - - val hybridMap: Map = mapOf( - "transactionIdentifier" to transactionId, - "revenueCatId" to transactionId, // Deprecated - "productIdentifier" to productId, - "productId" to productId, // Deprecated - "purchaseDateMillis" to purchaseDateMillis, - "purchaseDate" to purchaseDate - ) - - val result = buildStoreTransaction(storeTransactionMap = hybridMap) - - assertTrue(result.isSuccess) - val storeTransaction = result.getOrThrow() - assertEquals(transactionId, storeTransaction.transactionId) - assertEquals(listOf(productId), storeTransaction.productIds) - assertEquals(purchaseDateMillis.toLong(), storeTransaction.purchaseTime) - } - - @Test - fun `fromMap correctly parses map with all fields if purchaseDateMillis is an int`() { - val transactionId = "someTransactionIdentifier" - val productId = "someProductIdentifier" - val purchaseDateMillis = 1627689600000 - val purchaseDate = "2021-07-31T00:00:00Z" - - val hybridMap: Map = mapOf( - "transactionIdentifier" to transactionId, - "revenueCatId" to transactionId, // Deprecated - "productIdentifier" to productId, - "productId" to productId, // Deprecated - "purchaseDateMillis" to purchaseDateMillis, - "purchaseDate" to purchaseDate - ) - - val result = buildStoreTransaction(storeTransactionMap = hybridMap) - - assertTrue(result.isSuccess) - val storeTransaction = result.getOrThrow() - assertEquals(transactionId, storeTransaction.transactionId) - assertEquals(listOf(productId), storeTransaction.productIds) - assertEquals(purchaseDateMillis, storeTransaction.purchaseTime) - } - - @Test - fun `missing transactionIdentifier returns failure`() { - val productId = "someProductIdentifier" - val purchaseDateMillis = 1627689600000.0 - val purchaseDate = "2021-07-31T00:00:00Z" - - val hybridMap: Map = mapOf( - "productIdentifier" to productId, - "purchaseDateMillis" to purchaseDateMillis, - "purchaseDate" to purchaseDate - ) - - val result = buildStoreTransaction(storeTransactionMap = hybridMap) - - assertTrue(result.isFailure) - result.exceptionOrNull()?.let { - assertTrue(it is IllegalArgumentException) - assertEquals("Expected a non-null transactionIdentifier", it.message) - } - } - - @Test - fun `missing productIdentifier returns failure`() { - val transactionId = "someTransactionIdentifier" - val purchaseDateMillis = 1627689600000.0 - val purchaseDate = "2021-07-31T00:00:00Z" - - val hybridMap: Map = mapOf( - "transactionIdentifier" to transactionId, - "purchaseDateMillis" to purchaseDateMillis, - "purchaseDate" to purchaseDate - ) - - val result = buildStoreTransaction(storeTransactionMap = hybridMap) - - assertTrue(result.isFailure) - result.exceptionOrNull()?.let { - assertTrue(it is IllegalArgumentException) - assertEquals("Expected a non-null productIdentifier", it.message) - } - } - - @Test - fun `missing purchaseDateMillis returns failure`() { - val transactionId = "someTransactionIdentifier" - val productId = "someProductIdentifier" - val purchaseDate = "2021-07-31T00:00:00Z" - - val hybridMap: Map = mapOf( - "transactionIdentifier" to transactionId, - "productIdentifier" to productId, - "purchaseDateMillis" to "InvalidMillis", // This should be a Double - "purchaseDate" to purchaseDate - ) - - val result = buildStoreTransaction(storeTransactionMap = hybridMap) - - assertTrue(result.isFailure) - result.exceptionOrNull()?.let { - assertTrue(it is IllegalArgumentException) - assertEquals("Expected a non-null purchaseDateMillis", it.message) - } - } -} diff --git a/migrations/3.0.0-MIGRATION.md b/migrations/3.0.0-MIGRATION.md new file mode 100644 index 000000000..b3474e9a9 --- /dev/null +++ b/migrations/3.0.0-MIGRATION.md @@ -0,0 +1,36 @@ +# 3.0.0 Migration Guide + +This version makes integrating the SDK on iOS much easier. + +## Remove the native iOS SDK +Coming from a version before `3.0.0`, your iOS project will have a dependency on RevenueCat's `PurchasesHybridCommon` and/or `PurchasesHybridCommonUI` native iOS dependencies. These should be removed. The instructions depend on how you added them. + +### Remove from Swift Package Manager +If you've added `PurchasesHybridCommon` and/or `PurchasesHybridCommonUI` to your iOS project using Swift Package Manager, do the following: +1. Open Xcode and select your project in the Project navigator. +2. Click Package Dependencies. +3. Select the `PurchasesHybridCommon` and/or `PurchasesHybridCommonUI` dependencies and click the '-' (minus) button to remove them. + +### Remove from CocoaPods +If you've added `PurchasesHybridCommon` and/or `PurchasesHybridCommonUI` to your iOS project using CocoaPods, there are 2 possible places where these dependencies could be defined. Make sure they are removed from both. + +Your `Podfile` might contain lines like the following. Remove them: +```ruby +pod 'PurchasesHybridCommon', '17.21.2' +pod 'PurchasesHybridCommonUI', '17.21.2' +``` + +Your `build.gradle[.kts]` might contain sections like the following. Remove these: +```kotlin +pod("PurchasesHybridCommon") { + version = "17.21.2" + extraOpts += listOf("-compiler-option", "-fmodules") +} +pod("PurchasesHybridCommonUI") { + version = "17.21.2" + extraOpts += listOf("-compiler-option", "-fmodules") +} +``` + +## Reporting undocumented issues: +Feel free to file an issue! [New RevenueCat Issue](https://github.com/RevenueCat/purchases-kmp/issues/new/). Make sure to state clearly that you're using version 3.0.0. Thanks! diff --git a/models/api/models.api b/models/api/models.api index 279cab44f..3973069f1 100644 --- a/models/api/models.api +++ b/models/api/models.api @@ -256,15 +256,15 @@ public final class com/revenuecat/purchases/kmp/models/OfferPaymentMode : java/l } public abstract interface class com/revenuecat/purchases/kmp/models/Offering { - public abstract fun get (Ljava/lang/String;)Lcom/revenuecat/purchases/kmp/models/Package; + public fun get (Ljava/lang/String;)Lcom/revenuecat/purchases/kmp/models/Package; public abstract fun getAnnual ()Lcom/revenuecat/purchases/kmp/models/Package; public abstract fun getAvailablePackages ()Ljava/util/List; public abstract fun getIdentifier ()Ljava/lang/String; public abstract fun getLifetime ()Lcom/revenuecat/purchases/kmp/models/Package; public abstract fun getMetadata ()Ljava/util/Map; - public abstract fun getMetadataString (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public fun getMetadataString (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public abstract fun getMonthly ()Lcom/revenuecat/purchases/kmp/models/Package; - public abstract fun getPackage (Ljava/lang/String;)Lcom/revenuecat/purchases/kmp/models/Package; + public fun getPackage (Ljava/lang/String;)Lcom/revenuecat/purchases/kmp/models/Package; public abstract fun getServerDescription ()Ljava/lang/String; public abstract fun getSixMonth ()Lcom/revenuecat/purchases/kmp/models/Package; public abstract fun getThreeMonth ()Lcom/revenuecat/purchases/kmp/models/Package; diff --git a/models/build.gradle.kts b/models/build.gradle.kts index 646ca7f81..1a5e92e34 100644 --- a/models/build.gradle.kts +++ b/models/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("revenuecat-library") - alias(libs.plugins.kotlin.cocoapods) } revenueCat { @@ -10,7 +9,7 @@ revenueCat { kotlin { sourceSets { androidMain.dependencies { - implementation(libs.revenuecat.common) + implementation(libs.revenuecat.android) } commonTest.dependencies { implementation(libs.kotlin.test.annotations) @@ -20,17 +19,6 @@ kotlin { implementation(libs.kotlin.test.junit) } } - - cocoapods { - version = libs.versions.revenuecat.kmp.get() - ios.deploymentTarget = libs.versions.ios.deploymentTarget.core.get() - - pod("PurchasesHybridCommon") { - version = libs.versions.revenuecat.common.get() - extraOpts += listOf("-compiler-option", "-fmodules") - packageName = "swiftPMImport.com.revenuecat.purchases.kn.core" - } - } } android { diff --git a/models/models.podspec b/models/models.podspec deleted file mode 100644 index dd32a692f..000000000 --- a/models/models.podspec +++ /dev/null @@ -1,54 +0,0 @@ -Pod::Spec.new do |spec| - spec.name = 'models' - spec.version = '2.11.0-SNAPSHOT' - spec.homepage = '' - spec.source = { :http=> ''} - spec.authors = '' - spec.license = '' - spec.summary = '' - spec.vendored_frameworks = 'build/cocoapods/framework/models.framework' - spec.libraries = 'c++' - spec.ios.deployment_target = '13.0' - spec.dependency 'PurchasesHybridCommon', '17.55.1' - - if !Dir.exist?('build/cocoapods/framework/models.framework') || Dir.empty?('build/cocoapods/framework/models.framework') - raise " - - Kotlin framework 'models' doesn't exist yet, so a proper Xcode project can't be generated. - 'pod install' should be executed after running ':generateDummyFramework' Gradle task: - - ./gradlew :models:generateDummyFramework - - Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" - end - - spec.xcconfig = { - 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO', - } - - spec.pod_target_xcconfig = { - 'KOTLIN_PROJECT_PATH' => ':models', - 'PRODUCT_MODULE_NAME' => 'models', - } - - spec.script_phases = [ - { - :name => 'Build models', - :execution_position => :before_compile, - :shell_path => '/bin/sh', - :script => <<-SCRIPT - if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then - echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" - exit 0 - fi - set -ev - REPO_ROOT="$PODS_TARGET_SRCROOT" - "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ - -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ - -Pkotlin.native.cocoapods.archs="$ARCHS" \ - -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" - SCRIPT - } - ] - -end \ No newline at end of file diff --git a/renovate.json b/renovate.json index 57eced641..65dffe51a 100644 --- a/renovate.json +++ b/renovate.json @@ -9,6 +9,15 @@ { "matchManagers": ["git-submodules"], "enabled": true + }, + { + "matchPackageNames": [ + "com.revenuecat.purchases:purchases", + "com.revenuecat.purchases:purchases-ui" + ], + "matchManagers": ["gradle"], + "enabled": true, + "groupName": "purchases-android" } ] } diff --git a/revenuecatui/api/revenuecatui.api b/revenuecatui/api/revenuecatui.api index 7d6850693..23a168e65 100644 --- a/revenuecatui/api/revenuecatui.api +++ b/revenuecatui/api/revenuecatui.api @@ -57,13 +57,13 @@ public final class com/revenuecat/purchases/kmp/ui/revenuecatui/PaywallKt { } public abstract interface class com/revenuecat/purchases/kmp/ui/revenuecatui/PaywallListener { - public abstract fun onPurchaseCancelled ()V - public abstract fun onPurchaseCompleted (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;Lcom/revenuecat/purchases/kmp/models/StoreTransaction;)V - public abstract fun onPurchaseError (Lcom/revenuecat/purchases/kmp/models/PurchasesError;)V - public abstract fun onPurchaseStarted (Lcom/revenuecat/purchases/kmp/models/Package;)V - public abstract fun onRestoreCompleted (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)V - public abstract fun onRestoreError (Lcom/revenuecat/purchases/kmp/models/PurchasesError;)V - public abstract fun onRestoreStarted ()V + public fun onPurchaseCancelled ()V + public fun onPurchaseCompleted (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;Lcom/revenuecat/purchases/kmp/models/StoreTransaction;)V + public fun onPurchaseError (Lcom/revenuecat/purchases/kmp/models/PurchasesError;)V + public fun onPurchaseStarted (Lcom/revenuecat/purchases/kmp/models/Package;)V + public fun onRestoreCompleted (Lcom/revenuecat/purchases/kmp/models/CustomerInfo;)V + public fun onRestoreError (Lcom/revenuecat/purchases/kmp/models/PurchasesError;)V + public fun onRestoreStarted ()V } public final class com/revenuecat/purchases/kmp/ui/revenuecatui/PaywallListener$DefaultImpls { diff --git a/revenuecatui/api/revenuecatui.klib.api b/revenuecatui/api/revenuecatui.klib.api index 520299475..19a4fddad 100644 --- a/revenuecatui/api/revenuecatui.klib.api +++ b/revenuecatui/api/revenuecatui.klib.api @@ -21,6 +21,51 @@ abstract interface com.revenuecat.purchases.kmp.ui.revenuecatui/PaywallPurchaseL abstract suspend fun performRestore(com.revenuecat.purchases.kmp.models/CustomerInfo): com.revenuecat.purchases.kmp.ui.revenuecatui/PurchaseLogicResult // com.revenuecat.purchases.kmp.ui.revenuecatui/PaywallPurchaseLogic.performRestore|performRestore(com.revenuecat.purchases.kmp.models.CustomerInfo){}[0] } +abstract class com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue|null[0] + final class Boolean : com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean|null[0] + constructor (kotlin/Boolean) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.|(kotlin.Boolean){}[0] + + final val value // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.value|{}value[0] + final fun (): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.value.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.toString|toString(){}[0] + } + + final class Number : com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number|null[0] + constructor (kotlin/Double) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Double){}[0] + constructor (kotlin/Float) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Float){}[0] + constructor (kotlin/Int) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Int){}[0] + constructor (kotlin/Long) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Long){}[0] + + final val value // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.value|{}value[0] + final fun (): kotlin/Double // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.value.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.toString|toString(){}[0] + } + + final class String : com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String|null[0] + constructor (kotlin/String) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.|(kotlin.String){}[0] + + final val value // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.value|{}value[0] + final fun (): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.value.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.toString|toString(){}[0] + } + + final object Companion { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion|null[0] + final fun boolean(kotlin/Boolean): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.boolean|boolean(kotlin.Boolean){}[0] + final fun number(kotlin/Double): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.number|number(kotlin.Double){}[0] + final fun number(kotlin/Int): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.number|number(kotlin.Int){}[0] + final fun string(kotlin/String): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.string|string(kotlin.String){}[0] + } +} + abstract class com.revenuecat.purchases.kmp.ui.revenuecatui/PurchaseLogicResult { // com.revenuecat.purchases.kmp.ui.revenuecatui/PurchaseLogicResult|null[0] final class Cancellation : com.revenuecat.purchases.kmp.ui.revenuecatui/PurchaseLogicResult { // com.revenuecat.purchases.kmp.ui.revenuecatui/PurchaseLogicResult.Cancellation|null[0] constructor () // com.revenuecat.purchases.kmp.ui.revenuecatui/PurchaseLogicResult.Cancellation.|(){}[0] @@ -85,59 +130,10 @@ final class com.revenuecat.purchases.kmp.ui.revenuecatui/PaywallPurchaseLogicPar final fun (): com.revenuecat.purchases.kmp.models/Package // com.revenuecat.purchases.kmp.ui.revenuecatui/PaywallPurchaseLogicParams.rcPackage.|(){}[0] } -abstract class com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue|null[0] - final class Boolean : com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean|null[0] - constructor (kotlin/Boolean) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.|(kotlin.Boolean){}[0] - - final val value // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.value|{}value[0] - final fun (): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.value.|(){}[0] - - final fun equals(kotlin/Any?): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.equals|equals(kotlin.Any?){}[0] - final fun hashCode(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.hashCode|hashCode(){}[0] - final fun toString(): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Boolean.toString|toString(){}[0] - } - - final class Number : com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number|null[0] - constructor (kotlin/Double) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Double){}[0] - constructor (kotlin/Float) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Float){}[0] - constructor (kotlin/Int) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Int){}[0] - constructor (kotlin/Long) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.|(kotlin.Long){}[0] - - final val value // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.value|{}value[0] - final fun (): kotlin/Double // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.value.|(){}[0] - - final fun equals(kotlin/Any?): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.equals|equals(kotlin.Any?){}[0] - final fun hashCode(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.hashCode|hashCode(){}[0] - final fun toString(): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Number.toString|toString(){}[0] - } - - final class String : com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String|null[0] - constructor (kotlin/String) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.|(kotlin.String){}[0] - - final val value // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.value|{}value[0] - final fun (): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.value.|(){}[0] - - final fun equals(kotlin/Any?): kotlin/Boolean // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.equals|equals(kotlin.Any?){}[0] - final fun hashCode(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.hashCode|hashCode(){}[0] - final fun toString(): kotlin/String // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.String.toString|toString(){}[0] - } - - final object Companion { // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion|null[0] - final fun boolean(kotlin/Boolean): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.boolean|boolean(kotlin.Boolean){}[0] - final fun number(kotlin/Double): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.number|number(kotlin.Double){}[0] - final fun number(kotlin/Int): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.number|number(kotlin.Int){}[0] - final fun string(kotlin/String): com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomVariableValue.Companion.string|string(kotlin.String){}[0] - } -} - -final val com.revenuecat.purchases.kmp.ui.revenuecatui.modifier/com_revenuecat_purchases_kmp_ui_revenuecatui_modifier_LayoutViewControllerState$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui.modifier/com_revenuecat_purchases_kmp_ui_revenuecatui_modifier_LayoutViewControllerState$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_modifier_LayoutViewControllerState$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Boolean$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Boolean$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Boolean$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Number$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Number$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Number$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_String$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_String$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_String$stableprop[0] -final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosCustomerCenterDelegate$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosCustomerCenterDelegate$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_IosCustomerCenterDelegate$stableprop[0] -final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallDelegate$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallDelegate$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallDelegate$stableprop[0] -final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallProxyDelegate$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallProxyDelegate$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallProxyDelegate$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions_Builder$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions_Builder$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions_Builder$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallPurchaseLogicParams$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallPurchaseLogicParams$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallPurchaseLogicParams$stableprop[0] @@ -146,7 +142,6 @@ final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_ final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PurchaseLogicResult_Error$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PurchaseLogicResult_Error$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_PurchaseLogicResult_Error$stableprop[0] final val com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PurchaseLogicResult_Success$stableprop // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PurchaseLogicResult_Success$stableprop|#static{}com_revenuecat_purchases_kmp_ui_revenuecatui_PurchaseLogicResult_Success$stableprop[0] -final fun com.revenuecat.purchases.kmp.ui.revenuecatui.modifier/com_revenuecat_purchases_kmp_ui_revenuecatui_modifier_LayoutViewControllerState$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui.modifier/com_revenuecat_purchases_kmp_ui_revenuecatui_modifier_LayoutViewControllerState$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_modifier_LayoutViewControllerState$stableprop_getter(){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/CustomerCenter(androidx.compose.ui/Modifier?, kotlin/Function0, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.revenuecat.purchases.kmp.ui.revenuecatui/CustomerCenter|CustomerCenter(androidx.compose.ui.Modifier?;kotlin.Function0;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/OriginalTemplatePaywallFooter(com.revenuecat.purchases.kmp.ui.revenuecatui/PaywallOptions, kotlin/Function3?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.revenuecat.purchases.kmp.ui.revenuecatui/OriginalTemplatePaywallFooter|OriginalTemplatePaywallFooter(com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallOptions;kotlin.Function3?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/Paywall(com.revenuecat.purchases.kmp.ui.revenuecatui/PaywallOptions, androidx.compose.runtime/Composer?, kotlin/Int) // com.revenuecat.purchases.kmp.ui.revenuecatui/Paywall|Paywall(com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallOptions;androidx.compose.runtime.Composer?;kotlin.Int){}[0] @@ -156,9 +151,6 @@ final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_ final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Boolean$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Boolean$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Boolean$stableprop_getter(){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Number$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Number$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_Number$stableprop_getter(){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_String$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_String$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_CustomVariableValue_String$stableprop_getter(){}[0] -final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosCustomerCenterDelegate$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosCustomerCenterDelegate$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_IosCustomerCenterDelegate$stableprop_getter(){}[0] -final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallDelegate$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallDelegate$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallDelegate$stableprop_getter(){}[0] -final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallProxyDelegate$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallProxyDelegate$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_IosPaywallProxyDelegate$stableprop_getter(){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions$stableprop_getter(){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions_Builder$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions_Builder$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallOptions_Builder$stableprop_getter(){}[0] final fun com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallPurchaseLogicParams$stableprop_getter(): kotlin/Int // com.revenuecat.purchases.kmp.ui.revenuecatui/com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallPurchaseLogicParams$stableprop_getter|com_revenuecat_purchases_kmp_ui_revenuecatui_PaywallPurchaseLogicParams$stableprop_getter(){}[0] diff --git a/revenuecatui/build.gradle.kts b/revenuecatui/build.gradle.kts index dfd6ffd50..6a2238bfd 100644 --- a/revenuecatui/build.gradle.kts +++ b/revenuecatui/build.gradle.kts @@ -2,7 +2,6 @@ plugins { id("revenuecat-library") alias(libs.plugins.jetbrains.compose) alias(libs.plugins.compose.compiler) - alias(libs.plugins.kotlin.cocoapods) } revenueCat { @@ -20,24 +19,15 @@ kotlin { implementation(compose.components.uiToolingPreview) } androidMain.dependencies { - implementation(libs.revenuecat.commonUi) + implementation(libs.revenuecatUi.android) implementation(projects.mappings) } iosMain.dependencies { + implementation(projects.knCore) + implementation(projects.knUi) implementation(projects.mappings) } } - - cocoapods { - version = libs.versions.revenuecat.kmp.get() - ios.deploymentTarget = libs.versions.ios.deploymentTarget.ui.get() - - pod("PurchasesHybridCommonUI") { - version = libs.versions.revenuecat.common.get() - extraOpts += listOf("-compiler-option", "-fmodules") - packageName = "swiftPMImport.com.revenuecat.purchases.kn.ui" - } - } } android { diff --git a/revenuecatui/revenuecatui.podspec b/revenuecatui/revenuecatui.podspec deleted file mode 100644 index 4cdf16d49..000000000 --- a/revenuecatui/revenuecatui.podspec +++ /dev/null @@ -1,54 +0,0 @@ -Pod::Spec.new do |spec| - spec.name = 'revenuecatui' - spec.version = '2.11.0-SNAPSHOT' - spec.homepage = '' - spec.source = { :http=> ''} - spec.authors = '' - spec.license = '' - spec.summary = '' - spec.vendored_frameworks = 'build/cocoapods/framework/revenuecatui.framework' - spec.libraries = 'c++' - spec.ios.deployment_target = '15.0' - spec.dependency 'PurchasesHybridCommonUI', '17.55.1' - - if !Dir.exist?('build/cocoapods/framework/revenuecatui.framework') || Dir.empty?('build/cocoapods/framework/revenuecatui.framework') - raise " - - Kotlin framework 'revenuecatui' doesn't exist yet, so a proper Xcode project can't be generated. - 'pod install' should be executed after running ':generateDummyFramework' Gradle task: - - ./gradlew :revenuecatui:generateDummyFramework - - Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" - end - - spec.xcconfig = { - 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO', - } - - spec.pod_target_xcconfig = { - 'KOTLIN_PROJECT_PATH' => ':revenuecatui', - 'PRODUCT_MODULE_NAME' => 'revenuecatui', - } - - spec.script_phases = [ - { - :name => 'Build revenuecatui', - :execution_position => :before_compile, - :shell_path => '/bin/sh', - :script => <<-SCRIPT - if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then - echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" - exit 0 - fi - set -ev - REPO_ROOT="$PODS_TARGET_SRCROOT" - "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ - -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ - -Pkotlin.native.cocoapods.archs="$ARCHS" \ - -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" - SCRIPT - } - ] - spec.resources = ['build/compose/cocoapods/compose-resources'] -end \ No newline at end of file diff --git a/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/PaywallOptionsKtx.kt b/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/PaywallOptionsKtx.kt index 9cb9804f9..850c6354a 100644 --- a/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/PaywallOptionsKtx.kt +++ b/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/PaywallOptionsKtx.kt @@ -11,14 +11,14 @@ import kotlinx.cinterop.pointed import platform.CoreGraphics.CGSize import platform.Foundation.NSError import platform.darwin.NSObject -import swiftPMImport.com.revenuecat.purchases.kn.ui.RCCustomerInfo -import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPackage +import swiftPMImport.com.revenuecat.purchases.kn.core.RCCustomerInfo +import swiftPMImport.com.revenuecat.purchases.kn.core.RCPackage +import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreTransaction import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPaywallViewController import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPaywallViewControllerDelegateProtocol -import swiftPMImport.com.revenuecat.purchases.kn.ui.RCStoreTransaction -import swiftPMImport.com.revenuecat.purchases.kn.core.RCCustomerInfo as PhcCustomerInfo -import swiftPMImport.com.revenuecat.purchases.kn.core.RCPackage as PhcPackage -import swiftPMImport.com.revenuecat.purchases.kn.core.RCStoreTransaction as PhcStoreTransaction +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCCustomerInfo as RCCustomerInfoFromKnUi +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPackage as RCPackageFromKnUi +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCStoreTransaction as RCStoreTransactionFromKnUi internal class IosPaywallDelegate( private val listener: PaywallListener?, @@ -29,22 +29,22 @@ internal class IosPaywallDelegate( @Suppress("CAST_NEVER_SUCCEEDS") override fun paywallViewController( controller: RCPaywallViewController, - didStartPurchaseWithPackage: RCPackage + didStartPurchaseWithPackage: RCPackageFromKnUi, ) { listener?.onPurchaseStarted( - (didStartPurchaseWithPackage as PhcPackage).toPackage() + (didStartPurchaseWithPackage as RCPackage).toPackage() ) } @Suppress("CAST_NEVER_SUCCEEDS") override fun paywallViewController( controller: RCPaywallViewController, - didFinishPurchasingWithCustomerInfo: RCCustomerInfo, - transaction: RCStoreTransaction? + didFinishPurchasingWithCustomerInfo: RCCustomerInfoFromKnUi, + transaction: RCStoreTransactionFromKnUi?, ) { listener?.onPurchaseCompleted( - (didFinishPurchasingWithCustomerInfo as PhcCustomerInfo).toCustomerInfo(), - (transaction as PhcStoreTransaction).toStoreTransaction() + (didFinishPurchasingWithCustomerInfo as RCCustomerInfo).toCustomerInfo(), + (transaction as RCStoreTransaction).toStoreTransaction() ) } @@ -68,10 +68,10 @@ internal class IosPaywallDelegate( @Suppress("CAST_NEVER_SUCCEEDS") override fun paywallViewController( controller: RCPaywallViewController, - didFinishRestoringWithCustomerInfo: RCCustomerInfo + didFinishRestoringWithCustomerInfo: RCCustomerInfoFromKnUi ) { listener?.onRestoreCompleted( - (didFinishRestoringWithCustomerInfo as PhcCustomerInfo).toCustomerInfo() + (didFinishRestoringWithCustomerInfo as RCCustomerInfo).toCustomerInfo() ) } diff --git a/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitCustomerCenter.kt b/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitCustomerCenter.kt index d57407664..a7b3dc943 100644 --- a/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitCustomerCenter.kt +++ b/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitCustomerCenter.kt @@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.UIKitViewController -import swiftPMImport.com.revenuecat.purchases.kn.ui.CustomerCenterUIViewController -import swiftPMImport.com.revenuecat.purchases.kn.ui.RCCustomerCenterViewControllerDelegateWrapperProtocol +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCCustomerCenterViewControllerDelegateProtocol +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCCustomerCenterViewController import com.revenuecat.purchases.kmp.ui.revenuecatui.modifier.layoutViewController import com.revenuecat.purchases.kmp.ui.revenuecatui.modifier.rememberLayoutViewControllerState import platform.darwin.NSObject @@ -23,11 +23,8 @@ internal fun UIKitCustomerCenter( UIKitViewController( modifier = modifier.layoutViewController(layoutViewControllerState), factory = { - CustomerCenterUIViewController() - .apply { - setDelegate(delegate) - setOnCloseHandler(onDismiss) - }.also { + RCCustomerCenterViewController(delegate = delegate) + .also { layoutViewControllerState.setViewController(it) } }, @@ -38,11 +35,11 @@ internal fun UIKitCustomerCenter( ) } -internal class IosCustomerCenterDelegate( +private class IosCustomerCenterDelegate( private val onDismiss: () -> Unit -) : RCCustomerCenterViewControllerDelegateWrapperProtocol, NSObject() { +) : RCCustomerCenterViewControllerDelegateProtocol, NSObject() { - override fun customerCenterViewControllerWasDismissed(controller: CustomerCenterUIViewController) { + override fun customerCenterViewControllerWasDismissed(controller: RCCustomerCenterViewController) { onDismiss() } } diff --git a/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitPaywall.kt b/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitPaywall.kt index 1187cdb7d..17ff259fc 100644 --- a/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitPaywall.kt +++ b/revenuecatui/src/iosMain/kotlin/com/revenuecat/purchases/kmp/ui/revenuecatui/UIKitPaywall.kt @@ -1,35 +1,31 @@ package com.revenuecat.purchases.kmp.ui.revenuecatui import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.UIKitViewController -import com.revenuecat.purchases.kmp.mappings.toIosOffering -import com.revenuecat.purchases.kmp.ui.revenuecatui.modifier.layoutViewController -import com.revenuecat.purchases.kmp.ui.revenuecatui.modifier.rememberLayoutViewControllerState import com.revenuecat.purchases.kmp.Purchases +import com.revenuecat.purchases.kmp.ktx.awaitCustomerInfo +import com.revenuecat.purchases.kmp.mappings.toIosOffering +import com.revenuecat.purchases.kmp.mappings.toPackage import com.revenuecat.purchases.kmp.models.CustomerInfo -import com.revenuecat.purchases.kmp.models.Package import com.revenuecat.purchases.kmp.models.PurchasesError import com.revenuecat.purchases.kmp.models.PurchasesErrorCode -import kotlinx.cinterop.CValue -import kotlinx.cinterop.ObjCSignatureOverride -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.pointed +import com.revenuecat.purchases.kmp.ui.revenuecatui.modifier.layoutViewController +import com.revenuecat.purchases.kmp.ui.revenuecatui.modifier.rememberLayoutViewControllerState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import platform.CoreGraphics.CGSize +import platform.Foundation.NSError +import platform.Foundation.NSLocalizedDescriptionKey import platform.darwin.NSObject -import swiftPMImport.com.revenuecat.purchases.kn.ui.HybridPurchaseLogicBridge -import swiftPMImport.com.revenuecat.purchases.kn.ui.PaywallProxy -import swiftPMImport.com.revenuecat.purchases.kn.ui.PaywallViewCreationParams -import swiftPMImport.com.revenuecat.purchases.kn.ui.RCOffering +import swiftPMImport.com.revenuecat.purchases.kn.core.RCPackage import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPaywallFooterViewController +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPaywallPurchaseHandlerProtocol import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPaywallViewController -import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPaywallViewControllerDelegateWrapperProtocol +import kotlin.coroutines.cancellation.CancellationException +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCOffering as RCOfferingFromKnUi +import swiftPMImport.com.revenuecat.purchases.kn.ui.RCPackage as RCPackageFromKnUi @Composable internal fun UIKitPaywall( @@ -38,83 +34,39 @@ internal fun UIKitPaywall( modifier: Modifier = Modifier, ) { val layoutViewControllerState = rememberLayoutViewControllerState() - - if (options.purchaseLogic != null && footer) { - println("[RevenueCat] Warning: purchaseLogic is not supported with PaywallFooter on iOS. " + - "The custom purchase logic will be ignored.") - } - - // PaywallProxy is used for all non-footer paywalls (with or without custom purchase logic). - // It creates the VC and sets itself as its delegate (weak ref), so retain it here. val coroutineScope = rememberCoroutineScope() - val proxy = remember { if (!footer) PaywallProxy() else null } - val purchaseLogicBridge = remember(options.purchaseLogic, options.offering) { - if (!footer) { - options.purchaseLogic?.toHybridPurchaseLogicBridge( - packages = options.offering?.availablePackages.orEmpty(), - scope = coroutineScope, - ) - } else null - } - val proxyDelegate = remember(options.listener, options.dismissRequest) { - if (!footer) { - IosPaywallProxyDelegate( - listener = options.listener, - dismissRequest = options.dismissRequest, - onHeightChange = { layoutViewControllerState.updateIntrinsicContentSize() }, - ) - } else null - } - proxy?.setDelegate(proxyDelegate) - // Clean up bridge pending requests when the composable leaves composition. - DisposableEffect(purchaseLogicBridge) { - onDispose { - purchaseLogicBridge?.cancelPending() - } - } - - // Footer path still uses direct VC constructors since PaywallProxy doesn't support - // PaywallViewCreationParams for footer views. + // Keeping references to avoid them being deallocated. val dismissRequestedHandler: (RCPaywallViewController?) -> Unit = remember(options.dismissRequest) { { options.dismissRequest() } } - val footerDelegate = remember(options.listener) { - if (footer) { - IosPaywallDelegate( - listener = options.listener, - onHeightChange = { layoutViewControllerState.updateIntrinsicContentSize() } - ) - } else null + val delegate = remember(options.listener) { + IosPaywallDelegate( + listener = options.listener, + onHeightChange = { layoutViewControllerState.updateIntrinsicContentSize() } + ) } UIKitViewController( modifier = modifier.layoutViewController(layoutViewControllerState), factory = { - if (!footer && proxy != null) { - val params = PaywallViewCreationParams().apply { - purchaseLogicBridge?.let { setPurchaseLogicBridge(it) } - setDisplayCloseButton( - platform.Foundation.NSNumber(bool = options.shouldDisplayDismissButton) - ) - if (options.customVariables.isNotEmpty()) { - setCustomVariables(options.customVariables.toIosCustomVariables()) - } - } - val offering = options.offering?.toIosOffering() - if (offering != null) { - params.setOfferingIdentifier(offering.identifier()) - } - proxy.createPaywallViewWithParams(params) - .also { layoutViewControllerState.setViewController(it) } - } else { - RCPaywallFooterViewController( - offering = options.offering?.toIosOffering() as? RCOffering, - displayCloseButton = options.shouldDisplayDismissButton, - shouldBlockTouchEvents = false, - dismissRequestedHandler = dismissRequestedHandler, - ).apply { setDelegate(footerDelegate) } - .also { layoutViewControllerState.setViewController(it) } - } + val paywallViewController = if (footer) RCPaywallFooterViewController( + offering = options.offering?.toIosOffering() as? RCOfferingFromKnUi, + displayCloseButton = options.shouldDisplayDismissButton, + shouldBlockTouchEvents = false, + dismissRequestedHandler = dismissRequestedHandler, + purchaseHandler = options.purchaseLogic?.toPurchaseHandler(coroutineScope), + ) else RCPaywallViewController( + offering = options.offering?.toIosOffering() as? RCOfferingFromKnUi, + displayCloseButton = options.shouldDisplayDismissButton, + shouldBlockTouchEvents = false, + dismissRequestedHandler = dismissRequestedHandler, + purchaseHandler = options.purchaseLogic?.toPurchaseHandler(coroutineScope), + ) + + paywallViewController + .apply { setDelegate(delegate) } + .apply { setCustomVariables(options.customVariables) } + .also { layoutViewControllerState.setViewController(it) } }, properties = uiKitInteropPropertiesNonExperimental( nonCooperativeInteractionMode = true, @@ -123,165 +75,86 @@ internal fun UIKitPaywall( ) } -/** - * Delegate for the [PaywallProxy] path. Implements the dictionary-based - * [RCPaywallViewControllerDelegateWrapperProtocol] to receive events from the proxy. - * Handles dismiss requests, height changes, and forwards simple listener events. - */ -internal class IosPaywallProxyDelegate( - private val listener: PaywallListener?, - private val dismissRequest: () -> Unit, - private val onHeightChange: (Int) -> Unit, -) : RCPaywallViewControllerDelegateWrapperProtocol, - NSObject() { - - override fun paywallViewControllerRequestedDismissal( - controller: RCPaywallViewController - ) { - dismissRequest() - } - - override fun paywallViewControllerWasDismissed(controller: RCPaywallViewController) { - // Cleanup is handled by PaywallProxy internally. - } - - override fun paywallViewControllerDidStartPurchase(controller: RCPaywallViewController) { - // onPurchaseStarted requires a Package object that we don't have in the proxy path. - } - - override fun paywallViewControllerDidCancelPurchase(controller: RCPaywallViewController) { - listener?.onPurchaseCancelled() - } - - @ObjCSignatureOverride - @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") - override fun paywallViewController( - controller: RCPaywallViewController, - didFailPurchasingWithErrorDictionary: Map - ) { - listener?.onPurchaseError(didFailPurchasingWithErrorDictionary.toPurchasesError()) +private fun RCPaywallViewController.setCustomVariables(variables: Map) { + variables.forEach { (key, value) -> + when (value) { + is CustomVariableValue.String -> setCustomVariable(value = value.value, forKey = key) + is CustomVariableValue.Number -> setCustomVariableNumber( + value = value.value, + forKey = key + ) + is CustomVariableValue.Boolean -> setCustomVariableBool( + value = value.value, + forKey = key + ) + else -> error("Unknown CustomVariableValue type: ${value::class.simpleName}") + } } +} - override fun paywallViewControllerDidStartRestore(controller: RCPaywallViewController) { - listener?.onRestoreStarted() - } +private fun PaywallPurchaseLogic.toPurchaseHandler( + coroutineScope: CoroutineScope, +) = PurchaseHandler(this, coroutineScope) { + Purchases.sharedInstance.awaitCustomerInfo() +} - @ObjCSignatureOverride - @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") - override fun paywallViewController( - controller: RCPaywallViewController, - didFailRestoringWithErrorDictionary: Map - ) { - listener?.onRestoreError(didFailRestoringWithErrorDictionary.toPurchasesError()) +private class PurchaseHandler( + private val purchaseLogic: PaywallPurchaseLogic, + private val coroutineScope: CoroutineScope, + private val getCustomerInfo: suspend () -> CustomerInfo, +) : RCPaywallPurchaseHandlerProtocol, NSObject() { + + override fun performRestoreWithCompletion(completion: (success: Boolean, error: NSError?) -> Unit) { + coroutineScope.launch { + var success = false + var error: NSError? = null + try { + val result = purchaseLogic.performRestore(getCustomerInfo()) + error = (result as? PurchaseLogicResult.Error)?.errorDetails?.toNSError() + success = result is PurchaseLogicResult.Success + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + error = PurchasesError( + code = PurchasesErrorCode.UnknownError, + underlyingErrorMessage = "Error performing restore: ${t.message}" + ).toNSError() + } finally { + completion(success, error) + } + } } - override fun paywallViewController( - controller: RCPaywallViewController, - didChangeSizeTo: CValue + override fun performPurchaseFor( + `package`: RCPackageFromKnUi, + completion: (userCancelled: Boolean, error: NSError?) -> Unit ) { - onHeightChange(didChangeSizeTo.heightInt()) - } -} - -private fun PaywallPurchaseLogic.toHybridPurchaseLogicBridge( - packages: List, - scope: CoroutineScope, -): HybridPurchaseLogicBridge { - val purchaseLogic = this - val packagesByIdentifier = packages.associateBy { it.identifier } - - return HybridPurchaseLogicBridge( - onPerformPurchase = { eventData -> - eventData.launchBridgeRequest(scope) { requestId -> - @Suppress("UNCHECKED_CAST") - val packageDict = eventData?.get(HybridPurchaseLogicBridge.eventKeyPackageBeingPurchased()) - as? Map - val packageIdentifier = packageDict?.get("identifier") as? String - val rcPackage = packageIdentifier?.let { packagesByIdentifier[it] } - ?: error("Unable to find package with identifier: $packageIdentifier") - val params = PaywallPurchaseLogicParams( - rcPackage = rcPackage, + coroutineScope.launch { + var userCancelled = false + var error: NSError? = null + try { + val result = purchaseLogic.performPurchase( + PaywallPurchaseLogicParams((`package` as RCPackage).toPackage()) ) - purchaseLogic.performPurchase(params) - } - }, - onPerformRestore = { eventData -> - eventData.launchBridgeRequest(scope) { - val customerInfo = fetchCustomerInfo() - purchaseLogic.performRestore(customerInfo) + error = (result as? PurchaseLogicResult.Error)?.errorDetails?.toNSError() + userCancelled = result is PurchaseLogicResult.Cancellation + } catch (e: CancellationException) { + userCancelled = true + throw e + } catch (t: Throwable) { + error = PurchasesError( + code = PurchasesErrorCode.UnknownError, + underlyingErrorMessage = "Error performing purchase: ${t.message}" + ).toNSError() + } finally { + completion(userCancelled, error) } } - ) -} - -/** - * Extracts the request ID from bridge event data, launches a coroutine to execute [block], - * and resolves the result (or error) back to the bridge. - */ -private fun Map?.launchBridgeRequest( - scope: CoroutineScope, - block: suspend (requestId: String) -> PurchaseLogicResult, -) { - val requestId = this?.get(HybridPurchaseLogicBridge.eventKeyRequestId()) as? String ?: return - scope.launch { - try { - resolveResult(requestId, block(requestId)) - } catch (e: Exception) { - HybridPurchaseLogicBridge.resolveResultWithRequestId( - requestId = requestId, - resultString = HybridPurchaseLogicBridge.resultError(), - errorMessage = e.message, - ) - } } -} -private fun resolveResult(requestId: String, result: PurchaseLogicResult) { - when (result) { - is PurchaseLogicResult.Success -> HybridPurchaseLogicBridge.resolveResultWithRequestId( - requestId = requestId, - resultString = HybridPurchaseLogicBridge.resultSuccess(), - ) - is PurchaseLogicResult.Cancellation -> HybridPurchaseLogicBridge.resolveResultWithRequestId( - requestId = requestId, - resultString = HybridPurchaseLogicBridge.resultCancellation(), - ) - is PurchaseLogicResult.Error -> HybridPurchaseLogicBridge.resolveResultWithRequestId( - requestId = requestId, - resultString = HybridPurchaseLogicBridge.resultError(), - errorMessage = result.errorDetails?.let { - it.underlyingErrorMessage ?: it.code.description - }, - ) - } + private fun PurchasesError.toNSError() = NSError( + code = code.code.toLong(), + domain = "com.revenuecat.purchases.kmp", + userInfo = mapOf(NSLocalizedDescriptionKey to message), + ) } - -private suspend fun fetchCustomerInfo(): CustomerInfo = - suspendCancellableCoroutine { continuation -> - Purchases.sharedInstance.getCustomerInfo( - onError = { error -> - continuation.resumeWith(Result.failure(Exception(error.message))) - }, - onSuccess = { customerInfo -> - continuation.resumeWith(Result.success(customerInfo)) - }, - ) - } - -private fun Map.toPurchasesError(): PurchasesError = PurchasesError( - code = PurchasesErrorCode.UnknownError, - underlyingErrorMessage = this["message"] as? String, -) - -private fun CValue.heightInt(): Int = - memScoped { ptr.pointed.height.toInt() } - -private fun Map.toIosCustomVariables(): Map = - entries.associate { (key, value) -> - key to when (value) { - is CustomVariableValue.String -> value.value as Any - is CustomVariableValue.Number -> value.value as Any - is CustomVariableValue.Boolean -> value.value as Any - else -> error("Unknown CustomVariableValue type: ${value::class.simpleName}") - } - } diff --git a/settings.gradle.kts b/settings.gradle.kts index 23b60066a..d6c19c572 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,8 +21,9 @@ rootProject.name = "purchases-kmp" include(":apiTester") include(":composeApp") include(":core") -include(":datetime") include(":either") +include(":kn-core") +include(":kn-ui") include(":mappings") include(":models") include(":result")