From 5d21a0903bac4e6eafab814abc343be98e0db3a2 Mon Sep 17 00:00:00 2001 From: lng-stripe <91862945+lng-stripe@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:38:59 -0500 Subject: [PATCH] [connect] Use AndroidX Navigation in Example app (#9767) * refactor using androidx navigation compose * use bottom sheet in MainActivity; safe nav up * ruby scripts/dependencies/update_transitive_dependencies.rb --- connect-example/build.gradle | 1 + connect-example/dependencies/dependencies.txt | 53 ++++--- .../android/connect/example/MainActivity.kt | 133 ++++-------------- .../example/core/NavigationExtensions.kt | 14 ++ .../example/ui/appearance/AppearanceView.kt | 11 +- .../common/BasicExampleComponentActivity.kt | 107 ++++++++------ .../connect/example/ui/common/Buttons.kt | 9 +- .../ui/common/ConnectExampleScaffold.kt | 49 +------ .../componentpicker/ComponentPickerScreen.kt | 72 ++++++++++ .../EmbeddedComponentManagerLoader.kt | 77 ++++------ .../example/ui/settings/SettingsView.kt | 5 +- .../main/res/drawable/ic_material_palette.xml | 9 ++ dependencies.gradle | 44 +++--- 13 files changed, 280 insertions(+), 304 deletions(-) create mode 100644 connect-example/src/main/java/com/stripe/android/connect/example/core/NavigationExtensions.kt create mode 100644 connect-example/src/main/res/drawable/ic_material_palette.xml diff --git a/connect-example/build.gradle b/connect-example/build.gradle index 8fd22c83b86..c1b19a6a36e 100644 --- a/connect-example/build.gradle +++ b/connect-example/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation libs.androidx.browser implementation libs.androidx.fragment implementation libs.androidx.fragmentCompose + implementation libs.androidx.hiltNavigationCompose implementation libs.androidx.lifecycle implementation libs.androidx.savedState implementation libs.androidx.viewModel diff --git a/connect-example/dependencies/dependencies.txt b/connect-example/dependencies/dependencies.txt index af18b064f34..f8855263ede 100644 --- a/connect-example/dependencies/dependencies.txt +++ b/connect-example/dependencies/dependencies.txt @@ -131,8 +131,8 @@ | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.6 (c) | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.6 (c) | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.6 (c) -| | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6 (c) | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6 (c) | | | +--- androidx.lifecycle:lifecycle-common:2.8.6 (c) | | | +--- androidx.lifecycle:lifecycle-process:2.8.6 (c) | | | \--- androidx.lifecycle:lifecycle-livedata:2.8.6 (c) @@ -186,8 +186,8 @@ | | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.6 (c) | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.6 (c) | | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.6 (c) -| | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6 (c) | | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6 (c) | | | | +--- androidx.lifecycle:lifecycle-common:2.8.6 (c) | | | | +--- androidx.lifecycle:lifecycle-process:2.8.6 (c) | | | | \--- androidx.lifecycle:lifecycle-livedata:2.8.6 (c) @@ -637,8 +637,8 @@ | | | \--- androidx.navigation:navigation-common:2.7.7 (c) | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.21 (*) | | +--- androidx.navigation:navigation-runtime-ktx:2.7.7 (c) -| | +--- androidx.navigation:navigation-common-ktx:2.7.7 (c) | | +--- androidx.navigation:navigation-runtime:2.7.7 (c) +| | +--- androidx.navigation:navigation-common-ktx:2.7.7 (c) | | \--- androidx.navigation:navigation-common:2.7.7 (c) | +--- com.google.accompanist:accompanist-systemuicontroller:0.34.0 | | +--- androidx.core:core-ktx:1.8.0 -> 1.13.0 (*) @@ -674,6 +674,34 @@ | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.21 (*) | +--- androidx.fragment:fragment:1.8.4 (c) | \--- androidx.fragment:fragment-ktx:1.8.4 (c) ++--- androidx.hilt:hilt-navigation-compose:1.2.0 +| +--- androidx.compose.runtime:runtime:1.0.1 -> 1.6.8 (*) +| +--- androidx.compose.ui:ui:1.0.1 -> 1.6.8 (*) +| +--- androidx.hilt:hilt-navigation:1.2.0 +| | +--- androidx.annotation:annotation:1.1.0 -> 1.9.0 (*) +| | +--- androidx.navigation:navigation-runtime:2.5.1 -> 2.7.7 (*) +| | +--- com.google.dagger:hilt-android:2.49 -> 2.52 +| | | +--- com.google.dagger:dagger:2.52 (*) +| | | +--- com.google.dagger:dagger-lint-aar:2.52 +| | | +--- com.google.dagger:hilt-core:2.52 +| | | | +--- com.google.dagger:dagger:2.52 (*) +| | | | +--- com.google.code.findbugs:jsr305:3.0.2 +| | | | \--- javax.inject:javax.inject:1 +| | | +--- com.google.code.findbugs:jsr305:3.0.2 +| | | +--- androidx.activity:activity:1.5.1 -> 1.8.2 (*) +| | | +--- androidx.annotation:annotation:1.3.0 -> 1.9.0 (*) +| | | +--- androidx.annotation:annotation-experimental:1.3.1 -> 1.4.0 (*) +| | | +--- androidx.fragment:fragment:1.5.1 -> 1.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.5.1 -> 2.8.6 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.5.1 -> 2.8.6 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 -> 2.8.6 (*) +| | | +--- androidx.savedstate:savedstate:1.2.0 -> 1.2.1 (*) +| | | +--- javax.inject:javax.inject:1 +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.21 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.21 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1 -> 2.8.6 (*) +| +--- androidx.navigation:navigation-compose:2.5.1 -> 2.7.7 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.21 (*) +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.6 (*) +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6 (*) +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6 (*) @@ -697,22 +725,5 @@ +--- androidx.activity:activity-compose:1.8.2 (*) +--- androidx.navigation:navigation-compose:2.7.7 (*) +--- com.google.accompanist:accompanist-systemuicontroller:0.34.0 (*) -+--- com.google.dagger:hilt-android:2.52 -| +--- com.google.dagger:dagger:2.52 (*) -| +--- com.google.dagger:dagger-lint-aar:2.52 -| +--- com.google.dagger:hilt-core:2.52 -| | +--- com.google.dagger:dagger:2.52 (*) -| | +--- com.google.code.findbugs:jsr305:3.0.2 -| | \--- javax.inject:javax.inject:1 -| +--- com.google.code.findbugs:jsr305:3.0.2 -| +--- androidx.activity:activity:1.5.1 -> 1.8.2 (*) -| +--- androidx.annotation:annotation:1.3.0 -> 1.9.0 (*) -| +--- androidx.annotation:annotation-experimental:1.3.1 -> 1.4.0 (*) -| +--- androidx.fragment:fragment:1.5.1 -> 1.8.4 (*) -| +--- androidx.lifecycle:lifecycle-common:2.5.1 -> 2.8.6 (*) -| +--- androidx.lifecycle:lifecycle-viewmodel:2.5.1 -> 2.8.6 (*) -| +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1 -> 2.8.6 (*) -| +--- androidx.savedstate:savedstate:1.2.0 -> 1.2.1 (*) -| +--- javax.inject:javax.inject:1 -| \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.21 (*) ++--- com.google.dagger:hilt-android:2.52 (*) \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.21 (*) \ No newline at end of file diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt b/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt index 93f516964b5..371cb76d0eb 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt @@ -2,137 +2,52 @@ package com.stripe.android.connect.example import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource -import com.stripe.android.connect.PrivateBetaConnectSDK -import com.stripe.android.connect.example.core.Success -import com.stripe.android.connect.example.core.then -import com.stripe.android.connect.example.ui.appearance.AppearanceView -import com.stripe.android.connect.example.ui.common.ConnectExampleScaffold +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.stripe.android.connect.example.core.safeNavigateUp import com.stripe.android.connect.example.ui.common.ConnectSdkExampleTheme -import com.stripe.android.connect.example.ui.componentpicker.ComponentPickerList +import com.stripe.android.connect.example.ui.componentpicker.ComponentPickerContent import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentLoaderViewModel -import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentManagerLoader import com.stripe.android.connect.example.ui.settings.SettingsView +import com.stripe.android.connect.example.ui.settings.SettingsViewModel import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -@OptIn(PrivateBetaConnectSDK::class) @AndroidEntryPoint class MainActivity : ComponentActivity() { - private val viewModel: EmbeddedComponentLoaderViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + val viewModel = hiltViewModel() + val navController = rememberNavController() ConnectSdkExampleTheme { - ComponentPickerContent() - } - } - } - - @Suppress("LongMethod") - @OptIn(ExperimentalMaterialApi::class) - @Composable - private fun ComponentPickerContent() { - val state by viewModel.state.collectAsState() - val embeddedComponentAsync = state.embeddedComponentManagerAsync - - val sheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - skipHalfExpanded = true, - ) - var sheetType by rememberSaveable { mutableStateOf(SheetType.SETTINGS) } - val coroutineScope = rememberCoroutineScope() - - ConnectExampleScaffold( - title = stringResource(R.string.connect_sdk_example), - actions = (embeddedComponentAsync is Success).then { - { - IconButton( - onClick = { - coroutineScope.launch { - if (!sheetState.isVisible) { - sheetType = SheetType.SETTINGS - sheetState.show() - } else { - sheetState.hide() - } - } - } - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = stringResource(R.string.settings), + NavHost(navController = navController, startDestination = MainDestination.ComponentPicker) { + composable(MainDestination.ComponentPicker) { + ComponentPickerContent( + viewModel = viewModel, + openSettings = { navController.navigate(route = MainDestination.Settings) }, ) } - IconButton( - onClick = { - coroutineScope.launch { - if (!sheetState.isVisible) { - sheetType = SheetType.APPEARANCE - sheetState.show() - } else { - sheetState.hide() - } - } - } - ) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.customize_appearance), - ) - } - } - } ?: { }, - modalSheetState = sheetState, - modalContent = (embeddedComponentAsync is Success).then { - { - BackHandler(enabled = sheetState.isVisible) { - coroutineScope.launch { sheetState.hide() } - } - when (sheetType) { - SheetType.SETTINGS -> SettingsView( - onDismiss = { coroutineScope.launch { sheetState.hide() } }, + composable(MainDestination.Settings) { + val settingsViewModel = hiltViewModel() + SettingsView( + viewModel = settingsViewModel, + onDismiss = { navController.safeNavigateUp() }, onReloadRequested = viewModel::reload, ) - SheetType.APPEARANCE -> AppearanceView( - onDismiss = { coroutineScope.launch { sheetState.hide() } }, - ) } } - }, - ) { - EmbeddedComponentManagerLoader( - embeddedComponentAsync = embeddedComponentAsync, - reload = viewModel::reload, - ) { - ComponentPickerList() } } } +} - private enum class SheetType { - SETTINGS, - APPEARANCE, - } +@Suppress("ConstPropertyName") +private object MainDestination { + const val ComponentPicker = "ComponentPicker" + const val Settings = "Settings" } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/core/NavigationExtensions.kt b/connect-example/src/main/java/com/stripe/android/connect/example/core/NavigationExtensions.kt new file mode 100644 index 00000000000..3cb75050842 --- /dev/null +++ b/connect-example/src/main/java/com/stripe/android/connect/example/core/NavigationExtensions.kt @@ -0,0 +1,14 @@ +package com.stripe.android.connect.example.core + +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavHostController + +/** + * Safer [NavHostController.navigateUp] that prevents navigating past the previous screen, + * typically due to accidental double-clicks. + */ +fun NavHostController.safeNavigateUp() { + if (currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED) { + navigateUp() + } +} diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/appearance/AppearanceView.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/appearance/AppearanceView.kt index da96c8f5bd9..51e1a9fdac5 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/appearance/AppearanceView.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/appearance/AppearanceView.kt @@ -6,14 +6,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.RadioButton import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -21,15 +20,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.stripe.android.connect.example.R import com.stripe.android.connect.example.ui.common.ConnectExampleScaffold -@OptIn(ExperimentalMaterialApi::class) @Composable fun AppearanceView( - onDismiss: () -> Unit, - viewModel: AppearanceViewModel = viewModel() + viewModel: AppearanceViewModel, + onDismiss: () -> Unit ) { val state by viewModel.state.collectAsState() @@ -38,7 +35,7 @@ fun AppearanceView( navigationIcon = { IconButton(onClick = onDismiss) { Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, + imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.cancel) ) } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt index 29976d97f25..b644f262a43 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/BasicExampleComponentActivity.kt @@ -5,10 +5,10 @@ import android.os.Bundle import android.view.View import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -19,22 +19,34 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import androidx.fragment.app.FragmentActivity +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import com.stripe.android.connect.EmbeddedComponentManager import com.stripe.android.connect.PrivateBetaConnectSDK import com.stripe.android.connect.example.core.Success +import com.stripe.android.connect.example.core.safeNavigateUp import com.stripe.android.connect.example.core.then import com.stripe.android.connect.example.ui.appearance.AppearanceView +import com.stripe.android.connect.example.ui.appearance.AppearanceViewModel import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentLoaderViewModel import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentManagerLoader +import com.stripe.android.connect.example.ui.settings.SettingsView +import com.stripe.android.connect.example.ui.settings.SettingsViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +@Suppress("ConstPropertyName") +private object BasicComponentExampleDestination { + const val Component = "Component" + const val Settings = "Settings" +} + @OptIn(PrivateBetaConnectSDK::class) @AndroidEntryPoint abstract class BasicExampleComponentActivity : FragmentActivity() { - private val viewModel: EmbeddedComponentLoaderViewModel by viewModels() - @get:StringRes abstract val titleRes: Int @@ -45,16 +57,35 @@ abstract class BasicExampleComponentActivity : FragmentActivity() { setContent { BackHandler(onBack = ::finish) - + val viewModel = hiltViewModel() + val navController = rememberNavController() ConnectSdkExampleTheme { - ExampleComponentContent() + NavHost(navController = navController, startDestination = BasicComponentExampleDestination.Component) { + composable(BasicComponentExampleDestination.Component) { + ExampleComponentContent( + viewModel = viewModel, + openSettings = { navController.navigate(BasicComponentExampleDestination.Settings) }, + ) + } + composable(BasicComponentExampleDestination.Settings) { + val settingsViewModel = hiltViewModel() + SettingsView( + viewModel = settingsViewModel, + onDismiss = { navController.safeNavigateUp() }, + onReloadRequested = viewModel::reload, + ) + } + } } } } @OptIn(ExperimentalMaterialApi::class) @Composable - private fun ExampleComponentContent() { + private fun ExampleComponentContent( + viewModel: EmbeddedComponentLoaderViewModel, + openSettings: () -> Unit, + ) { val state by viewModel.state.collectAsState() val embeddedComponentAsync = state.embeddedComponentManagerAsync @@ -64,43 +95,39 @@ abstract class BasicExampleComponentActivity : FragmentActivity() { ) val coroutineScope = rememberCoroutineScope() - ConnectExampleScaffold( - title = stringResource(titleRes), - navigationIcon = (embeddedComponentAsync is Success).then { - { - BackIconButton(onClick = ::finish) - } + ModalBottomSheetLayout( + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + sheetContent = { + val appearanceViewModel = hiltViewModel() + AppearanceView( + viewModel = appearanceViewModel, + onDismiss = { coroutineScope.launch { sheetState.hide() } }, + ) }, - actions = (embeddedComponentAsync is Success).then { - { - MoreIconButton(onClick = { - coroutineScope.launch { - if (!sheetState.isVisible) { - sheetState.show() - } else { - sheetState.hide() - } - } - }) - } - } ?: { }, - modalSheetState = sheetState, - modalContent = (embeddedComponentAsync is Success).then { - { - BackHandler(enabled = sheetState.isVisible) { - coroutineScope.launch { sheetState.hide() } + ) { + ConnectExampleScaffold( + title = stringResource(titleRes), + navigationIcon = (embeddedComponentAsync is Success).then { + { + BackIconButton(onClick = ::finish) + } + }, + actions = (embeddedComponentAsync is Success).then { + { + CustomizeAppearanceIconButton(onClick = { coroutineScope.launch { sheetState.show() } }) } - AppearanceView(onDismiss = { coroutineScope.launch { sheetState.hide() } }) + } ?: { }, + ) { + EmbeddedComponentManagerLoader( + embeddedComponentAsync = embeddedComponentAsync, + openSettings = openSettings, + reload = viewModel::reload, + ) { embeddedComponentManager -> + AndroidView(modifier = Modifier.fillMaxSize(), factory = { context -> + createComponentView(context, embeddedComponentManager) + }) } - }, - ) { - EmbeddedComponentManagerLoader( - embeddedComponentAsync = embeddedComponentAsync, - reload = viewModel::reload, - ) { embeddedComponentManager -> - AndroidView(modifier = Modifier.fillMaxSize(), factory = { context -> - createComponentView(context, embeddedComponentManager) - }) } } } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/Buttons.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/Buttons.kt index 10787c5ce9e..852699260d3 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/Buttons.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/Buttons.kt @@ -4,8 +4,8 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.stripe.android.connect.example.R @@ -20,14 +20,13 @@ fun BackIconButton(onClick: () -> Unit) { } @Composable -fun MoreIconButton( +fun CustomizeAppearanceIconButton( onClick: () -> Unit, - contentDescription: String = stringResource(R.string.more), ) { IconButton(onClick = onClick) { Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = contentDescription, + painter = painterResource(R.drawable.ic_material_palette), + contentDescription = stringResource(R.string.customize_appearance), ) } } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/ConnectExampleScaffold.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/ConnectExampleScaffold.kt index 1f564337f90..8ee8b6017aa 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/ConnectExampleScaffold.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/common/ConnectExampleScaffold.kt @@ -1,35 +1,23 @@ package com.stripe.android.connect.example.ui.common import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -@OptIn(ExperimentalMaterialApi::class) @Composable fun ConnectExampleScaffold( title: String, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, - modalContent: (@Composable ColumnScope.() -> Unit)? = null, - modalSheetState: ModalBottomSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - skipHalfExpanded = true, - ), content: @Composable () -> Unit, ) { Scaffold( @@ -52,24 +40,12 @@ fun ConnectExampleScaffold( modifier = Modifier .fillMaxSize() .padding(contentPadding), - ) { - if (modalContent != null) { - ModalBottomSheetLayout( - modifier = Modifier.fillMaxSize(), - sheetState = modalSheetState, - sheetContent = modalContent, - content = content, - ) - } else { - content() - } - } + ) { content() } } } // Previews -@OptIn(ExperimentalMaterialApi::class) @Preview(showBackground = true) @Composable private fun ConnectExampleScaffoldPreview() { @@ -81,7 +57,6 @@ private fun ConnectExampleScaffoldPreview() { ) } -@OptIn(ExperimentalMaterialApi::class) @Preview(showBackground = true) @Composable private fun ConnectExampleScaffoldWithNavigationIconPreview() { @@ -94,32 +69,12 @@ private fun ConnectExampleScaffoldWithNavigationIconPreview() { ) } -@OptIn(ExperimentalMaterialApi::class) @Preview(showBackground = true) @Composable private fun ConnectExampleScaffoldWithActionsPreview() { ConnectExampleScaffold( title = "Title", - actions = { MoreIconButton(onClick = { }) }, - content = { - Text("Content") - } - ) -} - -@OptIn(ExperimentalMaterialApi::class) -@Preview(showBackground = true) -@Composable -private fun ConnectExampleScaffoldWithModalPreview() { - ConnectExampleScaffold( - title = "Title", - modalContent = { - Text("Modal Content") - }, - modalSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Expanded, - skipHalfExpanded = true, - ), + actions = { CustomizeAppearanceIconButton(onClick = { }) }, content = { Text("Content") } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/componentpicker/ComponentPickerScreen.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/componentpicker/ComponentPickerScreen.kt index 07f92404c7a..3734f9e5548 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/componentpicker/ComponentPickerScreen.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/componentpicker/ComponentPickerScreen.kt @@ -8,6 +8,7 @@ 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.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -15,11 +16,20 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -28,10 +38,72 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.stripe.android.connect.PrivateBetaConnectSDK import com.stripe.android.connect.example.R +import com.stripe.android.connect.example.core.Success +import com.stripe.android.connect.example.core.then +import com.stripe.android.connect.example.ui.appearance.AppearanceView +import com.stripe.android.connect.example.ui.appearance.AppearanceViewModel import com.stripe.android.connect.example.ui.common.BetaBadge +import com.stripe.android.connect.example.ui.common.ConnectExampleScaffold +import com.stripe.android.connect.example.ui.common.CustomizeAppearanceIconButton +import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentLoaderViewModel +import com.stripe.android.connect.example.ui.embeddedcomponentmanagerloader.EmbeddedComponentManagerLoader import com.stripe.android.connect.example.ui.features.accountonboarding.AccountOnboardingExampleActivity import com.stripe.android.connect.example.ui.features.payouts.PayoutsExampleActivity +import kotlinx.coroutines.launch + +@OptIn(PrivateBetaConnectSDK::class, ExperimentalMaterialApi::class) +@Composable +fun ComponentPickerContent( + viewModel: EmbeddedComponentLoaderViewModel, + openSettings: () -> Unit, +) { + val state by viewModel.state.collectAsState() + val embeddedComponentAsync = state.embeddedComponentManagerAsync + + val sheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + skipHalfExpanded = true, + ) + val coroutineScope = rememberCoroutineScope() + + ModalBottomSheetLayout( + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + sheetContent = { + val appearanceViewModel = hiltViewModel() + AppearanceView( + viewModel = appearanceViewModel, + onDismiss = { coroutineScope.launch { sheetState.hide() } }, + ) + }, + ) { + ConnectExampleScaffold( + title = stringResource(R.string.connect_sdk_example), + actions = (embeddedComponentAsync is Success).then { + { + IconButton(onClick = openSettings) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(R.string.settings), + ) + } + CustomizeAppearanceIconButton(onClick = { coroutineScope.launch { sheetState.show() } }) + } + } ?: { }, + ) { + EmbeddedComponentManagerLoader( + embeddedComponentAsync = embeddedComponentAsync, + reload = viewModel::reload, + openSettings = openSettings, + ) { + ComponentPickerList() + } + } + } +} @Composable fun ComponentPickerList() { diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt index 2315b49d260..faf201256f1 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/embeddedcomponentmanagerloader/EmbeddedComponentManagerLoader.kt @@ -5,14 +5,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -27,8 +22,6 @@ import com.stripe.android.connect.example.core.Fail import com.stripe.android.connect.example.core.Loading import com.stripe.android.connect.example.core.Success import com.stripe.android.connect.example.core.Uninitialized -import com.stripe.android.connect.example.ui.settings.SettingsView -import kotlinx.coroutines.launch /** * Manages the UI for loading an [EmbeddedComponentManager], including error states. @@ -39,6 +32,7 @@ import kotlinx.coroutines.launch fun EmbeddedComponentManagerLoader( embeddedComponentAsync: Async, reload: () -> Unit, + openSettings: () -> Unit, content: @Composable (embeddedComponentManager: EmbeddedComponentManager) -> Unit, ) { val embeddedComponentManager = embeddedComponentAsync() @@ -47,6 +41,7 @@ fun EmbeddedComponentManagerLoader( is Fail -> ErrorScreen( errorMessage = embeddedComponentAsync.error.message ?: stringResource(R.string.error_initializing), onReloadRequested = reload, + openSettings = openSettings, ) is Success -> { if (embeddedComponentManager != null) { @@ -55,6 +50,7 @@ fun EmbeddedComponentManagerLoader( ErrorScreen( errorMessage = stringResource(R.string.error_initializing), onReloadRequested = reload, + openSettings = openSettings, ) } } @@ -64,7 +60,9 @@ fun EmbeddedComponentManagerLoader( @Composable private fun LoadingScreen() { Box( - modifier = Modifier.fillMaxSize().padding(24.dp), + modifier = Modifier + .fillMaxSize() + .padding(24.dp), contentAlignment = Alignment.Center, ) { Text( @@ -74,51 +72,28 @@ private fun LoadingScreen() { } } -@OptIn(ExperimentalMaterialApi::class) @Composable private fun ErrorScreen( errorMessage: String, onReloadRequested: () -> Unit, + openSettings: () -> Unit, ) { - val settingsSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - skipHalfExpanded = true, - ) - val coroutineScope = rememberCoroutineScope() - - ModalBottomSheetLayout( - modifier = Modifier.fillMaxSize(), - sheetState = settingsSheetState, - sheetContent = { - SettingsView( - onDismiss = { coroutineScope.launch { settingsSheetState.hide() } }, - onReloadRequested = onReloadRequested, - ) - }, + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, ) { - Column( - modifier = Modifier.fillMaxSize().padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(stringResource(R.string.failed_to_start_app)) - TextButton(onClick = onReloadRequested) { - Text(stringResource(R.string.reload)) - } - TextButton(onClick = { - coroutineScope.launch { - if (!settingsSheetState.isVisible) { - settingsSheetState.show() - } else { - settingsSheetState.hide() - } - } - }) { - Text(stringResource(R.string.app_settings)) - } - - Text(errorMessage) + Text(stringResource(R.string.failed_to_start_app)) + TextButton(onClick = onReloadRequested) { + Text(stringResource(R.string.reload)) + } + TextButton(onClick = openSettings) { + Text(stringResource(R.string.app_settings)) } + + Text(errorMessage) } } @@ -130,8 +105,9 @@ private fun ErrorScreen( private fun EmbeddedComponentLoaderLoadingPreview() { EmbeddedComponentManagerLoader( embeddedComponentAsync = Loading(), - reload = { }, - content = { }, + reload = {}, + openSettings = {}, + content = {}, ) } @@ -141,7 +117,8 @@ private fun EmbeddedComponentLoaderLoadingPreview() { private fun EmbeddedComponentLoaderErrorPreview() { EmbeddedComponentManagerLoader( embeddedComponentAsync = Fail(IllegalStateException("Example error")), - reload = { }, - content = { }, + reload = {}, + openSettings = {}, + content = {}, ) } diff --git a/connect-example/src/main/java/com/stripe/android/connect/example/ui/settings/SettingsView.kt b/connect-example/src/main/java/com/stripe/android/connect/example/ui/settings/SettingsView.kt index 29ec4fb82e2..33ab1957dc1 100644 --- a/connect-example/src/main/java/com/stripe/android/connect/example/ui/settings/SettingsView.kt +++ b/connect-example/src/main/java/com/stripe/android/connect/example/ui/settings/SettingsView.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.Button -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -29,17 +28,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.stripe.android.connect.example.R import com.stripe.android.connect.example.ui.common.ConnectExampleScaffold import com.stripe.android.connect.example.ui.settings.SettingsViewModel.SettingsState.DemoMerchant -@OptIn(ExperimentalMaterialApi::class) @Composable fun SettingsView( + viewModel: SettingsViewModel, onDismiss: () -> Unit, onReloadRequested: () -> Unit, - viewModel: SettingsViewModel = viewModel() ) { val state by viewModel.state.collectAsState() var serverUrlDidChange = rememberSaveable { false } diff --git a/connect-example/src/main/res/drawable/ic_material_palette.xml b/connect-example/src/main/res/drawable/ic_material_palette.xml new file mode 100644 index 00000000000..a7d4bb9042a --- /dev/null +++ b/connect-example/src/main/res/drawable/ic_material_palette.xml @@ -0,0 +1,9 @@ + + + diff --git a/dependencies.gradle b/dependencies.gradle index b1758dde6e0..f54e0674f5e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -18,6 +18,7 @@ ext.versions = [ androidxConstraintlayout : '2.1.4', androidxCore : '1.13.1', androidxFragment : '1.8.4', + androidxHilt : '1.2.0', androidxLegacySupport : '1.0.0', androidxLifecycle : '2.8.6', androidxNavigation : '2.7.7', @@ -110,27 +111,28 @@ ext.libs = [ webView : "com.google.accompanist:accompanist-webview:${versions.accompanist}", ], alipay : "com.alipay.sdk:alipaysdk-android:${versions.alipay}", - androidx : [ - activity : "androidx.activity:activity-ktx:${versions.androidxActivity}", - annotation : "androidx.annotation:annotation:${versions.androidxAnnotation}", - appCompat : "androidx.appcompat:appcompat:${versions.androidxAppcompat}", - browser : "androidx.browser:browser:${versions.androidxBrowser}", - constraintLayout : "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintlayout}", - core : "androidx.core:core:${versions.androidxCore}", - coreKtx : "androidx.core:core-ktx:${versions.androidxCore}", - fragment : "androidx.fragment:fragment-ktx:${versions.androidxFragment}", - fragmentCompose : "androidx.fragment:fragment-compose:${versions.androidxFragment}", - legacySupport : "androidx.legacy:legacy-support-v4:${versions.androidxLegacySupport}", - lifecycle : "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidxLifecycle}", - lifecycleCompose : "androidx.lifecycle:lifecycle-runtime-compose:${versions.androidxLifecycle}", - viewModel : "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}", - savedState : "androidx.lifecycle:lifecycle-viewmodel-savedstate:${versions.androidxLifecycle}", - liveDataKtx : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}", - navigationFragment: "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}", - navigationUi : "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}", - preference : "androidx.preference:preference-ktx:${versions.androidxPreference}", - recyclerView : "androidx.recyclerview:recyclerview:${versions.androidxRecyclerview}", - workManager : "androidx.work:work-runtime-ktx:${versions.workManager}", + androidx: [ + activity : "androidx.activity:activity-ktx:${versions.androidxActivity}", + annotation : "androidx.annotation:annotation:${versions.androidxAnnotation}", + appCompat : "androidx.appcompat:appcompat:${versions.androidxAppcompat}", + browser : "androidx.browser:browser:${versions.androidxBrowser}", + constraintLayout : "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintlayout}", + core : "androidx.core:core:${versions.androidxCore}", + coreKtx : "androidx.core:core-ktx:${versions.androidxCore}", + fragment : "androidx.fragment:fragment-ktx:${versions.androidxFragment}", + fragmentCompose : "androidx.fragment:fragment-compose:${versions.androidxFragment}", + hiltNavigationCompose: "androidx.hilt:hilt-navigation-compose:${versions.androidxHilt}", + legacySupport : "androidx.legacy:legacy-support-v4:${versions.androidxLegacySupport}", + lifecycle : "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidxLifecycle}", + lifecycleCompose : "androidx.lifecycle:lifecycle-runtime-compose:${versions.androidxLifecycle}", + viewModel : "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}", + savedState : "androidx.lifecycle:lifecycle-viewmodel-savedstate:${versions.androidxLifecycle}", + liveDataKtx : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}", + navigationFragment : "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}", + navigationUi : "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}", + preference : "androidx.preference:preference-ktx:${versions.androidxPreference}", + recyclerView : "androidx.recyclerview:recyclerview:${versions.androidxRecyclerview}", + workManager : "androidx.work:work-runtime-ktx:${versions.workManager}", ], // use bcprov-jdk15to18 (1.5 to 1.8) instead of bcprov-jdk15on (1.5 onwards) // to avoid conflicts with newer Java versions