Skip to content

Commit

Permalink
[Experimental] Web purchase redemption (#1889)
Browse files Browse the repository at this point in the history
### Description
This adds experimental support to redeeming anonymous web purchases
performed through RCBilling. The main API changes here are:
- Adds a new `Purchases.parseAsDeepLink` that parses the intent and
returns a deep link if it needs to be handled
- Adds a new `Purchases.sharedInstance.redeemWebPurchase` that uses a
parsed deep link to perform the redemption.
- Adds a new `DeepLink` sealed class with the types of deep links
- Adds a new `RedeemWebPurchaseListener` to listen to the result of the
redemption.
- Adds a new `RedeemWebPurchaseListener.Result` sealed class with the
result of the redemption.

All these are currently experimental APIs
  • Loading branch information
tonidero authored Oct 30, 2024
1 parent 2f826a5 commit 6354b93
Show file tree
Hide file tree
Showing 22 changed files with 761 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.revenuecat.apitester.java;

import android.content.Context;
import android.content.Intent;

import androidx.annotation.NonNull;
import androidx.annotation.OptIn;

import com.revenuecat.purchases.AmazonLWAConsentStatus;
import com.revenuecat.purchases.CacheFetchPolicy;
import com.revenuecat.purchases.CustomerInfo;
import com.revenuecat.purchases.EntitlementVerificationMode;
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI;
import com.revenuecat.purchases.Offerings;
import com.revenuecat.purchases.Purchases;
import com.revenuecat.purchases.PurchasesAreCompletedBy;
Expand All @@ -18,6 +21,7 @@
import com.revenuecat.purchases.interfaces.GetAmazonLWAConsentStatusCallback;
import com.revenuecat.purchases.interfaces.LogInCallback;
import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback;
import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener;
import com.revenuecat.purchases.interfaces.SyncAttributesAndOfferingsCallback;
import com.revenuecat.purchases.interfaces.SyncPurchasesCallback;

Expand All @@ -26,9 +30,15 @@
import java.util.Map;
import java.util.concurrent.ExecutorService;

@OptIn(markerClass = ExperimentalPreviewRevenueCatPurchasesAPI.class)
@SuppressWarnings({"unused"})
final class PurchasesAPI {
static void check(final Purchases purchases) {
static void check(
final Purchases purchases,
final Purchases.DeepLink.WebPurchaseRedemption deepLink,
final RedeemWebPurchaseListener redeemWebPurchaseListener,
final Intent intent
) {
final ReceiveCustomerInfoCallback receiveCustomerInfoListener = new ReceiveCustomerInfoCallback() {
@Override
public void onReceived(@NonNull CustomerInfo customerInfo) {
Expand Down Expand Up @@ -85,6 +95,7 @@ public void onSuccess(@NonNull AmazonLWAConsentStatus contentStatus) {
purchases.getCustomerInfo(receiveCustomerInfoListener);
purchases.getCustomerInfo(CacheFetchPolicy.CACHED_OR_FETCHED, receiveCustomerInfoListener);
purchases.getAmazonLWAConsentStatus(getAmazonLWAContentStatusCallback);
purchases.redeemWebPurchase(deepLink, redeemWebPurchaseListener);

purchases.restorePurchases(receiveCustomerInfoListener);
purchases.invalidateCustomerInfoCache();
Expand All @@ -102,6 +113,8 @@ public void onSuccess(@NonNull AmazonLWAConsentStatus contentStatus) {
final String storefrontCountryCode = purchases.getStorefrontCountryCode();

final PurchasesConfiguration configuration = purchases.getCurrentConfiguration();

final Purchases.DeepLink parsedDeepLink = Purchases.parseAsDeepLink(intent);
}

static void check(final Purchases purchases, final Map<String, String> attributes) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.revenuecat.apitester.java;

import androidx.annotation.OptIn;

import com.revenuecat.purchases.CustomerInfo;
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI;
import com.revenuecat.purchases.PurchasesError;
import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener;

@OptIn(markerClass = ExperimentalPreviewRevenueCatPurchasesAPI.class)
@SuppressWarnings({"unused"})
final class RedeemWebPurchaseListenerAPI {
static void checkListener(RedeemWebPurchaseListener listener,
RedeemWebPurchaseListener.Result result) {
listener.handleResult(result);
}

static void checkRedeemResult(RedeemWebPurchaseListener.Result result) {
if (result instanceof RedeemWebPurchaseListener.Result.Success) {
CustomerInfo customerInfo = ((RedeemWebPurchaseListener.Result.Success) result).getCustomerInfo();
} else if (result instanceof RedeemWebPurchaseListener.Result.Error) {
PurchasesError error = ((RedeemWebPurchaseListener.Result.Error) result).getError();
}

boolean isSuccess = result.isSuccess();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.revenuecat.apitester.kotlin

import android.content.Intent
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.parseAsDeepLink

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
@Suppress("unused", "UNUSED_VARIABLE")
private class IntentExtensionsAPI {
fun check(intent: Intent) {
val deepLink: Purchases.DeepLink? = intent.parseAsDeepLink()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.revenuecat.apitester.kotlin

import android.content.Context
import android.content.Intent
import com.revenuecat.purchases.AmazonLWAConsentStatus
import com.revenuecat.purchases.CacheFetchPolicy
import com.revenuecat.purchases.CustomerInfo
Expand Down Expand Up @@ -28,6 +29,7 @@ import com.revenuecat.purchases.getCustomerInfoWith
import com.revenuecat.purchases.interfaces.GetAmazonLWAConsentStatusCallback
import com.revenuecat.purchases.interfaces.LogInCallback
import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener
import com.revenuecat.purchases.interfaces.SyncAttributesAndOfferingsCallback
import com.revenuecat.purchases.interfaces.SyncPurchasesCallback
import com.revenuecat.purchases.logInWith
Expand All @@ -37,11 +39,15 @@ import com.revenuecat.purchases.syncAttributesAndOfferingsIfNeededWith
import com.revenuecat.purchases.syncPurchasesWith
import java.util.concurrent.ExecutorService

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
@Suppress("unused", "UNUSED_VARIABLE", "EmptyFunctionBlock", "DEPRECATION")
private class PurchasesAPI {
@SuppressWarnings("LongParameterList")
fun check(
purchases: Purchases,
deepLink: Purchases.DeepLink.WebPurchaseRedemption,
redeemWebPurchaseListener: RedeemWebPurchaseListener,
intent: Intent,
) {
val receiveCustomerInfoCallback = object : ReceiveCustomerInfoCallback {
override fun onReceived(customerInfo: CustomerInfo) {}
Expand Down Expand Up @@ -92,6 +98,15 @@ private class PurchasesAPI {
val countryCode = purchases.storefrontCountryCode

val configuration: PurchasesConfiguration = purchases.currentConfiguration

purchases.redeemWebPurchase(deepLink, redeemWebPurchaseListener)
val parsedDeepLink: Purchases.DeepLink? = Purchases.parseAsDeepLink(intent)
}

fun checkDeepLink(deepLink: Purchases.DeepLink): Boolean {
when (deepLink) {
is Purchases.DeepLink.WebPurchaseRedemption -> return true
}
}

@Suppress("LongMethod", "LongParameterList")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.revenuecat.apitester.kotlin

import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
@Suppress("unused", "UNUSED_VARIABLE")
private class RedeemWebPurchaseListenerAPI {
fun checkListener(
redeemWebPurchaseListener: RedeemWebPurchaseListener,
result: RedeemWebPurchaseListener.Result,
) {
redeemWebPurchaseListener.handleResult(result)
}

fun checkResult(result: RedeemWebPurchaseListener.Result): Boolean {
val isSuccess: Boolean = result.isSuccess

when (result) {
is RedeemWebPurchaseListener.Result.Success -> {
val customerInfo: CustomerInfo = result.customerInfo
return true
}
is RedeemWebPurchaseListener.Result.Error -> {
val error: PurchasesError = result.error
return false
}
}
}
}
8 changes: 8 additions & 0 deletions examples/purchase-tester/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@
android:theme="@style/AppTheme">
<activity
android:name="com.revenuecat.purchasetester.MainActivity"
android:launchMode="singleTop"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="revenuecatbilling" />
<data android:scheme="rc-43e41a79a1" />
</intent-filter>
</activity>
</application>
<queries>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
package com.revenuecat.purchasetester

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases_sample.R

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
class MainActivity : AppCompatActivity() {

internal var rcDeepLink: Purchases.DeepLink? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rcDeepLink = Purchases.parseAsDeepLink(intent)
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent != null) {
rcDeepLink = Purchases.parseAsDeepLink(intent)
}
}

fun clearDeepLink() {
rcDeepLink = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.transition.MaterialElevationScale
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.Offerings
import com.revenuecat.purchases.PurchaseParams
Expand All @@ -29,6 +30,7 @@ import com.revenuecat.purchases.getAmazonLWAConsentStatusWith
import com.revenuecat.purchases.getOfferingsWith
import com.revenuecat.purchases.interfaces.GetStoreProductsCallback
import com.revenuecat.purchases.interfaces.PurchaseCallback
import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener
import com.revenuecat.purchases.interfaces.SyncAttributesAndOfferingsCallback
import com.revenuecat.purchases.logOutWith
import com.revenuecat.purchases.models.GoogleStoreProduct
Expand Down Expand Up @@ -113,6 +115,27 @@ class OverviewFragment : Fragment(), OfferingCardAdapter.OfferingCardAdapterList
}
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
override fun onResume() {
super.onResume()

val activity = requireActivity() as MainActivity
val deepLink = activity.rcDeepLink ?: return
if (deepLink is Purchases.DeepLink.WebPurchaseRedemption) {
activity.clearDeepLink()
Purchases.sharedInstance.redeemWebPurchase(deepLink) { result ->
when (result) {
is RedeemWebPurchaseListener.Result.Success -> {
showToast("Successfully redeemed web purchase. Updating customer info.")
}
is RedeemWebPurchaseListener.Result.Error -> {
showUserError(requireActivity(), result.error)
}
}
}
}
}

private fun populateOfferings(offerings: Offerings) {
if (offerings.all.isEmpty()) {
binding.offeringHeader.text = "No Offerings"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ class Purchases internal constructor(
}
//endregion

/**
* Represents a valid RevenueCat deep link.
*/
@ExperimentalPreviewRevenueCatPurchasesAPI
sealed interface DeepLink {
/**
* Represents a web redemption link, that can be redeemed using [Purchases.redeemWebPurchase]
*/
class WebPurchaseRedemption internal constructor(internal val redemptionToken: String) : DeepLink
}

// region Static
companion object {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.revenuecat.purchases

import android.content.Intent

@ExperimentalPreviewRevenueCatPurchasesAPI
@JvmSynthetic
fun Intent.parseAsDeepLink(): Purchases.DeepLink? {
return Purchases.parseAsDeepLink(this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package com.revenuecat.purchases
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.annotation.VisibleForTesting
import com.revenuecat.purchases.common.LogIntent
import com.revenuecat.purchases.common.PlatformInfo
import com.revenuecat.purchases.common.infoLog
import com.revenuecat.purchases.common.log
import com.revenuecat.purchases.deeplinks.DeepLinkParser
import com.revenuecat.purchases.interfaces.Callback
import com.revenuecat.purchases.interfaces.GetAmazonLWAConsentStatusCallback
import com.revenuecat.purchases.interfaces.GetCustomerCenterConfigCallback
Expand All @@ -16,6 +18,7 @@ import com.revenuecat.purchases.interfaces.LogInCallback
import com.revenuecat.purchases.interfaces.PurchaseCallback
import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
import com.revenuecat.purchases.interfaces.ReceiveOfferingsCallback
import com.revenuecat.purchases.interfaces.RedeemWebPurchaseListener
import com.revenuecat.purchases.interfaces.SyncAttributesAndOfferingsCallback
import com.revenuecat.purchases.interfaces.SyncPurchasesCallback
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
Expand Down Expand Up @@ -788,9 +791,41 @@ class Purchases internal constructor(
}
// endregion

/**
* Redeem a web purchase using a [DeepLink.WebPurchaseRedemption] object obtained
* through [Purchases.parseAsDeepLink].
*/
@ExperimentalPreviewRevenueCatPurchasesAPI
fun redeemWebPurchase(webPurchaseRedemption: DeepLink.WebPurchaseRedemption, listener: RedeemWebPurchaseListener) {
purchasesOrchestrator.redeemWebPurchase(webPurchaseRedemption, listener)
}

/**
* Represents a valid RevenueCat deep link.
*/
@ExperimentalPreviewRevenueCatPurchasesAPI
sealed interface DeepLink {
/**
* Represents a web redemption link, that can be redeemed using [Purchases.redeemWebPurchase]
*/
class WebPurchaseRedemption internal constructor(internal val redemptionToken: String) : DeepLink
}

// region Static
companion object {

/**
* Given an intent, parses the deep link if any and returns a parsed version of it.
* Currently supports web redemption links.
* @return A parsed version of the deep link or null if it's not a valid RevenueCat deep link.
*/
@ExperimentalPreviewRevenueCatPurchasesAPI
@JvmStatic
fun parseAsDeepLink(intent: Intent): DeepLink? {
val intentData = intent.data ?: return null
return DeepLinkParser.parseDeepLink(intentData)
}

/**
* DO NOT MODIFY. This is used internally by the Hybrid SDKs to indicate which platform is
* being used
Expand Down
Loading

0 comments on commit 6354b93

Please sign in to comment.