diff --git a/apiTester/src/iosMain/kotlin/com/revenuecat/purchases/kmp/apitester/CustomerInfoBuilderAPI.kt b/apiTester/src/iosMain/kotlin/com/revenuecat/purchases/kmp/apitester/CustomerInfoBuilderAPI.kt new file mode 100644 index 000000000..4e9a75d5a --- /dev/null +++ b/apiTester/src/iosMain/kotlin/com/revenuecat/purchases/kmp/apitester/CustomerInfoBuilderAPI.kt @@ -0,0 +1,11 @@ +package com.revenuecat.purchases.kmp.apitester + +import com.revenuecat.purchases.kmp.mappings.buildCustomerInfo +import com.revenuecat.purchases.kmp.models.CustomerInfo + +@Suppress("unused", "UNUSED_VARIABLE") +private class CustomerInfoBuilderAPI { + fun check(customerInfoMap: Map) { + val result: Result = buildCustomerInfo(customerInfoMap) + } +} diff --git a/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/CustomerInfoBuilder.ios.kt b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/CustomerInfoBuilder.ios.kt new file mode 100644 index 000000000..d21474b14 --- /dev/null +++ b/mappings/src/iosMain/kotlin/com/revenuecat/purchases/kmp/mappings/CustomerInfoBuilder.ios.kt @@ -0,0 +1,174 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.models.CustomerInfo +import com.revenuecat.purchases.kmp.models.EntitlementInfo +import com.revenuecat.purchases.kmp.models.EntitlementInfos +import com.revenuecat.purchases.kmp.models.OwnershipType +import com.revenuecat.purchases.kmp.models.PeriodType +import com.revenuecat.purchases.kmp.models.Store +import com.revenuecat.purchases.kmp.models.SubscriptionInfo +import com.revenuecat.purchases.kmp.models.Transaction +import com.revenuecat.purchases.kmp.models.VerificationResult + +/** + * Builds a [CustomerInfo] from a PHC dictionary representation. + * This is used when receiving CustomerInfo from the dictionary-based + * [PaywallViewControllerDelegateWrapper] protocol. + */ +public fun buildCustomerInfo(customerInfoMap: Map): Result = runCatching { + @Suppress("UNCHECKED_CAST") + val activeSubscriptions = (customerInfoMap["activeSubscriptions"] as? List<*>) + ?.filterIsInstance()?.toSet() ?: emptySet() + + @Suppress("UNCHECKED_CAST") + val allPurchasedProductIdentifiers = (customerInfoMap["allPurchasedProductIdentifiers"] as? List<*>) + ?.filterIsInstance()?.toSet() ?: emptySet() + + val allExpirationDateMillis = buildMillisMap(customerInfoMap["allExpirationDatesMillis"]) + val allPurchaseDateMillis = buildMillisMap(customerInfoMap["allPurchaseDatesMillis"]) + + val entitlements = buildEntitlementInfos(customerInfoMap["entitlements"]) + + val firstSeenMillis = (customerInfoMap["firstSeenMillis"] as? Number)?.toLong() + ?: error("Expected a non-null firstSeenMillis") + + val latestExpirationDateMillis = (customerInfoMap["latestExpirationDateMillis"] as? Number)?.toLong() + val managementUrlString = customerInfoMap["managementURL"] as? String + val originalAppUserId = customerInfoMap["originalAppUserId"] as? String + ?: error("Expected a non-null originalAppUserId") + val originalApplicationVersion = customerInfoMap["originalApplicationVersion"] as? String + val originalPurchaseDateMillis = (customerInfoMap["originalPurchaseDateMillis"] as? Number)?.toLong() + val requestDateMillis = (customerInfoMap["requestDateMillis"] as? Number)?.toLong() + ?: error("Expected a non-null requestDateMillis") + + @Suppress("UNCHECKED_CAST") + val subscriptionsByProductIdentifier = + (customerInfoMap["subscriptionsByProductIdentifier"] as? Map>) + ?.mapValues { (_, v) -> buildSubscriptionInfoFromDict(v) } ?: emptyMap() + + @Suppress("UNCHECKED_CAST") + val nonSubscriptionTransactions = + (customerInfoMap["nonSubscriptionTransactions"] as? List>) + ?.map { buildTransactionFromDict(it) } ?: emptyList() + + CustomerInfo( + activeSubscriptions = activeSubscriptions, + allExpirationDateMillis = allExpirationDateMillis, + allPurchaseDateMillis = allPurchaseDateMillis, + allPurchasedProductIdentifiers = allPurchasedProductIdentifiers, + entitlements = entitlements, + firstSeenMillis = firstSeenMillis, + latestExpirationDateMillis = latestExpirationDateMillis, + managementUrlString = managementUrlString, + subscriptionsByProductIdentifier = subscriptionsByProductIdentifier, + nonSubscriptionTransactions = nonSubscriptionTransactions, + originalAppUserId = originalAppUserId, + originalApplicationVersion = originalApplicationVersion, + originalPurchaseDateMillis = originalPurchaseDateMillis, + requestDateMillis = requestDateMillis, + ) +} + +private fun buildMillisMap(raw: Any?): Map { + @Suppress("UNCHECKED_CAST") + val map = raw as? Map ?: return emptyMap() + return map.mapValues { (_, v) -> (v as? Number)?.toLong() } +} + +@Suppress("UNCHECKED_CAST") +private fun buildEntitlementInfos(raw: Any?): EntitlementInfos { + val map = raw as? Map ?: return EntitlementInfos( + all = emptyMap(), + verification = VerificationResult.NOT_REQUESTED, + ) + val allMap = (map["all"] as? Map>) ?: emptyMap() + val all = allMap.mapValues { (_, v) -> buildEntitlementInfoFromDict(v) } + val verification = parseVerificationResult(map["verification"] as? String) + return EntitlementInfos(all = all, verification = verification) +} + +private fun buildEntitlementInfoFromDict(map: Map): EntitlementInfo = + EntitlementInfo( + identifier = map["identifier"] as? String ?: "", + isActive = map["isActive"] as? Boolean ?: false, + willRenew = map["willRenew"] as? Boolean ?: false, + periodType = parsePeriodType(map["periodType"] as? String), + latestPurchaseDateMillis = (map["latestPurchaseDateMillis"] as? Number)?.toLong(), + originalPurchaseDateMillis = (map["originalPurchaseDateMillis"] as? Number)?.toLong(), + expirationDateMillis = (map["expirationDateMillis"] as? Number)?.toLong(), + store = parseStore(map["store"] as? String), + productIdentifier = map["productIdentifier"] as? String ?: "", + productPlanIdentifier = map["productPlanIdentifier"] as? String, + isSandbox = map["isSandbox"] as? Boolean ?: false, + unsubscribeDetectedAtMillis = (map["unsubscribeDetectedAtMillis"] as? Number)?.toLong(), + billingIssueDetectedAtMillis = (map["billingIssueDetectedAtMillis"] as? Number)?.toLong(), + ownershipType = parseOwnershipType(map["ownershipType"] as? String), + verification = parseVerificationResult(map["verification"] as? String), + ) + +private fun buildSubscriptionInfoFromDict(map: Map): SubscriptionInfo = + SubscriptionInfo( + productIdentifier = map["productIdentifier"] as? String ?: "", + purchaseDateMillis = (map["purchaseDateMillis"] as? Number)?.toLong() + ?: error("Expected a non-null purchaseDateMillis in subscriptionInfo"), + originalPurchaseDateMillis = (map["originalPurchaseDateMillis"] as? Number)?.toLong(), + expiresDateMillis = (map["expiresDateMillis"] as? Number)?.toLong(), + store = parseStore(map["store"] as? String), + isSandbox = map["isSandbox"] as? Boolean ?: false, + unsubscribeDetectedAtMillis = (map["unsubscribeDetectedAtMillis"] as? Number)?.toLong(), + billingIssuesDetectedAtMillis = (map["billingIssuesDetectedAtMillis"] as? Number)?.toLong(), + gracePeriodExpiresDateMillis = (map["gracePeriodExpiresDateMillis"] as? Number)?.toLong(), + ownershipType = parseOwnershipType(map["ownershipType"] as? String), + periodType = parsePeriodType(map["periodType"] as? String), + refundedAtMillis = (map["refundedAtMillis"] as? Number)?.toLong(), + storeTransactionId = map["storeTransactionId"] as? String, + autoResumeDateMillis = null, + price = null, // Price is not reliably available from PHC dictionary format + productPlanIdentifier = null, + managementUrlString = null, + isActive = map["isActive"] as? Boolean ?: false, + willRenew = map["willRenew"] as? Boolean ?: false, + ) + +private fun buildTransactionFromDict(map: Map): Transaction = + Transaction( + transactionIdentifier = map["transactionIdentifier"] as? String ?: "", + productIdentifier = map["productIdentifier"] as? String ?: "", + purchaseDateMillis = (map["purchaseDateMillis"] as? Number)?.toLong() ?: 0L, + ) + +internal fun parseStore(value: String?): Store = when (value) { + "APP_STORE" -> Store.APP_STORE + "MAC_APP_STORE" -> Store.MAC_APP_STORE + "PLAY_STORE" -> Store.PLAY_STORE + "STRIPE" -> Store.STRIPE + "PROMOTIONAL" -> Store.PROMOTIONAL + "AMAZON" -> Store.AMAZON + "RC_BILLING" -> Store.RC_BILLING + "EXTERNAL" -> Store.EXTERNAL + "PADDLE" -> Store.PADDLE + "TEST_STORE" -> Store.TEST_STORE + "GALAXY" -> Store.GALAXY + else -> Store.UNKNOWN_STORE +} + +internal fun parsePeriodType(value: String?): PeriodType = when (value) { + "NORMAL" -> PeriodType.NORMAL + "INTRO" -> PeriodType.INTRO + "TRIAL" -> PeriodType.TRIAL + "PREPAID" -> PeriodType.PREPAID + else -> PeriodType.NORMAL +} + +internal fun parseOwnershipType(value: String?): OwnershipType = when (value) { + "PURCHASED" -> OwnershipType.PURCHASED + "FAMILY_SHARED" -> OwnershipType.FAMILY_SHARED + else -> OwnershipType.UNKNOWN +} + +internal fun parseVerificationResult(value: String?): VerificationResult = when (value) { + "VERIFIED" -> VerificationResult.VERIFIED + "VERIFIED_ON_DEVICE" -> VerificationResult.VERIFIED_ON_DEVICE + "FAILED" -> VerificationResult.FAILED + else -> VerificationResult.NOT_REQUESTED +} diff --git a/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/CustomerInfoBuilderTests.ios.kt b/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/CustomerInfoBuilderTests.ios.kt new file mode 100644 index 000000000..c492266cc --- /dev/null +++ b/mappings/src/iosTest/kotlin/com/revenuecat/purchases/kmp/mappings/CustomerInfoBuilderTests.ios.kt @@ -0,0 +1,230 @@ +package com.revenuecat.purchases.kmp.mappings + +import com.revenuecat.purchases.kmp.models.OwnershipType +import com.revenuecat.purchases.kmp.models.PeriodType +import com.revenuecat.purchases.kmp.models.Store +import com.revenuecat.purchases.kmp.models.VerificationResult +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class CustomerInfoBuilderTests { + + @Test + fun `buildCustomerInfo correctly parses a full PHC dictionary`() { + val map: Map = createFullCustomerInfoMap() + + val result = buildCustomerInfo(map) + + assertTrue(result.isSuccess) + val customerInfo = result.getOrThrow() + assertEquals(setOf("com.test.monthly"), customerInfo.activeSubscriptions) + assertEquals(setOf("com.test.monthly"), customerInfo.allPurchasedProductIdentifiers) + assertEquals("user123", customerInfo.originalAppUserId) + assertEquals(1700000000000L, customerInfo.firstSeenMillis) + assertEquals(1710000000000L, customerInfo.requestDateMillis) + assertEquals(1720000000000L, customerInfo.latestExpirationDateMillis) + assertEquals("https://apps.apple.com/manage", customerInfo.managementUrlString) + assertNull(customerInfo.originalApplicationVersion) + assertEquals(1690000000000L, customerInfo.originalPurchaseDateMillis) + } + + @Test + fun `buildCustomerInfo parses entitlements correctly`() { + val map: Map = createFullCustomerInfoMap() + + val customerInfo = buildCustomerInfo(map).getOrThrow() + + assertEquals(1, customerInfo.entitlements.all.size) + val entitlement = customerInfo.entitlements["premium"] + assertNotNull(entitlement) + assertEquals("premium", entitlement.identifier) + assertTrue(entitlement.isActive) + assertTrue(entitlement.willRenew) + assertEquals(PeriodType.NORMAL, entitlement.periodType) + assertEquals(Store.APP_STORE, entitlement.store) + assertEquals("com.test.monthly", entitlement.productIdentifier) + assertTrue(entitlement.isSandbox) + assertEquals(OwnershipType.PURCHASED, entitlement.ownershipType) + assertEquals(VerificationResult.VERIFIED, entitlement.verification) + assertEquals(1705000000000L, entitlement.latestPurchaseDateMillis) + assertEquals(1700000000000L, entitlement.originalPurchaseDateMillis) + assertEquals(1720000000000L, entitlement.expirationDateMillis) + } + + @Test + fun `buildCustomerInfo parses entitlements verification`() { + val map: Map = createFullCustomerInfoMap() + + val customerInfo = buildCustomerInfo(map).getOrThrow() + + assertEquals(VerificationResult.VERIFIED, customerInfo.entitlements.verification) + } + + @Test + fun `buildCustomerInfo parses expiration and purchase date maps`() { + val map: Map = createFullCustomerInfoMap() + + val customerInfo = buildCustomerInfo(map).getOrThrow() + + assertEquals(1720000000000L, customerInfo.allExpirationDateMillis["com.test.monthly"]) + assertEquals(1705000000000L, customerInfo.allPurchaseDateMillis["com.test.monthly"]) + } + + @Test + fun `buildCustomerInfo parses non-subscription transactions`() { + val map: Map = createFullCustomerInfoMap() + + val customerInfo = buildCustomerInfo(map).getOrThrow() + + assertEquals(1, customerInfo.nonSubscriptionTransactions.size) + val transaction = customerInfo.nonSubscriptionTransactions.first() + assertEquals("txn_001", transaction.transactionIdentifier) + assertEquals("com.test.consumable", transaction.productIdentifier) + assertEquals(1706000000000L, transaction.purchaseDateMillis) + } + + @Test + fun `buildCustomerInfo handles empty map with required fields only`() { + val map: Map = mapOf( + "originalAppUserId" to "user_minimal", + "firstSeenMillis" to 1700000000000.0, + "requestDateMillis" to 1710000000000.0, + ) + + val result = buildCustomerInfo(map) + + assertTrue(result.isSuccess) + val customerInfo = result.getOrThrow() + assertEquals("user_minimal", customerInfo.originalAppUserId) + assertEquals(emptySet(), customerInfo.activeSubscriptions) + assertEquals(emptyMap(), customerInfo.allExpirationDateMillis) + assertEquals(0, customerInfo.entitlements.all.size) + assertEquals(emptyList(), customerInfo.nonSubscriptionTransactions) + } + + @Test + fun `buildCustomerInfo fails when missing originalAppUserId`() { + val map: Map = mapOf( + "firstSeenMillis" to 1700000000000.0, + "requestDateMillis" to 1710000000000.0, + ) + + val result = buildCustomerInfo(map) + + assertTrue(result.isFailure) + } + + @Test + fun `buildCustomerInfo fails when missing firstSeenMillis`() { + val map: Map = mapOf( + "originalAppUserId" to "user123", + "requestDateMillis" to 1710000000000.0, + ) + + val result = buildCustomerInfo(map) + + assertTrue(result.isFailure) + } + + @Test + fun `buildCustomerInfo fails when missing requestDateMillis`() { + val map: Map = mapOf( + "originalAppUserId" to "user123", + "firstSeenMillis" to 1700000000000.0, + ) + + val result = buildCustomerInfo(map) + + assertTrue(result.isFailure) + } + + @Test + fun `parseStore maps all known store strings`() { + assertEquals(Store.APP_STORE, parseStore("APP_STORE")) + assertEquals(Store.MAC_APP_STORE, parseStore("MAC_APP_STORE")) + assertEquals(Store.PLAY_STORE, parseStore("PLAY_STORE")) + assertEquals(Store.STRIPE, parseStore("STRIPE")) + assertEquals(Store.PROMOTIONAL, parseStore("PROMOTIONAL")) + assertEquals(Store.AMAZON, parseStore("AMAZON")) + assertEquals(Store.RC_BILLING, parseStore("RC_BILLING")) + assertEquals(Store.EXTERNAL, parseStore("EXTERNAL")) + assertEquals(Store.PADDLE, parseStore("PADDLE")) + assertEquals(Store.TEST_STORE, parseStore("TEST_STORE")) + assertEquals(Store.GALAXY, parseStore("GALAXY")) + assertEquals(Store.UNKNOWN_STORE, parseStore("SOMETHING_ELSE")) + assertEquals(Store.UNKNOWN_STORE, parseStore(null)) + } + + @Test + fun `parsePeriodType maps all known period type strings`() { + assertEquals(PeriodType.NORMAL, parsePeriodType("NORMAL")) + assertEquals(PeriodType.INTRO, parsePeriodType("INTRO")) + assertEquals(PeriodType.TRIAL, parsePeriodType("TRIAL")) + assertEquals(PeriodType.PREPAID, parsePeriodType("PREPAID")) + assertEquals(PeriodType.NORMAL, parsePeriodType(null)) + } + + @Test + fun `parseOwnershipType maps all known ownership type strings`() { + assertEquals(OwnershipType.PURCHASED, parseOwnershipType("PURCHASED")) + assertEquals(OwnershipType.FAMILY_SHARED, parseOwnershipType("FAMILY_SHARED")) + assertEquals(OwnershipType.UNKNOWN, parseOwnershipType("SOMETHING_ELSE")) + assertEquals(OwnershipType.UNKNOWN, parseOwnershipType(null)) + } + + @Test + fun `parseVerificationResult maps all known verification result strings`() { + assertEquals(VerificationResult.VERIFIED, parseVerificationResult("VERIFIED")) + assertEquals(VerificationResult.VERIFIED_ON_DEVICE, parseVerificationResult("VERIFIED_ON_DEVICE")) + assertEquals(VerificationResult.FAILED, parseVerificationResult("FAILED")) + assertEquals(VerificationResult.NOT_REQUESTED, parseVerificationResult("NOT_REQUESTED")) + assertEquals(VerificationResult.NOT_REQUESTED, parseVerificationResult(null)) + } + + private fun createFullCustomerInfoMap(): Map = mapOf( + "activeSubscriptions" to listOf("com.test.monthly"), + "allPurchasedProductIdentifiers" to listOf("com.test.monthly"), + "allExpirationDatesMillis" to mapOf("com.test.monthly" to 1720000000000.0), + "allPurchaseDatesMillis" to mapOf("com.test.monthly" to 1705000000000.0), + "entitlements" to mapOf( + "all" to mapOf( + "premium" to mapOf( + "identifier" to "premium", + "isActive" to true, + "willRenew" to true, + "periodType" to "NORMAL", + "latestPurchaseDateMillis" to 1705000000000.0, + "originalPurchaseDateMillis" to 1700000000000.0, + "expirationDateMillis" to 1720000000000.0, + "store" to "APP_STORE", + "productIdentifier" to "com.test.monthly", + "productPlanIdentifier" to null, + "isSandbox" to true, + "unsubscribeDetectedAtMillis" to null, + "billingIssueDetectedAtMillis" to null, + "ownershipType" to "PURCHASED", + "verification" to "VERIFIED", + ), + ), + "verification" to "VERIFIED", + ), + "firstSeenMillis" to 1700000000000.0, + "latestExpirationDateMillis" to 1720000000000.0, + "requestDateMillis" to 1710000000000.0, + "originalAppUserId" to "user123", + "originalApplicationVersion" to null, + "originalPurchaseDateMillis" to 1690000000000.0, + "managementURL" to "https://apps.apple.com/manage", + "nonSubscriptionTransactions" to listOf( + mapOf( + "transactionIdentifier" to "txn_001", + "productIdentifier" to "com.test.consumable", + "purchaseDateMillis" to 1706000000000.0, + ), + ), + "subscriptionsByProductIdentifier" to emptyMap(), + ) +} 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..7782a8e69 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 @@ -42,9 +42,10 @@ internal class IosPaywallDelegate( didFinishPurchasingWithCustomerInfo: RCCustomerInfo, transaction: RCStoreTransaction? ) { + val storeTransaction = (transaction as? PhcStoreTransaction)?.toStoreTransaction() ?: return listener?.onPurchaseCompleted( (didFinishPurchasingWithCustomerInfo as PhcCustomerInfo).toCustomerInfo(), - (transaction as PhcStoreTransaction).toStoreTransaction() + storeTransaction ) } 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 360ee46a0..be78c2333 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 @@ -6,6 +6,8 @@ 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.buildCustomerInfo +import com.revenuecat.purchases.kmp.mappings.buildStoreTransaction 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 @@ -150,6 +152,22 @@ internal class IosPaywallProxyDelegate( listener?.onPurchaseCancelled() } + @ObjCSignatureOverride + @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun paywallViewController( + controller: RCPaywallViewController, + didFinishPurchasingWithCustomerInfoDictionary: Map, + transactionDictionary: Map? + ) { + val listener = listener ?: return + val customerInfo = buildCustomerInfo(didFinishPurchasingWithCustomerInfoDictionary) + .getOrElse { return } + val storeTransaction = transactionDictionary + ?.let { buildStoreTransaction(it).getOrElse { return } } + ?: return + listener.onPurchaseCompleted(customerInfo, storeTransaction) + } + @ObjCSignatureOverride @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") override fun paywallViewController( @@ -163,6 +181,18 @@ internal class IosPaywallProxyDelegate( listener?.onRestoreStarted() } + @ObjCSignatureOverride + @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun paywallViewController( + controller: RCPaywallViewController, + didFinishRestoringWithCustomerInfoDictionary: Map + ) { + val listener = listener ?: return + val customerInfo = buildCustomerInfo(didFinishRestoringWithCustomerInfoDictionary) + .getOrElse { return } + listener.onRestoreCompleted(customerInfo) + } + @ObjCSignatureOverride @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") override fun paywallViewController(