Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/checkpoints-sample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
89 changes: 89 additions & 0 deletions examples/checkpoints-sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import java.util.Properties

plugins {
alias(libs.plugins.revenuecat.android.application)
alias(libs.plugins.compose.compiler)
}

val localProperties = Properties().apply {
val localPropsFile = rootProject.file("local.properties")
if (localPropsFile.exists()) {
localPropsFile.inputStream().use { load(it) }
}
}

fun resolveProperty(name: String, default: String = ""): String {
val projectProp = project.findProperty(name) as? String
if (projectProp != null) return projectProp
return localProperties.getProperty(name) ?: default
}

android {
namespace = "com.revenuecat.checkpointssample"
compileSdk {
version = release(36)
}

defaultConfig {
// Currently reusing the same applicationId as the paywall tester
// for easier testing with the same store projects
applicationId = "com.revenuecat.paywall_tester"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"

// Library modules have a dimension used to separate different APIs.
// Applications don't need this, so we default to the "defaults" flavor.
missingDimensionStrategy("apis", "defaults")

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

// Defaults to the checkpoint tester's key (and then the paywall tester's) since all three apps
// share the same applicationId.
buildConfigField(
"String",
"CHECKPOINTS_SAMPLE_API_KEY",
"\"${resolveProperty(
"CHECKPOINTS_SAMPLE_API_KEY",
).ifEmpty { resolveProperty("CHECKPOINT_TESTER_API_KEY") }
.ifEmpty { resolveProperty("PAYWALL_TESTER_API_KEY_A") }}\"",
)
}

buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}

dependencies {
implementation(project(":purchases"))
implementation(project(":ui:revenuecatui"))

implementation(platform(libs.compose.bom))
implementation(libs.activity.compose)
implementation(libs.androidx.core)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.navigation.compose)
debugImplementation(libs.androidx.test.compose.manifest)
debugImplementation(libs.compose.ui.tooling)
}
21 changes: 21 additions & 0 deletions examples/checkpoints-sample/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
25 changes: 25 additions & 0 deletions examples/checkpoints-sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Purchasesandroid">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Purchasesandroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.revenuecat.checkpointssample

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.revenuecat.checkpointssample.paywall.PlayGamePaywall
import com.revenuecat.checkpointssample.paywall.SamplePaywall
import com.revenuecat.checkpointssample.paywall.SamplePaywallPresenters
import com.revenuecat.checkpointssample.ui.Screen
import com.revenuecat.checkpointssample.ui.dialogs.SetAttributeDialog
import com.revenuecat.checkpointssample.ui.screens.game.GameScreen
import com.revenuecat.checkpointssample.ui.screens.home.HomeScreen
import com.revenuecat.checkpointssample.ui.screens.onboarding.OnboardingScreen

@Composable
fun CheckpointsSampleApp(modifier: Modifier = Modifier) {
val navController = rememberNavController()
var showAttributeDialog by remember { mutableStateOf(false) }
Box(modifier = modifier.fillMaxSize()) {
Scaffold(
modifier = Modifier.fillMaxSize(),
floatingActionButton = {
SmallFloatingActionButton(onClick = { showAttributeDialog = true }) {
Icon(Icons.Filled.Person, contentDescription = "Set subscriber attribute")
}
},
) { innerPadding ->
NavHost(
navController = navController,
startDestination = Screen.Onboarding.route,
modifier = Modifier.padding(innerPadding),
) {
composable(Screen.Onboarding.route) {
OnboardingScreen(
onFinish = {
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Onboarding.route) { inclusive = true }
}
},
)
}
composable(Screen.Home.route) {
HomeScreen(onPlay = { navController.navigate(Screen.Game.route) })
}
composable(Screen.Game.route) {
GameScreen(onExit = { navController.popBackStack() })
}
}
}
if (showAttributeDialog) SetAttributeDialog(onDismiss = { showAttributeDialog = false })
SamplePaywallHost()
}
}

// Renders the sample's paywalls over the whole app whenever one of its PaywallPresenters has an offering to present.
@Composable
private fun SamplePaywallHost() {
val globalRequest by SamplePaywallPresenters.global.request.collectAsState()
val playGameRequest by SamplePaywallPresenters.playGame.request.collectAsState()
globalRequest?.let { SamplePaywall(request = it) }
playGameRequest?.let { PlayGamePaywall(request = it) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.revenuecat.checkpointssample

object Constants {
// Set CHECKPOINTS_SAMPLE_API_KEY in local.properties to override. Defaults to the checkpoint tester's
// key and then the paywall tester's, since the three apps share the same applicationId and store project.
val API_KEY: String = BuildConfig.CHECKPOINTS_SAMPLE_API_KEY.ifEmpty { "API_KEY" }

const val ONBOARDING_CHECKPOINT_ID = "onboarding_complete"
const val PLAY_GAME_CHECKPOINT_ID = "play_game"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.revenuecat.checkpointssample

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.revenuecat.checkpointssample.ui.theme.PurchasesandroidTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PurchasesandroidTheme {
CheckpointsSampleApp()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.revenuecat.checkpointssample

import android.app.Application
import com.revenuecat.checkpointssample.paywall.SamplePaywallPresenters
import com.revenuecat.purchases.InternalRevenueCatAPI
import com.revenuecat.purchases.LogLevel
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration
import com.revenuecat.purchases.ui.revenuecatui.checkpoints.paywallPresenter

class MainApplication : Application() {
@OptIn(InternalRevenueCatAPI::class)
override fun onCreate() {
super.onCreate()
Purchases.logLevel = LogLevel.VERBOSE
Purchases.configure(
PurchasesConfiguration.Builder(this, Constants.API_KEY)
.appUserID(null)
.build(),
)
// Offerings that checkpoints resolve to are presented by the sample's own paywall instead of the SDK's,
// unless the call supplies its own presenter through CheckpointParams.
Purchases.sharedInstance.paywallPresenter = SamplePaywallPresenters.global
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.revenuecat.checkpointssample.paywall

import android.app.Activity
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.awaitPurchase
import com.revenuecat.purchases.awaitRestore

sealed interface CheckoutOutcome {
object Completed : CheckoutOutcome
object Cancelled : CheckoutOutcome
class Failed(val message: String) : CheckoutOutcome
}

object PaywallCheckout {

suspend fun purchase(activity: Activity, packageToPurchase: Package): CheckoutOutcome = try {
Purchases.sharedInstance.awaitPurchase(PurchaseParams.Builder(activity, packageToPurchase).build())
CheckoutOutcome.Completed
} catch (e: PurchasesException) {
if (e.code == PurchasesErrorCode.PurchaseCancelledError) {
CheckoutOutcome.Cancelled
} else {
CheckoutOutcome.Failed("Purchase failed: ${e.message}")
}
}

suspend fun restore(): CheckoutOutcome = try {
val customerInfo = Purchases.sharedInstance.awaitRestore()
if (customerInfo.entitlements.active.isNotEmpty()) {
CheckoutOutcome.Completed
} else {
CheckoutOutcome.Failed("Nothing to restore.")
}
} catch (e: PurchasesException) {
CheckoutOutcome.Failed("Restore failed: ${e.message}")
}
}
Loading