Skip to content

Commit

Permalink
Debug view: Initial UI + Usage in MagicWeatherCompose (#1075)
Browse files Browse the repository at this point in the history
### Description
Same as was added in iOS in
RevenueCat/purchases-ios#2567. Includes the
changes in #1180

This PR adds a debug view in the Android SDK and uses it in the
`MagicWeatherCompose` sample app. This debug view is meant to be a
Jetpack compose function, even though later we might add convenience
accessors for people not using jetpack compose. It's extracted to a
different module so it can be published separately from the main SDK. It
also attempts to keep all the code in the debug source set, to try to
include as few code in the release variants.

The provided API is currently 2 composable functions:
- `DebugRevenueCatDebugScreen`: This is the actual UI that is part of
the debug screen. In case devs want to use it in a custom UI.
- `DebugRevenueCatBottomSheet`: This will display the debug screen as a
bottom sheet. This is what's used in the `MagicWeatherCompose` sample
app.

##### TODO
- [ ] Allow visualizing offerings in debug menu
- [ ] Support non-composable apps by wrapping it into a fragment, with
utility launch methods.
- [ ] Split library for debug and release
- [ ] Add UI tests
- [ ] Support publishing the new module as different libraries for debug
and release.
- [ ] Remove module local substitution once debugview library has been
published


https://github.com/RevenueCat/purchases-android/assets/808417/b3a953e4-b96f-4b06-8294-d2bcf9b66c7d
  • Loading branch information
tonidero authored Aug 3, 2023
1 parent b428869 commit 1c5f6e9
Show file tree
Hide file tree
Showing 28 changed files with 571 additions and 3 deletions.
26 changes: 25 additions & 1 deletion api-tester/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,27 @@ android {
}
}

defaultConfig {
minSdkVersion 21 // Compose requires minSdkVersion 21
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}

composeOptions {
kotlinCompilerExtensionVersion '1.4.0-alpha02'
}

buildFeatures {
compose true
}

