Skip to content

Commit

Permalink
Support Web Redemption Links (#987)
Browse files Browse the repository at this point in the history
This adds support for Redemption Links in PHC which will be used by the
hybrids.

Related RN PR:
RevenueCat/react-native-purchases#1145
  • Loading branch information
tonidero authored Dec 16, 2024
1 parent 7f57043 commit 2ebba06
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 18 deletions.
2 changes: 1 addition & 1 deletion PurchasesHybridCommon.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Pod::Spec.new do |s|

s.framework = 'StoreKit'

s.dependency 'RevenueCat', '5.12.1'
s.dependency 'RevenueCat', '5.14.0'
s.swift_version = '5.7'

s.ios.deployment_target = '13.0'
Expand Down
2 changes: 1 addition & 1 deletion PurchasesHybridCommonUI.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Pod::Spec.new do |s|
s.framework = 'StoreKit'
s.framework = 'SwiftUI'

s.dependency 'RevenueCatUI', '5.12.1'
s.dependency 'RevenueCatUI', '5.14.0'
s.dependency 'PurchasesHybridCommon', s.version.to_s
s.swift_version = '5.7'

Expand Down
2 changes: 1 addition & 1 deletion android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ junit-jupiter = "5.8.2"
kotlin = "1.7.21"
junit = "4.13.2"
mockk = "1.12.8"
purchases = "8.10.3"
purchases = "8.10.5"
detekt = "1.23.0"
android-junit5-version = "1.8.2.0"
mavenPublish = "0.22.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package com.revenuecat.purchases.hybridcommon

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.revenuecat.purchases.AmazonLWAConsentStatus
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.DangerousSettings
import com.revenuecat.purchases.EntitlementVerificationMode
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.LogLevel
import com.revenuecat.purchases.PresentedOfferingContext
import com.revenuecat.purchases.ProductType
Expand All @@ -18,6 +21,7 @@ import com.revenuecat.purchases.PurchasesConfiguration
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.Store
import com.revenuecat.purchases.WebPurchaseRedemption
import com.revenuecat.purchases.common.PlatformInfo
import com.revenuecat.purchases.getAmazonLWAConsentStatusWith
import com.revenuecat.purchases.getCustomerInfoWith
Expand All @@ -26,6 +30,7 @@ import com.revenuecat.purchases.getProductsWith
import com.revenuecat.purchases.hybridcommon.mappers.LogHandlerWithMapping
import com.revenuecat.purchases.hybridcommon.mappers.MappedProductCategory
import com.revenuecat.purchases.hybridcommon.mappers.map
import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener
import com.revenuecat.purchases.logInWith
import com.revenuecat.purchases.logOutWith
import com.revenuecat.purchases.models.BillingFeature
Expand Down Expand Up @@ -600,8 +605,78 @@ fun getPromotionalOffer(): ErrorContainer {
)
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun isWebPurchaseRedemptionURL(urlString: String): Boolean {
return urlString.toWebPurchaseRedemption() != null
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun redeemWebPurchase(
urlString: String,
onResult: OnResult,
) {
val webPurchaseRedemption: WebPurchaseRedemption? = urlString.toWebPurchaseRedemption()
if (webPurchaseRedemption == null) {
onResult.onError(
ErrorContainer(
PurchasesErrorCode.UnsupportedError.code,
"Invalid URL for web purchase redemption",
emptyMap(),
),
)
return
}

Purchases.sharedInstance.redeemWebPurchase(webPurchaseRedemption) { result ->
val resultMap: MutableMap<String, Any> = mutableMapOf(
"result" to result.toResultName(),
)
when (result) {
is RedeemWebPurchaseListener.Result.Success -> {
resultMap["customerInfo"] = result.customerInfo.map()
}
is RedeemWebPurchaseListener.Result.Error -> {
resultMap["error"] = result.error.map()
}
is RedeemWebPurchaseListener.Result.Expired -> {
resultMap["obfuscatedEmail"] = result.obfuscatedEmail
}
RedeemWebPurchaseListener.Result.PurchaseBelongsToOtherUser,
RedeemWebPurchaseListener.Result.InvalidToken,
-> {
// Do nothing
}
}
onResult.onReceived(resultMap)
}
}

// region private functions

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
private fun RedeemWebPurchaseListener.Result.toResultName(): String {
return when (this) {
is RedeemWebPurchaseListener.Result.Success -> "SUCCESS"
is RedeemWebPurchaseListener.Result.Error -> "ERROR"
RedeemWebPurchaseListener.Result.PurchaseBelongsToOtherUser -> "PURCHASE_BELONGS_TO_OTHER_USER"
RedeemWebPurchaseListener.Result.InvalidToken -> "INVALID_TOKEN"
is RedeemWebPurchaseListener.Result.Expired -> "EXPIRED"
}
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
private fun String.toWebPurchaseRedemption(): WebPurchaseRedemption? {
try {
// Replace this with parseAsWebPurchaseRedemption overload
// accepting strings once it's available.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(this))
return Purchases.parseAsWebPurchaseRedemption(intent)
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) {
errorLog("Error parsing WebPurchaseRedemption from URL: $this. Error: $e")
return null
}
}

private fun String.toPurchasesAreCompletedBy(): PurchasesAreCompletedBy? {
return try {
enumValueOf<PurchasesAreCompletedBy>(this)
Expand Down
10 changes: 5 additions & 5 deletions ios/PurchasesHybridCommon/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ target 'PurchasesHybridCommon' do
platform :ios, '13.0'
use_frameworks!

pod 'RevenueCat', '5.12.1'
pod 'RevenueCat', '5.14.0'

target 'PurchasesHybridCommonTests' do
# Pods for testing
Expand All @@ -24,15 +24,15 @@ target 'PurchasesHybridCommonUI' do
platform :ios, '13.0'
use_frameworks!

pod 'RevenueCat', '5.12.1'
pod 'RevenueCatUI', '5.12.1'
pod 'RevenueCat', '5.14.0'
pod 'RevenueCatUI', '5.14.0'

end

target 'ObjCAPITester' do
platform :ios, '13.0'
use_frameworks!

pod 'RevenueCat', '5.12.1'
pod 'RevenueCatUI', '5.12.1'
pod 'RevenueCat', '5.14.0'
pod 'RevenueCatUI', '5.14.0'
end
18 changes: 9 additions & 9 deletions ios/PurchasesHybridCommon/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
PODS:
- Nimble (10.0.0)
- Quick (5.0.1)
- RevenueCat (5.12.1)
- RevenueCatUI (5.12.1):
- RevenueCat (= 5.12.1)
- RevenueCat (5.14.0)
- RevenueCatUI (5.14.0):
- RevenueCat (= 5.14.0)

DEPENDENCIES:
- Nimble (= 10.0.0)
- Quick (= 5.0.1)
- RevenueCat (= 5.12.1)
- RevenueCatUI (= 5.12.1)
- RevenueCat (= 5.14.0)
- RevenueCatUI (= 5.14.0)

SPEC REPOS:
trunk:
Expand All @@ -21,9 +21,9 @@ SPEC REPOS:
SPEC CHECKSUMS:
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
Quick: 749aa754fd1e7d984f2000fe051e18a3a9809179
RevenueCat: c6a6faaaab1ca8903416b36a51470cca0ea6b5b5
RevenueCatUI: 26db6812d76d9141548a9810d345ba6b7c798a13
RevenueCat: 34f4147c8d26d2f3c691be4e3c9f033698e557f2
RevenueCatUI: d07cd11f991f9fe72fbc64aa14257438ff1b3189

PODFILE CHECKSUM: ccf273b8fbef84d5f1e09f9676a79c36d472e9c3
PODFILE CHECKSUM: 3c80fc40beb4019de97121bae6704fcf677cda6a

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,56 @@ import RevenueCat
}
}

// MARK: - Redemption links
@objc public extension CommonFunctionality {

@objc(isWebPurchaseRedemptionURL:)
static func isWebPurchaseRedemptionURL(urlString: String) -> Bool {
guard let url = URL(string: urlString) else { return false }

return url.asWebPurchaseRedemption != nil
}

@objc static func redeemWebPurchase(urlString: String,
completion: @escaping ([String: Any]?, ErrorContainer?) -> Void) {
guard let url = URL(string: urlString), let webPurchaseRedemption = url.asWebPurchaseRedemption else {
completion(nil, Self.createErrorContainer(error: ErrorCode.unsupportedError))
return
}

_ = Task<Void, Never> {
let result = await Self.sharedInstance.redeemWebPurchase(webPurchaseRedemption)
var resultMap: [String: Any] = ["result": result.resultName]
switch (result) {
case let .success(customerInfo):
resultMap["customerInfo"] = customerInfo.dictionary
case let .error(error):
resultMap["error"] = Self.createErrorContainer(error: error)
case let .expired(obfuscatedEmail):
resultMap["obfuscatedEmail"] = obfuscatedEmail
case .purchaseBelongsToOtherUser, .invalidToken:
// Do nothing
break
}
completion(resultMap, nil)
}
}

}

private extension WebPurchaseRedemptionResult {

var resultName: String {
switch self {
case .success: return "SUCCESS"
case .error: return "ERROR"
case .purchaseBelongsToOtherUser: return "PURCHASE_BELONGS_TO_OTHER_USER"
case .invalidToken: return "INVALID_TOKEN"
case .expired: return "EXPIRED"
}
}
}

private extension CommonFunctionality {

static func customerInfoCompletionBlock(from block: @escaping ([String: Any]?, ErrorContainer?) -> Void)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class EntitlementInfoHybridAdditionsTests: QuickSpec {
entitlement: .init(productIdentifier: "productId", rawData: self.mockEntitlementData),
subscription: .init(
periodType: .normal,
purchaseDate: nil,
purchaseDate: Date(),
originalPurchaseDate: nil,
expiresDate: nil,
store: store,
Expand Down
30 changes: 30 additions & 0 deletions typescript/api-report/purchases-typescript-internal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,36 @@ export enum VERIFICATION_RESULT {
VERIFIED_ON_DEVICE = "VERIFIED_ON_DEVICE"
}

// @public
export type WebPurchaseRedemption = {
redemptionLink: string;
};

// @public
export type WebPurchaseRedemptionResult = {
result: WebPurchaseRedemptionResultType.SUCCESS;
customerInfo: CustomerInfo;
} | {
result: WebPurchaseRedemptionResultType.ERROR;
error: PurchasesError;
} | {
result: WebPurchaseRedemptionResultType.PURCHASE_BELONGS_TO_OTHER_USER;
} | {
result: WebPurchaseRedemptionResultType.INVALID_TOKEN;
} | {
result: WebPurchaseRedemptionResultType.EXPIRED;
obfuscatedEmail: string;
};

// @public
export enum WebPurchaseRedemptionResultType {
ERROR = "ERROR",
EXPIRED = "EXPIRED",
INVALID_TOKEN = "INVALID_TOKEN",
PURCHASE_BELONGS_TO_OTHER_USER = "PURCHASE_BELONGS_TO_OTHER_USER",
SUCCESS = "SUCCESS"
}

// (No @packageDocumentation comment for this package)

```
1 change: 1 addition & 0 deletions typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './offerings';
export * from './enums';
export * from './purchasesConfiguration';
export * from './callbackTypes';
export * from './webRedemption';
51 changes: 51 additions & 0 deletions typescript/src/webRedemption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { CustomerInfo } from "./customerInfo";
import { PurchasesError } from "./errors";

/**
* An object containing the redemption link to be redeemed.
* @public
*/
export type WebPurchaseRedemption = {
/**
* The redemption link to be redeemed.
*/
redemptionLink: string;
}

/**
* The result type of a Redemption Link redemption attempt.
* @public
*/
export enum WebPurchaseRedemptionResultType {
/**
* The redemption was successful.
*/
SUCCESS = "SUCCESS",
/**
* The redemption failed.
*/
ERROR = "ERROR",
/**
* The purchase associated to the link belongs to another user.
*/
PURCHASE_BELONGS_TO_OTHER_USER = "PURCHASE_BELONGS_TO_OTHER_USER",
/**
* The token is invalid.
*/
INVALID_TOKEN = "INVALID_TOKEN",
/**
* The token has expired. A new Redemption Link will be sent to the email used during purchase.
*/
EXPIRED = "EXPIRED",
}

/**
* The result of a redemption attempt.
* @public
*/
export type WebPurchaseRedemptionResult =
| { result: WebPurchaseRedemptionResultType.SUCCESS, customerInfo: CustomerInfo }
| { result: WebPurchaseRedemptionResultType.ERROR, error: PurchasesError }
| { result: WebPurchaseRedemptionResultType.PURCHASE_BELONGS_TO_OTHER_USER }
| { result: WebPurchaseRedemptionResultType.INVALID_TOKEN }
| { result: WebPurchaseRedemptionResultType.EXPIRED, obfuscatedEmail: string }

0 comments on commit 2ebba06

Please sign in to comment.