-
Notifications
You must be signed in to change notification settings - Fork 659
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Connect SDK] Update example app to match iOS (#9538)
* Add app icon * WIP # Conflicts: # gradle.properties * WIP * WIP * UI main screen loading * Fix up main UI * Update theme, update settings * Update Settings * Fix other form field * fix strings and padding * add to manager * ui improvements to row * back button handling * Remove unneeded UI * Convert to string resources * Add appearance drawer * Fix padding, selection issues * Padding * Fix lint * fix Detekt lint * Remove unnecessary listOf * Add logging tag * Fix color retreival * rememberSaveable * Add firstOrNull as a guard * Remove unused imports
- Loading branch information
1 parent
c79142f
commit b229ed0
Showing
47 changed files
with
1,577 additions
and
333 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
217 changes: 82 additions & 135 deletions
217
connect-example/src/main/java/com/stripe/android/connect/example/MainActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,175 +1,122 @@ | ||
package com.stripe.android.connect.example | ||
|
||
import android.content.Intent | ||
import android.os.Bundle | ||
import androidx.activity.ComponentActivity | ||
import androidx.activity.compose.setContent | ||
import androidx.annotation.StringRes | ||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.border | ||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Row | ||
import androidx.compose.foundation.layout.Spacer | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.foundation.lazy.LazyColumn | ||
import androidx.compose.foundation.lazy.LazyItemScope | ||
import androidx.compose.foundation.lazy.items | ||
import androidx.compose.foundation.shape.RoundedCornerShape | ||
import androidx.compose.material.Divider | ||
import androidx.compose.material.Icon | ||
import androidx.compose.material.ExperimentalMaterialApi | ||
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.TextButton | ||
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 | ||
import androidx.compose.ui.res.colorResource | ||
import androidx.compose.ui.res.stringResource | ||
import androidx.compose.ui.text.TextStyle | ||
import androidx.compose.ui.text.font.FontWeight | ||
import androidx.compose.ui.text.style.LineHeightStyle | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.text.style.TextAlign | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.unit.sp | ||
import com.stripe.android.connect.example.ui.features.accountonboarding.AccountOnboardingExampleActivity | ||
import com.stripe.android.connect.example.ui.features.payouts.PayoutsExampleActivity | ||
import androidx.lifecycle.viewmodel.compose.viewModel | ||
import com.stripe.android.connect.example.ui.componentpicker.ComponentPickerScreen | ||
import com.stripe.android.connect.example.ui.settings.SettingsView | ||
import kotlinx.coroutines.launch | ||
|
||
class MainActivity : ComponentActivity() { | ||
|
||
private data class MenuItem( | ||
@StringRes val title: Int, | ||
@StringRes val subtitle: Int, | ||
val activity: Class<out ComponentActivity>, | ||
val isBeta: Boolean = false, | ||
) | ||
|
||
private val menuItems = listOf( | ||
MenuItem( | ||
title = R.string.account_onboarding, | ||
subtitle = R.string.account_onboarding_menu_subtitle, | ||
activity = AccountOnboardingExampleActivity::class.java, | ||
isBeta = true, | ||
), | ||
MenuItem( | ||
title = R.string.payouts, | ||
subtitle = R.string.payouts_menu_subtitle, | ||
activity = PayoutsExampleActivity::class.java, | ||
isBeta = true, | ||
), | ||
) | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
setContent { | ||
val viewModel: MainViewModel = viewModel() | ||
val state by viewModel.state.collectAsState() | ||
|
||
ConnectSdkExampleTheme { | ||
MainContent(title = stringResource(R.string.connect_sdk_example)) { | ||
ComponentList(menuItems) | ||
when { | ||
state.isLoading -> LoadingScreen() | ||
state.errorMessage != null -> ErrorScreen( | ||
errorMessage = state.errorMessage, | ||
onReloadRequested = viewModel::reload, | ||
) | ||
else -> ComponentPickerScreen( | ||
onReloadRequested = viewModel::reload, | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun ComponentList(components: List<MenuItem>) { | ||
LazyColumn { | ||
items(components) { menuItem -> | ||
MenuRowItem(menuItem) | ||
private fun LoadingScreen() { | ||
MainContent(title = stringResource(R.string.connect_sdk_example)) { | ||
Box( | ||
modifier = Modifier.fillMaxSize().padding(24.dp), | ||
contentAlignment = Alignment.Center, | ||
) { | ||
Text( | ||
text = stringResource(R.string.warming_up_the_server), | ||
textAlign = TextAlign.Center, | ||
) | ||
} | ||
} | ||
} | ||
|
||
@OptIn(ExperimentalMaterialApi::class) | ||
@Composable | ||
private fun LazyItemScope.MenuRowItem(menuItem: MenuItem) { | ||
val context = LocalContext.current | ||
Row( | ||
modifier = Modifier | ||
.fillParentMaxWidth() | ||
.clickable { context.startActivity(Intent(context, menuItem.activity)) }, | ||
verticalAlignment = Alignment.CenterVertically, | ||
private fun ErrorScreen( | ||
errorMessage: String? = null, | ||
onReloadRequested: () -> Unit, | ||
) { | ||
MainContent( | ||
title = stringResource(R.string.connect_sdk_example), | ||
) { | ||
Column(modifier = Modifier.weight(1f)) { | ||
Column(modifier = Modifier.padding(8.dp)) { | ||
Spacer(modifier = Modifier.height(8.dp)) | ||
Row( | ||
verticalAlignment = Alignment.CenterVertically, | ||
horizontalArrangement = Arrangement.spacedBy(8.dp), | ||
) { | ||
Text( | ||
text = stringResource(menuItem.title), | ||
fontSize = 16.sp, | ||
fontWeight = FontWeight.SemiBold, | ||
) | ||
if (menuItem.isBeta) { | ||
BetaBadge() | ||
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, | ||
) { | ||
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)) | ||
} | ||
|
||
if (errorMessage != null) { | ||
Text(errorMessage) | ||
} | ||
Spacer(modifier = Modifier.height(8.dp)) | ||
Text( | ||
text = stringResource(menuItem.subtitle), | ||
fontSize = 16.sp, | ||
) | ||
} | ||
Spacer(modifier = Modifier.height(8.dp)) | ||
Divider() | ||
} | ||
|
||
Icon( | ||
modifier = Modifier | ||
.size(36.dp) | ||
.padding(start = 8.dp), | ||
contentDescription = null, | ||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, | ||
) | ||
} | ||
} | ||
|
||
@Composable | ||
private fun BetaBadge() { | ||
val shape = RoundedCornerShape(4.dp) | ||
val labelMediumEmphasized = TextStyle.Default.copy( | ||
fontSize = 14.sp, | ||
fontWeight = FontWeight.SemiBold, | ||
lineHeight = 20.sp, | ||
lineHeightStyle = LineHeightStyle( | ||
alignment = LineHeightStyle.Alignment.Center, | ||
trim = LineHeightStyle.Trim.None | ||
) | ||
) | ||
Text( | ||
modifier = Modifier | ||
.border(1.dp, colorResource(R.color.default_border_color), shape) | ||
.background( | ||
color = colorResource(R.color.default_background_color), | ||
shape = shape | ||
) | ||
.padding(horizontal = 6.dp, vertical = 1.dp), | ||
color = colorResource(R.color.default_text_color), | ||
fontSize = 12.sp, | ||
lineHeight = 16.sp, | ||
style = labelMediumEmphasized, | ||
text = "BETA" | ||
) | ||
} | ||
|
||
@Composable | ||
@Preview(showBackground = true) | ||
fun ComponentListPreview() { | ||
ConnectSdkExampleTheme { | ||
ComponentList(menuItems) | ||
} | ||
} | ||
|
||
@Composable | ||
@Preview(showBackground = true) | ||
fun BetaBadgePreview() { | ||
ConnectSdkExampleTheme { | ||
BetaBadge() | ||
} | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
connect-example/src/main/java/com/stripe/android/connect/example/MainViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package com.stripe.android.connect.example | ||
|
||
import androidx.lifecycle.ViewModel | ||
import com.github.kittinunf.fuel.core.FuelError | ||
import com.stripe.android.connect.PrivateBetaConnectSDK | ||
import com.stripe.android.connect.example.data.EmbeddedComponentService | ||
import com.stripe.android.core.Logger | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.flow.asStateFlow | ||
import kotlinx.coroutines.flow.update | ||
import kotlinx.coroutines.launch | ||
|
||
class MainViewModel( | ||
private val embeddedComponentService: EmbeddedComponentService = EmbeddedComponentService.getInstance(), | ||
private val networkingScope: CoroutineScope = CoroutineScope(Dispatchers.IO), | ||
private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG), | ||
) : ViewModel() { | ||
|
||
private val loggingTag = this::class.java.name | ||
private val _state = MutableStateFlow(MainState()) | ||
val state: StateFlow<MainState> = _state.asStateFlow() | ||
|
||
init { | ||
fetchAccounts() | ||
} | ||
|
||
// Public methods | ||
|
||
fun reload() { | ||
_state.update { | ||
it.copy(isLoading = true, errorMessage = null) | ||
} | ||
fetchAccounts() | ||
} | ||
|
||
// Private methods | ||
|
||
@OptIn(PrivateBetaConnectSDK::class) | ||
private fun fetchAccounts() { | ||
networkingScope.launch { | ||
try { | ||
embeddedComponentService.getAccounts() | ||
_state.update { | ||
it.copy(isLoading = false, errorMessage = null) | ||
} | ||
} catch (e: FuelError) { | ||
_state.update { | ||
it.copy(isLoading = false, errorMessage = e.message) | ||
} | ||
logger.error("($loggingTag) Error getting accounts: $e") | ||
} | ||
} | ||
} | ||
|
||
// State | ||
|
||
data class MainState( | ||
val isLoading: Boolean = true, | ||
val errorMessage: String? = null, | ||
) | ||
} |
Oops, something went wrong.