diff --git a/.github/workflows/build_debug_app.yml b/.github/workflows/build_debug_app.yml index 01ca453..ca0289b 100644 --- a/.github/workflows/build_debug_app.yml +++ b/.github/workflows/build_debug_app.yml @@ -16,6 +16,8 @@ jobs: runs-on: macos-latest env: API_KEY: ${{ secrets.API_KEY }} + REVENUECAT_API_KEY_ANDROID: ${{ secrets.REVENUECAT_API_KEY_ANDROID }} + REVENUECAT_API_KEY_IOS: ${{ secrets.REVENUECAT_API_KEY_IOS }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/publish-playstore-appstore.yml b/.github/workflows/publish-playstore-appstore.yml index fd2cb47..c9a9349 100644 --- a/.github/workflows/publish-playstore-appstore.yml +++ b/.github/workflows/publish-playstore-appstore.yml @@ -14,6 +14,8 @@ jobs: env: github_token: ${{ secrets.GITHUB_TOKEN }} API_KEY: ${{ secrets.API_KEY }} + REVENUECAT_API_KEY_ANDROID: ${{ secrets.REVENUECAT_API_KEY_ANDROID }} + REVENUECAT_API_KEY_IOS: ${{ secrets.REVENUECAT_API_KEY_IOS }} steps: diff --git a/androidApp/src/androidMain/AndroidManifest.xml b/androidApp/src/androidMain/AndroidManifest.xml index 75c3f79..666d5bc 100644 --- a/androidApp/src/androidMain/AndroidManifest.xml +++ b/androidApp/src/androidMain/AndroidManifest.xml @@ -35,7 +35,7 @@ android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/orange" /> - + \ No newline at end of file diff --git a/distribution/whatsnew/whatsnew-en-US b/distribution/whatsnew/whatsnew-en-US index 0323d17..62f27aa 100644 --- a/distribution/whatsnew/whatsnew-en-US +++ b/distribution/whatsnew/whatsnew-en-US @@ -1 +1 @@ -Authentication with Google, Apple, Github is added. \ No newline at end of file +Subscription Premium is added for premium features. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f766c3d..736caeb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ android.useAndroidX=true android.targetSdk=34 android.compileSdk=34 android.minSdk=24 -android.versionCode=17 -android.versionName=2.2.0 +android.versionCode=18 +android.versionName=2.3.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2894cbd..4daf07f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ firebaseCrashlytics = "2.9.9" kmpNotifier = "0.3.0" kmpAuth = "1.1.0" coil = "3.0.0-alpha03" +kmpRevenueCat="0.0.6" @@ -79,6 +80,11 @@ kmpAuth-uihelper = { group = "io.github.mirzemehdi", name = "kmpauth-uihelper", coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } coil-ktor = { group = "io.coil-kt.coil3", name = "coil-network-ktor", version.ref = "coil" } +# KMPRevenuecat - Subscription lib +kmprevenuecat-purchases = { group = "io.github.mirzemehdi", name = "kmprevenuecat-purchases", version.ref = "kmpRevenueCat" } +kmprevenuecat-purchases-ui = { group = "io.github.mirzemehdi", name = "kmprevenuecat-purchases-ui", version.ref = "kmpRevenueCat" } + + [bundles] voyager = ["voyager-navigator", "voyager-tabNavigator", "voyager-transitions"] diff --git a/iosApp/FindTravelNow StoreKit.storekit b/iosApp/FindTravelNow StoreKit.storekit new file mode 100644 index 0000000..21dee6a --- /dev/null +++ b/iosApp/FindTravelNow StoreKit.storekit @@ -0,0 +1,80 @@ +{ + "identifier" : "69C00004", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "770F93F8", + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "lifetime_premium_v2", + "referenceName" : "lifetime_premium_v2", + "type" : "NonConsumable" + } + ], + "settings" : { + "_failTransactionsEnabled" : false, + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + + ], + "version" : { + "major" : 3, + "minor" : 0 + } +} diff --git a/iosApp/Podfile.lock b/iosApp/Podfile.lock index 934c571..2bce42a 100644 --- a/iosApp/Podfile.lock +++ b/iosApp/Podfile.lock @@ -11,6 +11,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: shared: 45f487984c6fbabf19d6b4eb91ffb5129a2bfc20 -PODFILE CHECKSUM: eae97542391f0ffa4310330e32cfe71726dbe5ec +PODFILE CHECKSUM: 0a20442581fb5fc64112ac1d7a63be54e50e4662 COCOAPODS: 1.14.3 diff --git a/iosApp/Pods/Manifest.lock b/iosApp/Pods/Manifest.lock index 934c571..2bce42a 100644 --- a/iosApp/Pods/Manifest.lock +++ b/iosApp/Pods/Manifest.lock @@ -11,6 +11,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: shared: 45f487984c6fbabf19d6b4eb91ffb5129a2bfc20 -PODFILE CHECKSUM: eae97542391f0ffa4310330e32cfe71726dbe5ec +PODFILE CHECKSUM: 0a20442581fb5fc64112ac1d7a63be54e50e4662 COCOAPODS: 1.14.3 diff --git a/iosApp/Pods/Pods.xcodeproj/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist b/iosApp/Pods/Pods.xcodeproj/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist index 7dce8f0..84bd584 100644 --- a/iosApp/Pods/Pods.xcodeproj/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/iosApp/Pods/Pods.xcodeproj/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -8,11 +8,15 @@ isShown + orderHint + 2 shared.xcscheme isShown + orderHint + 6 SuppressBuildableAutocreation diff --git a/iosApp/StoreKitTestCertificate.cer b/iosApp/StoreKitTestCertificate.cer new file mode 100644 index 0000000..5896156 Binary files /dev/null and b/iosApp/StoreKitTestCertificate.cer differ diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index c18b956..e07f8ea 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -23,11 +23,13 @@ A6886A9B2AFBF140000FC219 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A6886A9A2AFBF140000FC219 /* GoogleService-Info.plist */; }; A689DB122AF5F91200EC7353 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = A689DB112AF5F91200EC7353 /* FirebaseAnalytics */; }; A68EBF9F2AF5ACA30038BFFD /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = A68EBF9E2AF5ACA30038BFFD /* FirebaseCrashlytics */; }; + A6BD7BF22B86F6940052C214 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6BD7BF12B86F6940052C214 /* StoreKit.framework */; }; A6D548BA2B0ECD9A0060761F /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = A6D548B92B0ECD9A0060761F /* FirebaseMessaging */; }; + A6DB22392B85B77B0086C4D3 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = A6DB22382B85B77B0086C4D3 /* RevenueCat */; }; + A6DB223B2B85B77B0086C4D3 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = A6DB223A2B85B77B0086C4D3 /* RevenueCatUI */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 034C794B9477822EA66F43DC /* 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 = ""; }; 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 1C775BE0B2EC6251EEB5DDB2 /* libPods-iosApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iosApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -35,11 +37,13 @@ 7555FF7B242A565900829871 /* FindTravelNow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FindTravelNow.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 = ""; }; - A0A10663FE7537E54A8D6A37 /* 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 = ""; }; A621B42A2AF4899C00D87165 /* iosAppRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iosAppRelease.entitlements; sourceTree = ""; }; A6886A9A2AFBF140000FC219 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../iosApp/GoogleService-Info.plist"; sourceTree = ""; }; + A6BD7BF12B86F6940052C214 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; A6D548B82B0ECD840060761F /* iosApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iosApp.entitlements; sourceTree = ""; }; AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + AEC633E835AD47E2894F710A /* 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 = ""; }; + B27AACAA71233EC440B04D5C /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +53,7 @@ files = ( A648BF9C2B5D48CC00896CA6 /* FirebaseAnalyticsSwift in Frameworks */, A648BF9E2B5D48CC00896CA6 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */, + A6DB223B2B85B77B0086C4D3 /* RevenueCatUI in Frameworks */, A648BF982B5D48CC00896CA6 /* FirebaseAnalytics in Frameworks */, A648BF942B5D48AA00896CA6 /* GoogleSignIn in Frameworks */, A6D548BA2B0ECD9A0060761F /* FirebaseMessaging in Frameworks */, @@ -56,7 +61,9 @@ A648BF962B5D48AA00896CA6 /* GoogleSignInSwift in Frameworks */, A648BFA22B5D49FD00896CA6 /* FirebaseAuth in Frameworks */, A68EBF9F2AF5ACA30038BFFD /* FirebaseCrashlytics in Frameworks */, + A6DB22392B85B77B0086C4D3 /* RevenueCat in Frameworks */, 769F3BF91D39AE2AF514DB56 /* libPods-iosApp.a in Frameworks */, + A6BD7BF22B86F6940052C214 /* StoreKit.framework in Frameworks */, A648BF9A2B5D48CC00896CA6 /* FirebaseAnalyticsOnDeviceConversion in Frameworks */, A648BFA02B5D48CC00896CA6 /* FirebaseAppCheck in Frameworks */, ); @@ -76,20 +83,12 @@ 42799AB246E5F90AF97AA0EF /* Frameworks */ = { isa = PBXGroup; children = ( + A6BD7BF12B86F6940052C214 /* StoreKit.framework */, 1C775BE0B2EC6251EEB5DDB2 /* libPods-iosApp.a */, ); name = Frameworks; sourceTree = ""; }; - 5BDE45FF738E720B07260B4E /* Pods */ = { - isa = PBXGroup; - children = ( - A0A10663FE7537E54A8D6A37 /* Pods-iosApp.debug.xcconfig */, - 034C794B9477822EA66F43DC /* Pods-iosApp.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; 7555FF72242A565900829871 = { isa = PBXGroup; children = ( @@ -97,7 +96,7 @@ 7555FF7D242A565900829871 /* iosApp */, 7555FF7C242A565900829871 /* Products */, 42799AB246E5F90AF97AA0EF /* Frameworks */, - 5BDE45FF738E720B07260B4E /* Pods */, + A75D105B3C54BB927D9736BE /* Pods */, ); sourceTree = ""; }; @@ -124,6 +123,15 @@ path = iosApp; sourceTree = ""; }; + A75D105B3C54BB927D9736BE /* Pods */ = { + isa = PBXGroup; + children = ( + B27AACAA71233EC440B04D5C /* Pods-iosApp.debug.xcconfig */, + AEC633E835AD47E2894F710A /* Pods-iosApp.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; AB1DB47929225F7C00F7AF9C /* Configuration */ = { isa = PBXGroup; children = ( @@ -164,6 +172,8 @@ A648BF9D2B5D48CC00896CA6 /* FirebaseAnalyticsWithoutAdIdSupport */, A648BF9F2B5D48CC00896CA6 /* FirebaseAppCheck */, A648BFA12B5D49FD00896CA6 /* FirebaseAuth */, + A6DB22382B85B77B0086C4D3 /* RevenueCat */, + A6DB223A2B85B77B0086C4D3 /* RevenueCatUI */, ); productName = iosApp; productReference = 7555FF7B242A565900829871 /* FindTravelNow.app */; @@ -196,6 +206,7 @@ packageReferences = ( A68EBF792AF5AC5A0038BFFD /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, A648BF922B5D48AA00896CA6 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + A6DB22372B85B77B0086C4D3 /* XCRemoteSwiftPackageReference "purchases-ios" */, ); productRefGroup = 7555FF7C242A565900829871 /* Products */; projectDirPath = ""; @@ -434,10 +445,11 @@ }; 7555FFA6242A565B00829871 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A0A10663FE7537E54A8D6A37 /* Pods-iosApp.debug.xcconfig */; + baseConfigurationReference = B27AACAA71233EC440B04D5C /* Pods-iosApp.debug.xcconfig */; buildSettings = { APP_NAME = FindTravelNow; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME[sdk=*]" = AccentColor; BUNDLE_ID = com.measify.findtravelnow; CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -454,7 +466,7 @@ INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = FindTravelNow; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.travel"; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -477,10 +489,11 @@ }; 7555FFA7242A565B00829871 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 034C794B9477822EA66F43DC /* Pods-iosApp.release.xcconfig */; + baseConfigurationReference = AEC633E835AD47E2894F710A /* Pods-iosApp.release.xcconfig */; buildSettings = { APP_NAME = FindTravelNow; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME[sdk=*]" = AccentColor; BUNDLE_ID = com.measify.findtravelnow; CODE_SIGN_ENTITLEMENTS = iosApp/iosAppRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -497,7 +510,7 @@ INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = FindTravelNow; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.travel"; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -558,6 +571,14 @@ minimumVersion = 10.17.0; }; }; + A6DB22372B85B77B0086C4D3 /* XCRemoteSwiftPackageReference "purchases-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RevenueCat/purchases-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.36.3; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -611,6 +632,16 @@ package = A68EBF792AF5AC5A0038BFFD /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseMessaging; }; + A6DB22382B85B77B0086C4D3 /* RevenueCat */ = { + isa = XCSwiftPackageProductDependency; + package = A6DB22372B85B77B0086C4D3 /* XCRemoteSwiftPackageReference "purchases-ios" */; + productName = RevenueCat; + }; + A6DB223A2B85B77B0086C4D3 /* RevenueCatUI */ = { + isa = XCSwiftPackageProductDependency; + package = A6DB22372B85B77B0086C4D3 /* XCRemoteSwiftPackageReference "purchases-ios" */; + productName = RevenueCatUI; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 7555FF73242A565900829871 /* Project object */; diff --git a/iosApp/iosApp.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcworkspace/contents.xcworkspacedata index c009e7d..552cf8a 100644 --- a/iosApp/iosApp.xcworkspace/contents.xcworkspacedata +++ b/iosApp/iosApp.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,12 @@ + + + + diff --git a/iosApp/iosApp.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iosApp/iosApp.xcworkspace/xcshareddata/swiftpm/Package.resolved index ed8857e..e825b15 100644 --- a/iosApp/iosApp.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iosApp/iosApp.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -135,6 +135,15 @@ "version" : "2.3.1" } }, + { + "identity" : "purchases-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RevenueCat/purchases-ios", + "state" : { + "revision" : "c598bfa60672881933d8945584cc327acd6f018a", + "version" : "4.36.3" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", diff --git a/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/UserInterfaceState.xcuserstate b/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/UserInterfaceState.xcuserstate index 8d3e69e..861c182 100644 Binary files a/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/UserInterfaceState.xcuserstate and b/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist b/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist index a607980..b7a9846 100644 --- a/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/iosApp/iosApp.xcworkspace/xcuserdata/mirzemehdi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,7 +9,21 @@ isShown orderHint - 7 + 8 + + Promises (Playground) 10.xcscheme + + isShown + + orderHint + 14 + + Promises (Playground) 11.xcscheme + + isShown + + orderHint + 15 Promises (Playground) 2.xcscheme @@ -23,49 +37,56 @@ isShown orderHint - 1 + 4 Promises (Playground) 4.xcscheme isShown orderHint - 2 + 5 Promises (Playground) 5.xcscheme isShown orderHint - 4 + 6 Promises (Playground) 6.xcscheme isShown orderHint - 15 + 7 Promises (Playground) 7.xcscheme isShown orderHint - 16 + 8 Promises (Playground) 8.xcscheme isShown orderHint - 17 + 9 + + Promises (Playground) 9.xcscheme + + isShown + + orderHint + 13 Promises (Playground).xcscheme isShown orderHint - 3 + 7 diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json index ee7e3ca..89b5d90 100644 --- a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,10 @@ { "colors" : [ { + "color" : { + "platform" : "ios", + "reference" : "systemYellowColor" + }, "idiom" : "universal" } ], @@ -8,4 +12,4 @@ "author" : "xcode", "version" : 1 } -} \ No newline at end of file +} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index 68bbe1b..a81cdc2 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.2.0 + 2.3.0 CFBundleURLTypes @@ -36,7 +36,7 @@ CFBundleVersion - 22 + 23 GIDClientID 400988245981-9tv75mdhd0he2unncn3t1g3dq0gur3oc.apps.googleusercontent.com GIDServerClientID diff --git a/secrets.properties b/secrets.properties index 2387a6f..1b5203b 100644 --- a/secrets.properties +++ b/secrets.properties @@ -1 +1,3 @@ -API_KEY= \ No newline at end of file +API_KEY= +REVENUECAT_API_KEY_ANDROID= +REVENUECAT_API_KEY_IOS= \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0ad46e0..81bd399 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,7 @@ plugins { dependencyResolutionManagement { repositories { mavenCentral() + mavenLocal() google() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 04db505..fae3010 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -38,6 +38,8 @@ kotlin { | |object BuildConfig { | const val API_KEY = "${secretProperties.getPropertyValue("API_KEY")}" + | const val REVENUECAT_API_KEY_ANDROID = "${secretProperties.getPropertyValue("REVENUECAT_API_KEY_ANDROID")}" + | const val REVENUECAT_API_KEY_IOS = "${secretProperties.getPropertyValue("REVENUECAT_API_KEY_IOS")}" |} | """.trimMargin() @@ -78,6 +80,9 @@ kotlin { implementation(libs.coil.compose) implementation(libs.coil.ktor) + implementation(libs.kmprevenuecat.purchases) + implementation(libs.kmprevenuecat.purchases.ui) + } } @@ -95,8 +100,6 @@ kotlin { api(libs.firebase.crashlytics) api(libs.firebase.messaging) -// implementation("io.ktor:ktor-client-okhttp:2.3.7") - } } iosMain { diff --git a/shared/src/androidMain/kotlin/util/PlatformSpecific.kt b/shared/src/androidMain/kotlin/util/PlatformSpecific.kt index d2e3bd8..275d4f4 100644 --- a/shared/src/androidMain/kotlin/util/PlatformSpecific.kt +++ b/shared/src/androidMain/kotlin/util/PlatformSpecific.kt @@ -37,3 +37,5 @@ internal class AndroidAppVersion(private val context: Context) : AppVersion { context.packageManager.getPackageInfo(context.packageName, 0) } + +actual fun isAndroid() = true \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/data/repository/UserRepository.kt b/shared/src/commonMain/kotlin/data/repository/UserRepository.kt index e9cff1f..f594ee1 100644 --- a/shared/src/commonMain/kotlin/data/repository/UserRepository.kt +++ b/shared/src/commonMain/kotlin/data/repository/UserRepository.kt @@ -1,19 +1,33 @@ package data.repository +import com.mmk.kmprevenuecat.purchases.Purchases +import com.mmk.kmprevenuecat.purchases.PurchasesException +import com.mmk.kmprevenuecat.purchases.awaitCustomerInfo +import com.mmk.kmprevenuecat.purchases.data.CustomerInfo import data.source.remote.apiservice.UserApiService import data.source.remote.request.UserUpdateRequest import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.auth.auth import domain.model.AuthProvider +import domain.model.Subscription import domain.model.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.MainScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import util.logging.AppLogger import kotlin.coroutines.CoroutineContext @@ -23,26 +37,77 @@ class UserRepository( private val backgroundScope: CoroutineContext = Dispatchers.IO ) { + private val currentUserSubscriptionFlow = MutableStateFlow(null) + private val firebaseCurrentUserFlow = Firebase.auth.authStateChanged + .onEach { firebaseUser -> + firebaseUser?.let { + Purchases.login(firebaseUser.uid) { subscriptionLoginResult -> + val subscription = + subscriptionLoginResult.getOrNull()?.customerInfo?.asSubscription() + currentUserSubscriptionFlow.tryEmit(subscription) + } + } + } + .map { firebaseUser -> + if (firebaseUser == null) null + else userApiService.createOrGetUser().data?.mapToDomainModel().also { + Purchases.setAttributes(mapOf("\$email" to it?.email,"\$displayName" to it?.displayName)) + } + } + + + val currentUser = + combine(firebaseCurrentUserFlow, currentUserSubscriptionFlow) { user, subscription -> + user?.copy(subscription = subscription) + } + .flowOn(backgroundScope) + .stateIn(MainScope(), SharingStarted.WhileSubscribed(5000), null) + - val currentUser = Firebase.auth.authStateChanged.map { - if (it == null) null - else userApiService.createOrGetUser().data?.mapToDomainModel() - }.flowOn(backgroundScope).stateIn(MainScope(), SharingStarted.Eagerly,null) + fun refreshUserSubscription(){ + MainScope().launch { + currentUserSubscriptionFlow.emit(getUserSubscription()) + } + } + private suspend fun getUserSubscription(): Subscription? { + val customerInfo = try { + Purchases.awaitCustomerInfo() + } catch (e: PurchasesException) { + null + } ?: return null + return customerInfo.asSubscription() + } + + private fun CustomerInfo?.asSubscription(): Subscription? { + val customerInfo = this ?: return null + val premiumSubscriptionEntitlement = customerInfo.entitlements.all["Premium"] + return premiumSubscriptionEntitlement?.let { + Subscription( + entitlementId = it.identifier, + expirationDate = it.expirationDate, + isActive = it.isActive + ) + } + } suspend fun logOut() = withContext(backgroundScope) { Firebase.auth.signOut() + Purchases.logOut { } } suspend fun deleteAccount() = withContext(backgroundScope) { val currentUser = Firebase.auth.currentUser currentUser?.delete() - kotlin.runCatching { Firebase.auth.signOut() } + kotlin.runCatching { logOut() } } suspend fun updateProfile(user: User) = withContext(backgroundScope) { - userApiService.updateUser(userUpdateRequest = UserUpdateRequest(displayName = user.displayName,profilePicUrl = user.profilePicSrc)) -// val currentUser = Firebase.auth.currentUser -// currentUser?.updateProfile(displayName = user.displayName, photoUrl = user.profilePicSrc) + userApiService.updateUser( + userUpdateRequest = UserUpdateRequest( + displayName = user.displayName, + profilePicUrl = user.profilePicSrc + ) + ) } fun getCurrentUserProviders(): List { diff --git a/shared/src/commonMain/kotlin/domain/model/Subscription.kt b/shared/src/commonMain/kotlin/domain/model/Subscription.kt new file mode 100644 index 0000000..c7f717c --- /dev/null +++ b/shared/src/commonMain/kotlin/domain/model/Subscription.kt @@ -0,0 +1,7 @@ +package domain.model + +data class Subscription( + val entitlementId:String, + val expirationDate:Long?, + val isActive:Boolean +) diff --git a/shared/src/commonMain/kotlin/domain/model/User.kt b/shared/src/commonMain/kotlin/domain/model/User.kt index d707811..b212c1c 100644 --- a/shared/src/commonMain/kotlin/domain/model/User.kt +++ b/shared/src/commonMain/kotlin/domain/model/User.kt @@ -5,4 +5,7 @@ data class User( val displayName: String = "", val profilePicSrc: String? = "", val email: String? = null, -) \ No newline at end of file + val subscription: Subscription? = null +) { + fun hasPremiumSubscription() = subscription != null && subscription.isActive +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/presentation/components/SubscriptionPaywall.kt b/shared/src/commonMain/kotlin/presentation/components/SubscriptionPaywall.kt new file mode 100644 index 0000000..ed0f48c --- /dev/null +++ b/shared/src/commonMain/kotlin/presentation/components/SubscriptionPaywall.kt @@ -0,0 +1,36 @@ +package presentation.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.mmk.kmprevenuecat.purchases.ui.Paywall +import com.mmk.kmprevenuecat.purchases.ui.PaywallListener +import util.isAndroid + +@Composable +fun SubscriptionPaywall(onDismiss: () -> Unit, listener: PaywallListener? = null) { + //In ios no need for fullscreen dialog,in android we add extra full screen dialog + if (isAndroid()) + Dialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = onDismiss + ) { + Surface(modifier = Modifier.fillMaxSize()) { + Paywall( + shouldDisplayDismissButton = true, + onDismiss = onDismiss, + listener = listener + ) + } + } + else { + Paywall( + shouldDisplayDismissButton = true, + onDismiss = onDismiss, + listener = listener + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt index 343ae32..f9b0341 100644 --- a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt +++ b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState @@ -42,8 +43,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter +import com.mmk.kmprevenuecat.purchases.data.CustomerInfo +import com.mmk.kmprevenuecat.purchases.ui.Paywall +import com.mmk.kmprevenuecat.purchases.ui.PaywallListener import dev.gitlive.firebase.auth.FirebaseUser import domain.model.AuthProvider import domain.model.User @@ -55,11 +61,13 @@ import presentation.components.ExpandableBoxItem import presentation.components.GradientButton import presentation.components.MyAppCircularProgressIndicator import presentation.components.MyAppLabelledTextInput +import presentation.components.SubscriptionPaywall import presentation.components.TitleDescription import presentation.theme.Alabaster import presentation.theme.Black_22 import presentation.theme.strings.Strings import util.asState +import util.isAndroid @OptIn(ExperimentalResourceApi::class) @@ -83,10 +91,11 @@ fun ProfileScreen(modifier: Modifier = Modifier, uiStateHolder: ProfileUiStateHo } val snackbarHostState = remember { SnackbarHostState() } - Scaffold(modifier = modifier.fillMaxSize(), + Scaffold( + modifier = modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackbarHostState) - } + }, ) { LaunchedEffect(uiState.message) { if (uiState.message.isNullOrEmpty().not()) { @@ -104,7 +113,8 @@ fun ProfileScreen(modifier: Modifier = Modifier, uiStateHolder: ProfileUiStateHo onClickDeleteAccount = uiStateHolder::onClickDeleteAccount, onClickEnableEditMode = uiStateHolder::onCLickEnableEditMode, onClickUpdateProfile = uiStateHolder::onClickUpdateProfile, - onChangeDisplayName = uiStateHolder::onChangeDisplayName + onChangeDisplayName = uiStateHolder::onChangeDisplayName, + onClickUpgradePremium = uiStateHolder::onClickUpgradePremium, ) } @@ -116,6 +126,33 @@ fun ProfileScreen(modifier: Modifier = Modifier, uiStateHolder: ProfileUiStateHo MyAppCircularProgressIndicator() } } + if (uiState.isSubscribePlansViewVisible) { + SubscriptionPaywall( + onDismiss = uiStateHolder::onDismissSubscriptionPlansView, + listener = object:PaywallListener{ + override fun onPurchaseCompleted(customerInfo: CustomerInfo?) { + super.onPurchaseCompleted(customerInfo) + uiStateHolder.onSubscriptionPurchaseCompleted() + } + + override fun onPurchaseError(error: String?) { + super.onPurchaseError(error) + uiStateHolder.onSubscriptionPurchaseError(error) + } + + + override fun onRestoreCompleted(customerInfo: CustomerInfo?) { + super.onRestoreCompleted(customerInfo) + uiStateHolder.onSubscriptionPurchaseCompleted() + } + + override fun onRestoreError(error: String?) { + super.onRestoreError(error) + uiStateHolder.onSubscriptionPurchaseError(error) + } + } + ) + } } @OptIn(ExperimentalResourceApi::class, ExperimentalMaterial3Api::class) @@ -130,6 +167,7 @@ private fun ProfileScreen( onClickEnableEditMode: () -> Unit, onClickUpdateProfile: () -> Unit, onChangeDisplayName: (String) -> Unit, + onClickUpgradePremium: () -> Unit, ) { Box(modifier = modifier) { @@ -208,6 +246,12 @@ private fun ProfileScreen( modifier = Modifier.fillMaxWidth().padding(top = 24.dp), currentUser = currentUser ) + + SubscriptionInfo( + modifier = Modifier.fillMaxWidth().padding(top = 20.dp), + currentUser = currentUser, + onClickUpgradePremium = onClickUpgradePremium + ) GradientButton( modifier = Modifier.padding(top = 32.dp).fillMaxWidth(), text = Strings.btn_log_out, @@ -272,6 +316,54 @@ private fun BasicInfo(modifier: Modifier = Modifier, currentUser: User) { } } +@Composable +private fun SubscriptionInfo( + modifier: Modifier = Modifier, + currentUser: User, + onClickUpgradePremium: () -> Unit +) { + var isExpanded by remember { mutableStateOf(false) } + ExpandableBoxItem(modifier = modifier, + text = Strings.subscription_info, + isExpanded = isExpanded, + onToggle = { isExpanded = !isExpanded } + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + + + TitleDescription( + title = Strings.subscription_plan_title, + description = if (currentUser.hasPremiumSubscription()) Strings.subscription_plan_premium else Strings.subscription_plan_free, + ) + + if ( + currentUser.hasPremiumSubscription() && + (currentUser.subscription?.expirationDate ?: 0L) == 0L + ) + TitleDescription( + title = Strings.subscription_plan_expiration_date, + description = Strings.subscription_lifetime, + ) + if (currentUser.hasPremiumSubscription().not()) { + TextButton( + onClick = { onClickUpgradePremium() }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Text( + text = Strings.btn_get_premium_subscription_plan_free, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Start, + ) + } + } + + } + } +} + @Composable private fun EditInfoContainer( modifier: Modifier = Modifier, diff --git a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreenUiState.kt b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreenUiState.kt index 07928ea..f31defe 100644 --- a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreenUiState.kt +++ b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileScreenUiState.kt @@ -11,6 +11,7 @@ data class ProfileScreenUiState( val reAuthenticateUserViewShown: Boolean = false, val isEditMode: Boolean = false, val isEditInProgress: Boolean = false, + val isSubscribePlansViewVisible: Boolean = false, val currentUserAuthProviderList: List = emptyList(), val message: String? = null, ) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt index b585630..69661ae 100644 --- a/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt +++ b/shared/src/commonMain/kotlin/presentation/screens/account/profile/ProfileUiStateHolder.kt @@ -60,6 +60,15 @@ class ProfileUiStateHolder(private val userRepository: UserRepository) : UiState _profileScreenUiState.update { it.copy(currentUser = it.currentUser?.copy(displayName = displayName)) } } + fun onClickUpgradePremium(){ + _profileScreenUiState.update { it.copy(isSubscribePlansViewVisible = true) } + } + + fun onDismissSubscriptionPlansView(){ + _profileScreenUiState.update { it.copy(isSubscribePlansViewVisible = false) } + userRepository.refreshUserSubscription() + } + fun onConfirmDeleteAccount() = uiStateHolderScope.launch { _profileScreenUiState.update { it.copy( @@ -78,6 +87,14 @@ class ProfileUiStateHolder(private val userRepository: UserRepository) : UiState _profileScreenUiState.update { it.copy(message = null) } } + fun onSubscriptionPurchaseCompleted() = uiStateHolderScope.launch { + userRepository.refreshUserSubscription() + } + + fun onSubscriptionPurchaseError(errorMessage:String?){ + _profileScreenUiState.update { it.copy(message = errorMessage) } + } + fun onUserReAuthenticatedResult(result: Result) = uiStateHolderScope.launch { _profileScreenUiState.update { it.copy(isDeleteInProgress = true) } result.onSuccess { user -> diff --git a/shared/src/commonMain/kotlin/presentation/screens/main/navigation/MainScreenNavigation.kt b/shared/src/commonMain/kotlin/presentation/screens/main/navigation/MainScreenNavigation.kt index b692d59..12e49f5 100644 --- a/shared/src/commonMain/kotlin/presentation/screens/main/navigation/MainScreenNavigation.kt +++ b/shared/src/commonMain/kotlin/presentation/screens/main/navigation/MainScreenNavigation.kt @@ -11,16 +11,25 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow +import com.mmk.kmprevenuecat.purchases.ui.Paywall import data.repository.UserRepository +import domain.model.User +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import org.koin.compose.koinInject +import presentation.components.MyAppCircularProgressIndicator import presentation.screens.about.AboutScreen import presentation.screens.account.profile.ProfileScreen import presentation.screens.account.profile.ProfileUiStateHolder @@ -134,31 +143,46 @@ interface MainScreenDestination { val navigator = LocalNavigator.currentOrThrow val userRepository = koinInject() - val currentUser by userRepository.currentUser.collectAsState(null) - if (currentUser == null) { - SignInScreen( - onNavigatePrivacyPolicy = { - navigator.navigate( - WebView( - url = Strings.url_privacy_policy, - title = Strings.privacy_policy + val currentUserState by userRepository.currentUser + .map { Pair(false, it) } + .collectAsState(Pair(true, null)) + val (isLoading, currentUser) = currentUserState + if (isLoading) Box(modifier = Modifier.fillMaxSize()) { + MyAppCircularProgressIndicator() + } + when { + isLoading -> { + Box(modifier = Modifier.fillMaxSize()) { + MyAppCircularProgressIndicator() + } + } + + isLoading.not() && currentUser != null -> { + val uiStateHolder = getUiStateHolder() + ProfileScreen(uiStateHolder = uiStateHolder) + } + + else -> { + SignInScreen( + onNavigatePrivacyPolicy = { + navigator.navigate( + WebView( + url = Strings.url_privacy_policy, + title = Strings.privacy_policy + ) ) - ) - }, - onNavigateTermsConditions = { - navigator.navigate( - WebView( - url = Strings.url_terms_conditions, - title = Strings.terms_conditions + }, + onNavigateTermsConditions = { + navigator.navigate( + WebView( + url = Strings.url_terms_conditions, + title = Strings.terms_conditions + ) ) - ) - } - ) - } else { - val uiStateHolder = getUiStateHolder() - ProfileScreen(uiStateHolder = uiStateHolder) + } + ) + } } - } override fun getTitle(): String = Strings.title_screen_account diff --git a/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt b/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt index dd32ca1..bf769ea 100644 --- a/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt +++ b/shared/src/commonMain/kotlin/presentation/theme/strings/Strings.kt @@ -1,6 +1,7 @@ package presentation.theme.strings object Strings { + const val subscription_lifetime: String = "Lifetime" const val btn_update_profile: String = "UPDATE PROFILE" const val btn_cancel: String = "Cancel" const val btn_delete: String = "Delete" @@ -10,7 +11,13 @@ object Strings { const val display_name_default: String = "Traveller" const val btn_delete_account: String = "Delete Account" const val display_name_title: String = "Display Name" + const val subscription_plan_title: String = "Subscription Plan" + const val subscription_plan_expiration_date: String = "Expiration Date" + const val subscription_plan_free: String = "Free" + const val subscription_plan_premium: String = "Premium" + const val btn_get_premium_subscription_plan_free: String = "Get Premium access" const val basic_info: String = "Basic Info" + const val subscription_info: String = "Subscription Info" const val edit_profile: String = "Edit Profile" const val btn_log_out: String = "LOG OUT" const val btn_sign_in_with_github: String = "Continue with Github" diff --git a/shared/src/commonMain/kotlin/root/AppInitializer.kt b/shared/src/commonMain/kotlin/root/AppInitializer.kt index 42f66ca..bc21d34 100644 --- a/shared/src/commonMain/kotlin/root/AppInitializer.kt +++ b/shared/src/commonMain/kotlin/root/AppInitializer.kt @@ -3,11 +3,15 @@ package root import com.mmk.kmpauth.google.GoogleAuthCredentials import com.mmk.kmpauth.google.GoogleAuthProvider import com.mmk.kmpnotifier.notification.NotifierManager +import com.mmk.kmprevenuecat.purchases.LogLevel +import com.mmk.kmprevenuecat.purchases.Purchases import di.appModules import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.koin.core.KoinApplication import org.koin.core.context.startKoin +import secrets.BuildConfig +import util.isAndroid import util.logging.AppLogger object AppInitializer { @@ -20,6 +24,9 @@ object AppInitializer { } if (isDebug) AppLogger.initialize() + + Purchases.logLevel = LogLevel.DEBUG + Purchases.configure(if (isAndroid()) BuildConfig.REVENUECAT_API_KEY_ANDROID else BuildConfig.REVENUECAT_API_KEY_IOS) } private fun KoinApplication.onApplicationStart() { diff --git a/shared/src/commonMain/kotlin/util/PlatformSpecific.kt b/shared/src/commonMain/kotlin/util/PlatformSpecific.kt index acb39e4..11835c9 100644 --- a/shared/src/commonMain/kotlin/util/PlatformSpecific.kt +++ b/shared/src/commonMain/kotlin/util/PlatformSpecific.kt @@ -21,3 +21,5 @@ interface AppVersion { fun name(): String } +expect fun isAndroid():Boolean + diff --git a/shared/src/iosMain/kotlin/util/PlatformSpecific.kt b/shared/src/iosMain/kotlin/util/PlatformSpecific.kt index 73970a8..bcd6ee0 100644 --- a/shared/src/iosMain/kotlin/util/PlatformSpecific.kt +++ b/shared/src/iosMain/kotlin/util/PlatformSpecific.kt @@ -40,7 +40,8 @@ internal class IosAppVersion : AppVersion { }.getOrDefault("") - private fun getInfoDictionary() = NSBundle.mainBundle.infoDictionary } + +actual fun isAndroid() = false \ No newline at end of file