Skip to content

Commit

Permalink
Feedback Surveys (#2010)
Browse files Browse the repository at this point in the history
  • Loading branch information
vegaro authored Dec 31, 2024
1 parent 742b0d9 commit 44e3fd4
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -31,20 +27,27 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import com.revenuecat.purchases.InternalRevenueCatAPI
import com.revenuecat.purchases.ui.debugview.DebugRevenueCatBottomSheet
import com.revenuecat.purchases.ui.revenuecatui.ExperimentalPreviewRevenueCatUIPurchasesAPI
import com.revenuecat.purchases.ui.revenuecatui.customercenter.CustomerCenter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

private const val BOTTOM_SHEET_MAX_HEIGHT_PERCENTAGE = 0.9f

@OptIn(ExperimentalMaterial3Api::class, ExperimentalPreviewRevenueCatUIPurchasesAPI::class)
@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class, InternalRevenueCatAPI::class)
@Composable
fun AppInfoScreen(viewModel: AppInfoScreenViewModel = viewModel<AppInfoScreenViewModelImpl>()) {
var isDebugBottomSheetVisible by remember { mutableStateOf(false) }
var isCustomerCenterBottomSheetVisible by remember { mutableStateOf(false) }
var isCustomerCenterVisible by remember { mutableStateOf(false) }
var showLogInDialog by remember { mutableStateOf(false) }

if (isCustomerCenterVisible) {
CustomerCenter(modifier = Modifier.fillMaxSize()) {
isCustomerCenterVisible = false
}
return
}

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
Expand All @@ -61,7 +64,9 @@ fun AppInfoScreen(viewModel: AppInfoScreenViewModel = viewModel<AppInfoScreenVie
Button(onClick = { isDebugBottomSheetVisible = true }) {
Text(text = "Show debug view")
}
Button(onClick = { isCustomerCenterBottomSheetVisible = true }) {
Button(onClick = {
isCustomerCenterVisible = true
}) {
Text(text = "Show customer center")
}
}
Expand All @@ -76,19 +81,6 @@ fun AppInfoScreen(viewModel: AppInfoScreenViewModel = viewModel<AppInfoScreenVie
isVisible = isDebugBottomSheetVisible,
onDismissCallback = { isDebugBottomSheetVisible = false },
)

if (isCustomerCenterBottomSheetVisible) {
val customerCenterSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
onDismissRequest = { isCustomerCenterBottomSheetVisible = false },
modifier = Modifier.fillMaxHeight(BOTTOM_SHEET_MAX_HEIGHT_PERCENTAGE),
sheetState = customerCenterSheetState,
) {
// CustomerCenter WIP: Uncomment when ready
// CustomerCenter(modifier = Modifier.fillMaxSize())
Text("CustomerCenter disabled. Uncomment when ready.")
}
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package com.revenuecat.purchases.ui.revenuecatui.customercenter

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.revenuecat.purchases.InternalRevenueCatAPI
import com.revenuecat.purchases.ui.revenuecatui.ExperimentalPreviewRevenueCatUIPurchasesAPI

/**
Expand All @@ -13,7 +14,10 @@ import com.revenuecat.purchases.ui.revenuecatui.ExperimentalPreviewRevenueCatUIP
@Composable
@ExperimentalPreviewRevenueCatUIPurchasesAPI
@SuppressWarnings("PreviewPublic")
// CustomerCenter WIP: Make public when ready
internal fun CustomerCenter(modifier: Modifier = Modifier) {
InternalCustomerCenter(modifier)
@InternalRevenueCatAPI
fun CustomerCenter(
modifier: Modifier = Modifier,
onDismiss: () -> Unit,
) {
InternalCustomerCenter(modifier = modifier, onDismiss = onDismiss)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
@file:Suppress("TooManyFunctions")
@file:JvmSynthetic
@file:OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)

package com.revenuecat.purchases.ui.revenuecatui.customercenter

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
Expand All @@ -29,6 +41,7 @@ import com.revenuecat.purchases.ui.revenuecatui.customercenter.dialogs.RestorePu
import com.revenuecat.purchases.ui.revenuecatui.customercenter.viewmodel.CustomerCenterViewModel
import com.revenuecat.purchases.ui.revenuecatui.customercenter.viewmodel.CustomerCenterViewModelFactory
import com.revenuecat.purchases.ui.revenuecatui.customercenter.viewmodel.CustomerCenterViewModelImpl
import com.revenuecat.purchases.ui.revenuecatui.customercenter.views.FeedbackSurveyView
import com.revenuecat.purchases.ui.revenuecatui.customercenter.views.ManageSubscriptionsView
import com.revenuecat.purchases.ui.revenuecatui.data.PurchasesImpl
import com.revenuecat.purchases.ui.revenuecatui.data.PurchasesType
Expand All @@ -39,16 +52,32 @@ import kotlinx.coroutines.launch
internal fun InternalCustomerCenter(
modifier: Modifier = Modifier,
viewModel: CustomerCenterViewModel = getCustomerCenterViewModel(),
onDismiss: () -> Unit,
) {
val state by viewModel.state.collectAsState()
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current

if (state is CustomerCenterState.NotLoaded) {
coroutineScope.launch {
viewModel.loadCustomerCenter()
}
}

BackHandler {
val buttonType = state.navigationButtonType
viewModel.onNavigationButtonPressed()
if (buttonType == CustomerCenterState.NavigationButtonType.CLOSE) {
onDismiss()
}
}

InternalCustomerCenter(
state,
modifier,
onAction = { action ->
when (action) {
is CustomerCenterAction.DetermineFlow -> {
is CustomerCenterAction.PathButtonPressed -> {
coroutineScope.launch {
viewModel.pathButtonPressed(context, action.path)
}
Expand All @@ -62,6 +91,13 @@ internal fun InternalCustomerCenter(

is CustomerCenterAction.DismissRestoreDialog -> viewModel.dismissRestoreDialog()
is CustomerCenterAction.ContactSupport -> viewModel.contactSupport(context, action.email)
is CustomerCenterAction.NavigationButtonPressed -> {
val buttonType = state.navigationButtonType
viewModel.onNavigationButtonPressed()
if (buttonType == CustomerCenterState.NavigationButtonType.CLOSE) {
onDismiss()
}
}
}
},
)
Expand All @@ -73,8 +109,20 @@ private fun InternalCustomerCenter(
modifier: Modifier = Modifier,
onAction: (CustomerCenterAction) -> Unit,
) {
CustomerCenterScaffold(modifier) {
val title = getTitleForState(state)
CustomerCenterScaffold(
modifier = modifier,
title = title,
onAction = onAction,
navigationButtonType =
if (state is CustomerCenterState.Success) {
state.navigationButtonType
} else {
CustomerCenterState.NavigationButtonType.CLOSE
},
) {
when (state) {
is CustomerCenterState.NotLoaded -> {}
is CustomerCenterState.Loading -> CustomerCenterLoading()
is CustomerCenterState.Error -> CustomerCenterError(state)
is CustomerCenterState.Success -> CustomerCenterLoaded(
Expand All @@ -87,14 +135,46 @@ private fun InternalCustomerCenter(

@Composable
private fun CustomerCenterScaffold(
onAction: (CustomerCenterAction) -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
navigationButtonType: CustomerCenterState.NavigationButtonType = CustomerCenterState.NavigationButtonType.CLOSE,
mainContent: @Composable () -> Unit,
) {
Column(
modifier = modifier,
modifier = modifier
.fillMaxSize()
.systemBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.Top,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.statusBarsPadding(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
IconButton(onClick = {
onAction(CustomerCenterAction.NavigationButtonPressed)
}) {
Icon(
imageVector = when (navigationButtonType) {
CustomerCenterState.NavigationButtonType.BACK -> Icons.AutoMirrored.Filled.ArrowBack
CustomerCenterState.NavigationButtonType.CLOSE -> Icons.Default.Close
},
contentDescription = null,
)
}
title?.let {
Text(
text = title,
modifier = Modifier.padding(start = 4.dp),
style = MaterialTheme.typography.titleLarge,
)
}
}
mainContent()
}
}
Expand All @@ -111,13 +191,14 @@ private fun CustomerCenterError(state: CustomerCenterState.Error) {
Text("Error: ${state.error}")
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
@Composable
private fun CustomerCenterLoaded(
state: CustomerCenterState.Success,
onAction: (CustomerCenterAction) -> Unit,
) {
if (state.showRestoreDialog) {
if (state.feedbackSurveyData != null) {
FeedbackSurveyView(state.feedbackSurveyData)
} else if (state.showRestoreDialog) {
RestorePurchasesDialog(
state = state.restorePurchasesState,
onDismiss = { onAction(CustomerCenterAction.DismissRestoreDialog) },
Expand All @@ -128,16 +209,25 @@ private fun CustomerCenterLoaded(
}
},
)
} else {
val configuration = state.customerCenterConfigData
MainScreen(state, configuration, onAction)
}
}

val configuration = state.customerCenterConfigData
@Composable
private fun MainScreen(
state: CustomerCenterState.Success,
configuration: CustomerCenterConfigData,
onAction: (CustomerCenterAction) -> Unit,
) {
if (state.purchaseInformation != null) {
configuration.getManagementScreen()?.let { managementScreen ->
ManageSubscriptionsView(
screen = managementScreen,
purchaseInformation = state.purchaseInformation,
onDetermineFlow = { path ->
onAction(CustomerCenterAction.DetermineFlow(path))
onPathButtonPress = { path ->
onAction(CustomerCenterAction.PathButtonPressed(path))
},
)
} ?: run {
Expand All @@ -148,8 +238,8 @@ private fun CustomerCenterLoaded(
configuration.getNoActiveScreen()?.let { noActiveScreen ->
ManageSubscriptionsView(
screen = noActiveScreen,
onDetermineFlow = { path ->
onAction(CustomerCenterAction.DetermineFlow(path))
onPathButtonPress = { path ->
onAction(CustomerCenterAction.PathButtonPressed(path))
},
)
} ?: run {
Expand All @@ -159,6 +249,16 @@ private fun CustomerCenterLoaded(
}
}

private fun getTitleForState(state: CustomerCenterState): String? {
return when (state) {
is CustomerCenterState.Success -> {
state.title
}

else -> null
}
}

@Composable
private fun getCustomerCenterViewModel(
purchases: PurchasesType = PurchasesImpl(),
Expand All @@ -169,7 +269,6 @@ private fun getCustomerCenterViewModel(
return viewModel
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
private val previewConfigData = CustomerCenterConfigData(
screens = mapOf(
CustomerCenterConfigData.Screen.ScreenType.MANAGEMENT to CustomerCenterConfigData.Screen(
Expand Down Expand Up @@ -222,7 +321,6 @@ internal fun CustomerCenterErrorPreview() {
)
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
@Preview
@Composable
internal fun CustomerCenterLoadedPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import com.revenuecat.purchases.customercenter.CustomerCenterConfigData

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
internal sealed class CustomerCenterAction {
data class DetermineFlow(val path: CustomerCenterConfigData.HelpPath) : CustomerCenterAction()
data class PathButtonPressed(val path: CustomerCenterConfigData.HelpPath) : CustomerCenterAction()
object PerformRestore : CustomerCenterAction()
object DismissRestoreDialog : CustomerCenterAction()
data class ContactSupport(val email: String) : CustomerCenterAction()
object NavigationButtonPressed : CustomerCenterAction()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,36 @@ import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.customercenter.CustomerCenterConfigData
import com.revenuecat.purchases.ui.revenuecatui.customercenter.dialogs.RestorePurchasesState

internal sealed class CustomerCenterState {
internal sealed class CustomerCenterState(
open val navigationButtonType: NavigationButtonType = NavigationButtonType.CLOSE,
) {

enum class NavigationButtonType {
BACK, CLOSE
}

object NotLoaded : CustomerCenterState()

object Loading : CustomerCenterState()
data class Error(val error: PurchasesError) : CustomerCenterState()

// CustomerCenter WIP: Change to use the actual data the customer center will use.
data class Error(
val error: PurchasesError,
) : CustomerCenterState()

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
data class Success(
val customerCenterConfigData: CustomerCenterConfigData,
val purchaseInformation: PurchaseInformation? = null,
val showRestoreDialog: Boolean = false,
val restorePurchasesState: RestorePurchasesState = RestorePurchasesState.INITIAL,
) : CustomerCenterState()
val feedbackSurveyData: FeedbackSurveyData? = null,
val title: String? = null,
override val navigationButtonType: NavigationButtonType = NavigationButtonType.CLOSE,
) : CustomerCenterState(navigationButtonType)
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
internal data class FeedbackSurveyData(
val feedbackSurvey: CustomerCenterConfigData.HelpPath.PathDetail.FeedbackSurvey,
val onOptionSelected: (CustomerCenterConfigData.HelpPath.PathDetail.FeedbackSurvey.Option?) -> Unit,
)
Loading

0 comments on commit 44e3fd4

Please sign in to comment.