diff --git a/apiTester/src/androidMain/kotlin/com/revenuecat/purchases/kmp/apitester/PurchasesErrorCodeAPI.kt b/apiTester/src/androidMain/kotlin/com/revenuecat/purchases/kmp/apitester/PurchasesErrorCodeAPI.kt index c5d1b247..66ee9454 100644 --- a/apiTester/src/androidMain/kotlin/com/revenuecat/purchases/kmp/apitester/PurchasesErrorCodeAPI.kt +++ b/apiTester/src/androidMain/kotlin/com/revenuecat/purchases/kmp/apitester/PurchasesErrorCodeAPI.kt @@ -42,7 +42,10 @@ private class PurchasesErrorCodeAPI { PurchasesErrorCode.InvalidPromotionalOfferError, PurchasesErrorCode.OfflineConnectionError, PurchasesErrorCode.SignatureVerificationError, - PurchasesErrorCode.FeatureNotAvailableInCustomEntitlementsComputationMode -> { + PurchasesErrorCode.FeatureNotAvailableInCustomEntitlementsComputationMode, + PurchasesErrorCode.InvalidWebPurchaseToken, + PurchasesErrorCode.PurchaseBelongsToOtherUser, + PurchasesErrorCode.ExpiredWebPurchaseToken -> { } }.exhaustive } diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index f0692533..17e2f1f8 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -19,6 +19,13 @@ + + + + + + + diff --git a/composeApp/src/androidMain/kotlin/com/revenuecat/purchases/kmp/sample/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/revenuecat/purchases/kmp/sample/MainActivity.kt index 7ddc8892..4edb19c8 100644 --- a/composeApp/src/androidMain/kotlin/com/revenuecat/purchases/kmp/sample/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/revenuecat/purchases/kmp/sample/MainActivity.kt @@ -1,23 +1,35 @@ package com.revenuecat.purchases.kmp.sample +import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import org.jetbrains.compose.ui.tooling.preview.Preview class MainActivity : ComponentActivity() { + private var urlString by mutableStateOf(null) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + urlString = intent.dataString setContent { - App() + App(urlString = urlString, urlProcessed = { urlString = null }) } } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + urlString = intent.dataString + } } @Preview @Composable fun AppAndroidPreview() { - App() + App(urlString = null, urlProcessed = {}) } diff --git a/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/App.kt b/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/App.kt index 7df6d1ac..0d917d15 100644 --- a/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/App.kt +++ b/composeApp/src/commonMain/kotlin/com/revenuecat/purchases/kmp/sample/App.kt @@ -3,8 +3,11 @@ package com.revenuecat.purchases.kmp.sample import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -17,13 +20,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.kmp.Purchases +import com.revenuecat.purchases.kmp.models.RedeemWebPurchaseListener import com.revenuecat.purchases.kmp.ui.revenuecatui.Paywall import com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallFooter import com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallOptions @Composable -fun App() { +fun App(urlString: String?, urlProcessed: () -> Unit) { MaterialTheme { + if (urlString != null) { + ProcessDeepLink(urlString, urlProcessed) + } Column( modifier = Modifier .fillMaxSize(), @@ -92,3 +101,47 @@ private fun CustomPaywallContent( } } } + +@Composable +private fun ProcessDeepLink(urlString: String, urlProcessed: () -> Unit) { + var alertMessage by remember { mutableStateOf(null) } + + val webPurchaseRedemption = Purchases.parseAsWebPurchaseRedemption(urlString) + if (webPurchaseRedemption != null && Purchases.isConfigured) { + Purchases.sharedInstance.redeemWebPurchase(webPurchaseRedemption) { result -> + alertMessage = when (result) { + is RedeemWebPurchaseListener.Result.Error -> + "Error redeeming web purchase: ${result.error.message}" + is RedeemWebPurchaseListener.Result.Expired -> + "Web purchase redemption token expired. Email sent to: ${result.obfuscatedEmail}" + RedeemWebPurchaseListener.Result.InvalidToken -> + "Invalid web purchase redemption token" + RedeemWebPurchaseListener.Result.PurchaseBelongsToOtherUser -> + "Web purchase belongs to another user" + is RedeemWebPurchaseListener.Result.Success -> + "Web purchase redeemed successfully. Entitlements: ${result.customerInfo.entitlements.active}" + } + } + } + + alertMessage?.let { + AlertDialog( + onDismissRequest = { urlProcessed() }, + title = { Text("Web Purchase Redemption result") }, + text = { Text(it) }, + buttons = { + Row( + modifier = Modifier. padding(all = 8.dp), + horizontalArrangement = Arrangement. Center + ) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { urlProcessed() } + ) { + Text("Dismiss") + } + } + } + ) + } +} 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 8f37feb4..8925ffce 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 @@ -1,10 +1,14 @@ package com.revenuecat.purchases.kmp +import android.content.Intent +import android.net.Uri +import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI import com.revenuecat.purchases.PurchaseParams import com.revenuecat.purchases.common.PlatformInfo import com.revenuecat.purchases.getCustomerInfoWith import com.revenuecat.purchases.getOfferingsWith import com.revenuecat.purchases.getProductsWith +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 @@ -22,6 +26,7 @@ import com.revenuecat.purchases.kmp.mappings.toPurchasesError import com.revenuecat.purchases.kmp.mappings.toStore import com.revenuecat.purchases.kmp.mappings.toStoreProduct import com.revenuecat.purchases.kmp.mappings.toStoreTransaction +import com.revenuecat.purchases.kmp.mappings.toWebPurchaseResult import com.revenuecat.purchases.kmp.models.BillingFeature import com.revenuecat.purchases.kmp.models.CacheFetchPolicy import com.revenuecat.purchases.kmp.models.CustomerInfo @@ -32,6 +37,7 @@ 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 @@ -39,6 +45,7 @@ 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.SubscriptionOption +import com.revenuecat.purchases.kmp.models.WebPurchaseRedemption import com.revenuecat.purchases.kmp.strings.ConfigureStrings import com.revenuecat.purchases.logInWith import com.revenuecat.purchases.logOutWith @@ -128,6 +135,14 @@ public actual class Purchases private constructor(private val androidPurchases: private fun DangerousSettings.toAndroidDangerousSettings(): AndroidDangerousSettings = AndroidDangerousSettings(autoSyncPurchases) + + public actual fun parseAsWebPurchaseRedemption(url: String): WebPurchaseRedemption? { + return if (isWebPurchaseRedemptionURL(url)) { + WebPurchaseRedemption(url) + } else { + null + } + } } public actual val appUserID: String by androidPurchases::appUserID @@ -417,6 +432,38 @@ public actual class Purchases private constructor(private val androidPurchases: public actual fun setCreative(creative: String?): Unit = androidPurchases.setCreative(creative) + @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) + public actual fun redeemWebPurchase( + webPurchaseRedemption: WebPurchaseRedemption, + listener: RedeemWebPurchaseListener + ) { + val respondInvalidUrlError = { + listener.handleResult( + RedeemWebPurchaseListener.Result.Error( + PurchasesError( + PurchasesErrorCode.ConfigurationError, + "Invalid URL: ${webPurchaseRedemption.redemptionUrl}" + ) + )) + } + val nativeWebPurchaseRedemption = try { + // Replace this with parseAsWebPurchaseRedemption overload + // accepting strings once it's available. + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(webPurchaseRedemption.redemptionUrl)) + AndroidPurchases.parseAsWebPurchaseRedemption(intent) + } catch (@Suppress("TooGenericExceptionCaught") e: Throwable) { + respondInvalidUrlError() + return + } + if (nativeWebPurchaseRedemption == null) { + respondInvalidUrlError() + return + } + androidPurchases.redeemWebPurchase(nativeWebPurchaseRedemption) { result -> + listener.handleResult(result.toWebPurchaseResult()) + } + } + private fun StoreMessageType.toInAppMessageTypeOrNull(): InAppMessageType? = when (this) { StoreMessageType.BILLING_ISSUES -> InAppMessageType.BILLING_ISSUES diff --git a/core/src/commonMain/kotlin/com/revenuecat/purchases/kmp/Purchases.kt b/core/src/commonMain/kotlin/com/revenuecat/purchases/kmp/Purchases.kt index 66459d99..feb5cc99 100644 --- a/core/src/commonMain/kotlin/com/revenuecat/purchases/kmp/Purchases.kt +++ b/core/src/commonMain/kotlin/com/revenuecat/purchases/kmp/Purchases.kt @@ -8,6 +8,7 @@ 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.RedeemWebPurchaseListener import com.revenuecat.purchases.kmp.models.ReplacementMode import com.revenuecat.purchases.kmp.models.Store import com.revenuecat.purchases.kmp.models.StoreMessageType @@ -15,6 +16,7 @@ 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.SubscriptionOption +import com.revenuecat.purchases.kmp.models.WebPurchaseRedemption import kotlin.jvm.JvmSynthetic /** @@ -97,6 +99,13 @@ public expect class Purchases { features: List = listOf(), callback: (Boolean) -> Unit, ) + + /** + * Given a url string, parses the link and returns a [WebPurchaseRedemption], which can + * be used to redeem a web purchase using [Purchases.redeemWebPurchase] + * @return A parsed version of the link or null if it's not a valid RevenueCat web purchase redemption link. + */ + public fun parseAsWebPurchaseRedemption(url: String): WebPurchaseRedemption? } /** @@ -650,6 +659,14 @@ public expect class Purchases { */ public fun setCreative(creative: String?) + /** + * Redeem a web purchase using a [WebPurchaseRedemption] object obtained + * through [Purchases.parseAsWebPurchaseRedemption]. + */ + public fun redeemWebPurchase( + webPurchaseRedemption: WebPurchaseRedemption, + listener: RedeemWebPurchaseListener, + ) } /** 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 4549f8d3..398668ab 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 @@ -4,7 +4,10 @@ import cocoapods.PurchasesHybridCommon.RCCommonFunctionality import cocoapods.PurchasesHybridCommon.RCPurchasesDelegateProtocol import cocoapods.PurchasesHybridCommon.RCStoreProduct import cocoapods.PurchasesHybridCommon.configureWithAPIKey +import cocoapods.PurchasesHybridCommon.isWebPurchaseRedemptionURL +import cocoapods.PurchasesHybridCommon.parseAsWebPurchaseRedemptionWithUrlString import cocoapods.PurchasesHybridCommon.recordPurchaseForProductID +import cocoapods.PurchasesHybridCommon.redeemWebPurchaseWithUrlString import cocoapods.PurchasesHybridCommon.setAirshipChannelID import cocoapods.PurchasesHybridCommon.setOnesignalUserID import cocoapods.PurchasesHybridCommon.showStoreMessagesForTypes @@ -30,6 +33,7 @@ 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 @@ -37,6 +41,7 @@ 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.SubscriptionOption +import com.revenuecat.purchases.kmp.models.WebPurchaseRedemption import com.revenuecat.purchases.kmp.strings.ConfigureStrings import platform.Foundation.NSURL import cocoapods.PurchasesHybridCommon.RCDangerousSettings as IosDangerousSettings @@ -113,6 +118,19 @@ public actual class Purchases private constructor(private val iosPurchases: IosP private fun DangerousSettings.toIosDangerousSettings(): IosDangerousSettings = IosDangerousSettings(autoSyncPurchases) + + /** + * Given a url string, parses the link and returns a [WebPurchaseRedemption], which can + * be used to redeem a web purchase using [Purchases.redeemWebPurchase] + * @return A parsed version of the link or null if it's not a valid RevenueCat web purchase redemption link. + */ + public actual fun parseAsWebPurchaseRedemption(url: String): WebPurchaseRedemption? { + return if (RCCommonFunctionality.isWebPurchaseRedemptionURL(url)) { + WebPurchaseRedemption(url) + } else { + null + } + } } public actual val appUserID: String @@ -446,4 +464,59 @@ public actual class Purchases private constructor(private val iosPurchases: IosP public actual fun setCreative(creative: String?): Unit = iosPurchases.setCreative(creative) + + public actual fun redeemWebPurchase( + webPurchaseRedemption: WebPurchaseRedemption, + listener: RedeemWebPurchaseListener, + ) { + val nativeWebPurchaseRedemption = RCCommonFunctionality.parseAsWebPurchaseRedemptionWithUrlString( + webPurchaseRedemption.redemptionUrl, + ) + if (nativeWebPurchaseRedemption == null) { + listener.handleResult(RedeemWebPurchaseListener.Result.Error( + PurchasesError( + code = PurchasesErrorCode.ConfigurationError, + underlyingErrorMessage = "Invalid web purchase redemption URL." + ) + )) + return + } + iosPurchases.redeemWebPurchaseWithWebPurchaseRedemption( + nativeWebPurchaseRedemption, + ) { rcCustomerInfo, nsError -> + if (nsError != null) { + val errorCode = nsError.code.toInt() + val result = when (errorCode) { + PurchasesErrorCode.InvalidWebPurchaseToken.code -> + RedeemWebPurchaseListener.Result.InvalidToken + PurchasesErrorCode.PurchaseBelongsToOtherUser.code -> + RedeemWebPurchaseListener.Result.PurchaseBelongsToOtherUser + PurchasesErrorCode.ExpiredWebPurchaseToken.code -> + RedeemWebPurchaseListener.Result.Expired( + nsError.userInfo["rc_obfuscated_email"] as String? ?: "" + ) + else -> + RedeemWebPurchaseListener.Result.Error(nsError.toPurchasesErrorOrThrow()) + } + listener.handleResult(result) + return@redeemWebPurchaseWithWebPurchaseRedemption + } + if (rcCustomerInfo == null) { + listener.handleResult( + RedeemWebPurchaseListener.Result.Error( + PurchasesError( + code = PurchasesErrorCode.UnknownError, + underlyingErrorMessage = "Expected a non-null RCCustomerInfo when error is null." + ) + ) + ) + return@redeemWebPurchaseWithWebPurchaseRedemption + } + listener.handleResult( + RedeemWebPurchaseListener.Result.Success( + rcCustomerInfo.toCustomerInfo() + ) + ) + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 066731fb..0c76345f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ java = "1.8" kotlin = "1.9.23" revenuecat-common = "13.15.0" revenuecat-kmp = "1.4.0-SNAPSHOT" +rinku = "1.3.1" [libraries] android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } @@ -21,6 +22,8 @@ kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version 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" } +rinku = { module = "dev.theolm:rinku", version.ref = "rinku" } +rinku-compose-ext = { module = "dev.theolm:rinku-compose-ext", version.ref = "rinku" } [plugins] adamko-dokkatoo-html = { id = "dev.adamko.dokkatoo-html", version = "2.3.1" } diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index aaf5e182..7104d971 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -18,6 +18,19 @@ $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + RevenueCat Redemption Links + CFBundleURLSchemes + + rc-39976e83d0 + + + CFBundleVersion 1 LSRequiresIPhoneOS diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 0648e860..12f635d9 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -1,10 +1,27 @@ import SwiftUI +import ComposeApp -@main -struct iOSApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} \ No newline at end of file +@UIApplicationMain +class AppDelegate: NSObject, UIApplicationDelegate { + var window: UIWindow? + let rinku = RinkuIos.init(deepLinkFilter: nil, deepLinkMapper: nil) + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + self.window = UIWindow(frame: UIScreen.main.bounds) + let mainViewController = UIHostingController(rootView: ContentView()) + self.window!.rootViewController = mainViewController + self.window!.makeKeyAndVisible() + + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + rinku.onDeepLinkReceived(url: url.absoluteString) + return true + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + rinku.onDeepLinkReceived(userActivity: userActivity) + return true + } +} diff --git a/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/RedeemWebPurchaseListener.android.kt b/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/RedeemWebPurchaseListener.android.kt new file mode 100644 index 00000000..7064fc2a --- /dev/null +++ b/mappings/src/androidMain/kotlin/com/revenuecat/purchases/kmp/mappings/RedeemWebPurchaseListener.android.kt @@ -0,0 +1,21 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI +import com.revenuecat.purchases.kmp.models.RedeemWebPurchaseListener +import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener as NativeRedeemWebPurchaseListener + +@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) +public fun NativeRedeemWebPurchaseListener.Result.toWebPurchaseResult(): RedeemWebPurchaseListener.Result { + return when (this) { + is NativeRedeemWebPurchaseListener.Result.Success -> + RedeemWebPurchaseListener.Result.Success(customerInfo.toCustomerInfo()) + is NativeRedeemWebPurchaseListener.Result.Error -> + RedeemWebPurchaseListener.Result.Error(error.toPurchasesError()) + NativeRedeemWebPurchaseListener.Result.InvalidToken -> + RedeemWebPurchaseListener.Result.InvalidToken + is NativeRedeemWebPurchaseListener.Result.Expired -> + RedeemWebPurchaseListener.Result.Expired(obfuscatedEmail) + NativeRedeemWebPurchaseListener.Result.PurchaseBelongsToOtherUser -> + RedeemWebPurchaseListener.Result.PurchaseBelongsToOtherUser + } +} diff --git a/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/RedeemWebPurchaseListener.kt b/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/RedeemWebPurchaseListener.kt new file mode 100644 index 00000000..4a55141c --- /dev/null +++ b/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/RedeemWebPurchaseListener.kt @@ -0,0 +1,55 @@ +package com.revenuecat.purchases.kmp.models + +/** + * Interface to handle the redemption of a RevenueCat Web purchase. + */ +public fun interface RedeemWebPurchaseListener { + /** + * Result of the redemption of a RevenueCat Web purchase. + */ + public sealed class Result { + /** + * Indicates that the web purchase was redeemed successfully. + */ + public data class Success(val customerInfo: CustomerInfo) : Result() + + /** + * Indicates that an unknown error occurred during the redemption. + */ + public data class Error(val error: PurchasesError) : Result() + + /** + * Indicates that the redemption token is invalid. + */ + public object InvalidToken : Result() + + /** + * Indicates that the redemption token has expired. An email with a new redemption token + * might be sent if a new one wasn't already sent recently. + * The email where it will be sent is indicated by the [obfuscatedEmail]. + */ + public data class Expired(val obfuscatedEmail: String) : Result() + + /** + * Indicates that the redemption couldn't be performed because the purchase belongs to a different user. + */ + public object PurchaseBelongsToOtherUser : Result() + + /** + * Whether the redemption was successful or not. + */ + public val isSuccess: Boolean + get() = when (this) { + is Success -> true + is Error -> false + InvalidToken -> false + is Expired -> false + PurchaseBelongsToOtherUser -> false + } + } + + /** + * Called when a RevenueCat Web purchase redemption finishes with the result of the operation. + */ + public fun handleResult(result: Result) +} diff --git a/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/WebPurchaseRedemption.kt b/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/WebPurchaseRedemption.kt new file mode 100644 index 00000000..7b72103b --- /dev/null +++ b/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/WebPurchaseRedemption.kt @@ -0,0 +1,8 @@ +package com.revenuecat.purchases.kmp.models + +/** + * Represents a web redemption link, that can be redeemed using [Purchases.redeemWebPurchase] + */ +public class WebPurchaseRedemption( + public val redemptionUrl: String, +) diff --git a/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/errors.kt b/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/errors.kt index 33aba020..b2ecef3b 100644 --- a/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/errors.kt +++ b/models/src/commonMain/kotlin/com/revenuecat/purchases/kmp/models/errors.kt @@ -96,6 +96,18 @@ public enum class PurchasesErrorCode(public val code: Int, public val descriptio ), FeatureNotAvailableInCustomEntitlementsComputationMode( 37, - "This feature is not available when utilizing the customEntitlementsComputation dangerousSetting." + "This feature is not available when utilizing the customEntitlementsComputation dangerousSetting.", + ), + InvalidWebPurchaseToken( + 39, + "The link you provided does not contain a valid purchase token.", + ), + PurchaseBelongsToOtherUser( + 40, + "The web purchase already belongs to other user.", + ), + ExpiredWebPurchaseToken( + 41, + "The link you provided has expired. A new one will be sent to the email used to make the purchase.", ) }