testOptions {
unitTests.includeAndroidResources = true
unitTests.all {
Expand All @@ -28,6 +49,9 @@ android {
dependencies {
implementation project(path: ':purchases')
implementation project(path: ':feature:amazon')
implementation project(path: ':debugview')

implementation libs.androidx.annotation
implementation(platform(libs.kotlin.bom))
implementation platform(libs.compose.bom)
implementation libs.compose.ui
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.revenuecat.apitester.kotlin

import androidx.compose.runtime.Composable
import com.revenuecat.purchases.debugview.DebugRevenueCatBottomSheet
import com.revenuecat.purchases.debugview.DebugRevenueCatScreen

@Suppress("unused")
private class PurchasesDebugViewAPI {
@Composable
fun CheckDebugView(isVisible: Boolean, onDismissCallback: (() -> Unit)? = null) {
DebugRevenueCatScreen()
DebugRevenueCatBottomSheet(isVisible = isVisible, onDismissCallback = onDismissCallback)
}
}
4 changes: 4 additions & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ formatting:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true
style:
UnusedPrivateMember:
active: true
ignoreAnnotated: [ 'Preview' ]
1 change: 1 addition & 0 deletions debugview/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
49 changes: 49 additions & 0 deletions debugview/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
plugins {
alias libs.plugins.android.library
alias libs.plugins.kotlin.android
}

// TODO Uncomment apply plugin once debugview package is ready to be published.
//if (!project.getProperties()["ANDROID_VARIANT_TO_PUBLISH"].contains("customEntitlementComputation")) {
// apply plugin: "com.vanniktech.maven.publish"
//}

apply from: "$rootProject.projectDir/library.gradle"

android {
namespace 'com.revenuecat.purchases.debugview'

flavorDimensions = ["apis"]
productFlavors {
defaults {
dimension "apis"
}
}

defaultConfig {
minSdkVersion 21 // Compose requires minSdkVersion 21
}

buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.4.0-alpha02'
}
}

dependencies {
implementation project(path: ':purchases')

implementation libs.androidx.core
implementation platform(libs.compose.bom)
implementation libs.compose.ui
implementation libs.compose.ui.graphics
implementation libs.compose.ui.tooling.preview
implementation libs.compose.material
implementation libs.compose.material3
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.lifecycle.viewmodel
implementation libs.androidx.lifecycle.viewmodel.compose
debugImplementation libs.compose.ui.tooling
}
Empty file added debugview/consumer-rules.pro
Empty file.
21 changes: 21 additions & 0 deletions debugview/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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.revenuecat.purchases.debugview

import com.revenuecat.purchases.debugview.models.SettingScreenState
import kotlinx.coroutines.flow.StateFlow

internal interface DebugRevenueCatViewModel {
val state: StateFlow<SettingScreenState>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.revenuecat.purchases.debugview

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun InternalDebugRevenueCatBottomSheet(
isVisible: Boolean = false,
onDismissCallback: (() -> Unit)? = null,
) {
if (isVisible) {
val rcDebugMenuSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)

val scope = rememberCoroutineScope()

ModalBottomSheet(
sheetState = rcDebugMenuSheetState,
onDismissRequest = {
scope.launch {
rcDebugMenuSheetState.hide()
onDismissCallback?.invoke()
}
},
) {
InternalDebugRevenueCatScreen()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.revenuecat.purchases.debugview

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.revenuecat.purchases.debugview.models.InternalDebugRevenueCatScreenViewModel
import com.revenuecat.purchases.debugview.models.SettingGroupState
import com.revenuecat.purchases.debugview.models.SettingScreenState
import com.revenuecat.purchases.debugview.models.SettingState
import com.revenuecat.purchases.debugview.settings.SettingGroup
import kotlinx.coroutines.flow.MutableStateFlow

@Composable
internal fun InternalDebugRevenueCatScreen(
screenViewModel: DebugRevenueCatViewModel = viewModel<InternalDebugRevenueCatScreenViewModel>(),
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(bottom = 16.dp),
) {
Text(
text = "RevenueCat Debug Menu",
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp),
)
screenViewModel.state.collectAsState().value.toSettingGroupStates().forEach { SettingGroup(it) }
}
}

@Preview(showBackground = true)
@Composable
private fun InternalDebugRevenueCatScreenPreview() {
InternalDebugRevenueCatScreen(
screenViewModel = object : DebugRevenueCatViewModel {
override val state = MutableStateFlow<SettingScreenState>(
SettingScreenState.Configured(
SettingGroupState(
"Configuration",
listOf(
SettingState.Text("SDK version", "3.0.0"),
SettingState.Text("Observer mode", "true"),
),
),
SettingGroupState(
"Customer info",
listOf(
SettingState.Text("Current User ID", "current-user-id"),
SettingState.Text("Active entitlements", "pro, premium"),
),
),
SettingGroupState(
"Offerings",
listOf(
SettingState.Text("current", "TODO"),
SettingState.Text("default", "TODO"),
),
),
),
)
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.revenuecat.purchases.debugview.models

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.Offerings
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.awaitCustomerInfo
import com.revenuecat.purchases.awaitOfferings
import com.revenuecat.purchases.debugview.DebugRevenueCatViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
internal class InternalDebugRevenueCatScreenViewModel : ViewModel(), DebugRevenueCatViewModel {
override val state: StateFlow<SettingScreenState>
get() = _state

private var _state: MutableStateFlow<SettingScreenState> = MutableStateFlow(
SettingScreenState.NotConfigured(configurationGroup()),
)

init {
if (Purchases.isConfigured) {
refreshInfo()
}
}

private fun refreshInfo() {
viewModelScope.launch {
try {
val offerings = Purchases.sharedInstance.awaitOfferings()
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()
_state.update {
SettingScreenState.Configured(
configurationGroup(),
customerInfoGroup(customerInfo),
offeringsGroup(offerings),
)
}
} catch (e: PurchasesException) {
Log.e("RevenueCatDebugView", "Error getting RevenueCat SDK info for debug view. Exception: $e")
}
}
}

private fun configurationGroup(): SettingGroupState {
val storeName = if (Purchases.isConfigured) {
Purchases.sharedInstance.store.name
} else {
"Not configured"
}
val observerMode = if (Purchases.isConfigured) {
"${!Purchases.sharedInstance.finishTransactions}"
} else {
"Not configured"
}
return SettingGroupState(
"Configuration",
listOf(
SettingState.Text("SDK version", Purchases.frameworkVersion),
SettingState.Text("Is configured", "${Purchases.isConfigured}"),
SettingState.Text("Store", storeName),
SettingState.Text("Observer mode", observerMode),
),
)
}

private fun customerInfoGroup(customerInfo: CustomerInfo): SettingGroupState {
return SettingGroupState(
title = "Customer info",
settings = listOf(
SettingState.Text("Current User ID", Purchases.sharedInstance.appUserID),
SettingState.Text("Original User ID", customerInfo.originalAppUserId),
SettingState.Text(
"Active entitlements",
customerInfo.entitlements.active
.map { "${it.key} until ${it.value.expirationDate}" }
.joinToString("\n")
.takeIf { it.isNotEmpty() } ?: "None",
),
SettingState.Text("Verification result", customerInfo.entitlements.verification.name),
SettingState.Text("Request date", customerInfo.requestDate.toString()),
),
)
}

private fun offeringsGroup(offerings: Offerings): SettingGroupState {
return SettingGroupState(
title = "Offerings",
settings = offerings.all.values.map { offering ->
SettingState.Text(
title = offering.identifier,
content = "TODO",
)
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.revenuecat.purchases.debugview.models

internal data class SettingGroupState(val title: String, val settings: List<SettingState>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.revenuecat.purchases.debugview.models

internal sealed class SettingScreenState(open val configuration: SettingGroupState) {
data class NotConfigured(override val configuration: SettingGroupState) : SettingScreenState(configuration)
data class Configured(
override val configuration: SettingGroupState,
val customerInfo: SettingGroupState,
val offerings: SettingGroupState,
) : SettingScreenState(configuration)

fun toSettingGroupStates(): List<SettingGroupState> {
return when (this) {
is NotConfigured -> listOf(configuration)
is Configured -> listOf(configuration, customerInfo, offerings)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.revenuecat.purchases.debugview.models

internal sealed class SettingState(open val title: String) {
data class Text(override val title: String, val content: String) : SettingState(title)
}
Loading

0 comments on commit 1c5f6e9

Please sign in to comment.