Skip to content

Commit

Permalink
Custom entitlements: add README and other improvements (#1167)
Browse files Browse the repository at this point in the history
Added the README file and did more cleanup on other tings.

---------

Co-authored-by: Cesar de la Vega <cesarvegaro@gmail.com>
  • Loading branch information
aboedo and vegaro authored Jul 24, 2023
1 parent 0f1f798 commit 66e1566
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 33 deletions.
134 changes: 134 additions & 0 deletions examples/CustomEntitlementComputationSample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Custom Entitlements Computation Example App

This app is useful for testing RevenueCat under Custom Entitlements Computation mode and understanding how it works.

## What is Custom Entitlements Computation mode?

This is a special behavior mode for RevenueCat SDK. It's intended for apps that will do their own entitlement computation separate from RevenueCat.

Apps using this mode rely on webhooks to signal their backends to refresh entitlements with RevenueCat.

In this mode, RevenueCat will not generate anonymous user IDs, or refresh customerInfo cache automatically. CustomerInfo is only refreshed when a purchase goes through
and this mode will disallow all methods other than those for configuration, switching users, getting offerings and making purchases.

When in this mode, the app should use `switchUser()` to switch to a different App User ID if needed.
The SDK should only be configured once the initial appUserID is known.

## Using the app

To use the app, you should do the following:
- Configure your app in the [RevenueCat dashboard](https://app.revenuecat.com/). No special configuration is needed, but you should contact RevenueCat support
before enabling this mode to ensure that it's the right one for your app. It's highly recommended to set Transfer Behavior to "Keep with original App User ID" in the RevenueCat Dashboard.
- Update the API key in `Constants.kt`. You can update the default `appUserID` there too, although apps in this mode should
always be calling configure only when the `appUserID` is already known.
- Update the `applicationId` in `defaultConfig` of the app-level build.gradle to match your RevenueCat app configuration.
- Have at least one Offering with at least one Package configured for Android, since this is the one that the purchase button will use.

Once configured correctly, the app will allow you to log in with different users, and will show a list of all the times `CustomerInfoListener` fired, as well as
the values for each one.

Happy testing!

![sample screenshot](./sample_screenshot.png)

## Using Custom Entitlements mode

### Installation:

This package is available on Maven and can be included via Gradle.

To use this mode, ensure that you install the purchases-custom-entitlement-computation artifact by specifying the dependency as:

```gradle
implementation 'com.revenuecat.purchases:purchases-custom-entitlement-computation:6.8.0'
```

### Configuration:

The SDK should be configured once the user has already logged in. To configure, call:

```kotlin
Purchases.configureInCustomEntitlementsComputationMode(
applicationContext,
Constants.GOOGLE_API_KEY,
Constants.defaultAppUserID
)
```

### Getting Offerings:

Call getOfferings through either the Async / Await or completion blocks alternatives:

```kotlin

val offerings = Purchases.sharedInstance.awaitOfferings()

```

### Switching users:

To switch to a different user, call:

```kotlin
Purchases.sharedInstance.switchUser(newUserID)
```

This will ensure that all purchases made from this point on are posted for the new appUserID.
After calling this method, you might need to call your backend to refresh entitlements for the new appUserID if they haven't been refreshed already.

### Making purchases:

Call `awaitPurchase()`:

```kotlin
val purchaseParams = PurchaseParams.Builder(activity, aPackage).build()

try {
val (transaction, customerInfo) =
Purchases.sharedInstance.awaitPurchase(purchaseParams)
// refresh entitlements with your backend
} catch (error: PurchasesTransactionException) {
if (error.userCancelled) {
_uiState.update { it.copy(displayErrorMessage = "Purchase was cancelled by the user") }
} else {
val errorMessage = when (error.code) {
PurchasesErrorCode.ReceiptAlreadyInUseError ->
"The receipt is already in use by another subscriber. " +
"Log in with the previous account or contact support " +
"to get your purchases transferred to regain access"
PurchasesErrorCode.PaymentPendingError ->
"The purchase is pending and may be completed at a later time. " +
"This can happen when awaiting parental approval or going " +
"through extra authentication flows for credit cards " +
"in some countries"
PurchasesErrorCode.ProductAlreadyPurchasedError ->
"Subscription is already purchased. Log in with the account " +
"that originally performed this purchase if you're using a different one."
PurchasesErrorCode.PurchaseNotAllowedError ->
"Purchasing wasn't allowed, which is common if the card is declined " +
"or the purchase is not available in the country " +
"you're trying to purchase from."
PurchasesErrorCode.StoreProblemError ->
"There was a problem with the Google Play Store. This is a generic " +
"Google error, and there's not enough information to " +
"determine the cause."
else -> "FAILED TO PURCHASE: ${error.message}"
}
_uiState.update { it.copy(displayErrorMessage = errorMessage) }

}
}
```

### Observing changes to purchases:

To ensure that your app reacts to changes to subscriptions in real time, you can use `updatedCustomerInfoListener`. This listener will only fire when new `customerInfo` is registered
in RevenueCat, like with new purchases. If there are no changes from the last value, it will not fire. This means it's not guaranteed to fire on every app open.
This listener won't fire for renewals.

```kotlin
Purchases.sharedInstance.updatedCustomerInfoListener =
UpdatedCustomerInfoListener { customerInfo ->
updateCustomerInfoInformation(customerInfo)
}
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 0 additions & 13 deletions examples/CustomEntitlementComputationSample/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,6 @@ android {
}
}

flavorDimensions "store"
productFlavors {
google {
dimension "store"
buildConfigField "String", "STORE", "\"google\""
}
amazon {
dimension "store"
buildConfigField "String", "STORE", "\"amazon\""
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
Expand Down Expand Up @@ -76,5 +64,4 @@ dependencies {
debugImplementation libs.compose.ui.tooling
debugImplementation libs.compose.ui.test.manifest
implementation libs.revenuecat
implementation libs.revenuecat.amazon
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ data class CustomerInfoEvent(
)

@Composable
fun CustomerInfoEventsList(events: List<CustomerInfoEvent>, onEventClicked: (CustomerInfoEvent) -> Unit) {
fun CustomerInfoEventsList(
events: List<CustomerInfoEvent>,
onEventClicked: (CustomerInfoEvent) -> Unit
) {
LazyColumn {
items(events) { event ->
CustomerInfoEventsListItem(event = event, onEventClicked = onEventClicked)
Expand All @@ -38,7 +41,10 @@ fun CustomerInfoEventsList(events: List<CustomerInfoEvent>, onEventClicked: (Cus
}

@Composable
fun CustomerInfoEventsListItem(event: CustomerInfoEvent, onEventClicked: (CustomerInfoEvent) -> Unit) {
fun CustomerInfoEventsListItem(
event: CustomerInfoEvent,
onEventClicked: (CustomerInfoEvent) -> Unit
) {
Column(
modifier = Modifier
.clickable { onEventClicked(event) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,9 @@ fun MainScreen(
Box(
contentAlignment = Alignment.TopStart,
) {
Column {

Column(modifier = Modifier.fillMaxWidth().padding(32.dp)) {
Text(
text = "CustomerInfo delegate values",
text = "CustomerInfo listener values",
fontSize = 16.sp,
color = Color.Black,
textAlign = TextAlign.Center,
Expand All @@ -196,7 +195,6 @@ fun MainScreen(
color = Color.Black,
textAlign = TextAlign.Center,
)

CustomerInfoEventsList(
uiState.value.customerInfoList,
onEventClicked = { customerInfoEvent ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PurchaseParams
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.PurchasesTransactionException
import com.revenuecat.purchases.awaitOfferings
import com.revenuecat.purchases.awaitPurchase
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
import com.revenuecat.purchases.purchaseWith
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -78,13 +77,36 @@ class MainViewModel : ViewModel() {
val (transaction, customerInfo) =
Purchases.sharedInstance.awaitPurchase(purchaseParams)
val logMessage = "Purchase finished:\nTransaction: $transaction\n" +
"CustomerInfo: $customerInfo"
"CustomerInfo: $customerInfo"
Log.d("Purchase", logMessage)
} catch (error: PurchasesTransactionException) {
if (error.userCancelled) {
_uiState.update { it.copy(displayErrorMessage = "User cancelled") }
_uiState.update { it.copy(displayErrorMessage = "Purchase was cancelled by the user") }
} else {
_uiState.update { it.copy(displayErrorMessage = error.message) }
val errorMessage = when (error.code) {
PurchasesErrorCode.ReceiptAlreadyInUseError ->
"The receipt is already in use by another subscriber. " +
"Log in with the previous account or contact support " +
"to get your purchases transferred to regain access"
PurchasesErrorCode.PaymentPendingError ->
"The purchase is pending and may be completed at a later time. " +
"This can happen when awaiting parental approval or going " +
"through extra authentication flows for credit cards " +
"in some countries"
PurchasesErrorCode.ProductAlreadyPurchasedError ->
"This product is already active for the user."
PurchasesErrorCode.PurchaseNotAllowedError ->
"Purchasing wasn't allowed, which is common if the card is declined " +
"or the purchase is not available in the country " +
"you're trying to purchase from."
PurchasesErrorCode.StoreProblemError ->
"There was a problem with the Google Play Store. This is a generic " +
"Google error, and there's not enough information to " +
"determine the cause."
else -> "FAILED TO PURCHASE: ${error.message}"
}
_uiState.update { it.copy(displayErrorMessage = errorMessage) }

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
agp = "8.0.2"
androidxNavigation = "2.5.3"
kotlin = "1.7.20"
purchases = "6.5.0"
purchases = "6.8.0"
lifecycle = "2.5.0"
androidxCore = "1.10.1"

Expand Down Expand Up @@ -33,5 +33,4 @@ navigation-compose = "androidx.navigation:navigation-compose:2.5.3"
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }

revenuecat = { module = "com.revenuecat.purchases:purchases", version.ref = "purchases" }
revenuecat-amazon = { module = "com.revenuecat.purchases:purchases-store-amazon", version.ref = "purchases" }
revenuecat = { module = "com.revenuecat.purchases:purchases-custom-entitlement-computation", version.ref = "purchases" }
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions examples/CustomEntitlementComputationSample/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ dependencyResolutionManagement {
rootProject.name = "CustomEntitlementComputationSample"
include ':app'

includeBuild("../../") {
dependencySubstitution {
substitute module("com.revenuecat.purchases:purchases") using project(":purchases")
substitute module("com.revenuecat.purchases:purchases-store-amazon") using project(":feature:amazon")
}
}
// Uncomment to use local version of the SDK
//includeBuild("../../") {
// dependencySubstitution {
// substitute module("com.revenuecat.purchases:purchases-custom-entitlement-computation") using project(":purchases")
// }
//}

0 comments on commit 66e1566

Please sign in to comment.