Skip to content

Commit

Permalink
[Connect SDK] Update example app to match iOS (#9538)
Browse files Browse the repository at this point in the history
* 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
simond-stripe authored Nov 7, 2024
1 parent c79142f commit b229ed0
Show file tree
Hide file tree
Showing 47 changed files with 1,577 additions and 333 deletions.
5 changes: 4 additions & 1 deletion connect-example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application android:name=".App">
<application
android:name=".App"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package com.stripe.android.connect.example

import android.app.Application
import android.os.StrictMode
import com.stripe.android.connect.example.data.EmbeddedComponentManagerWrapper
import com.stripe.android.connect.example.data.SettingsService

class App : Application() {

override fun onCreate() {
super.onCreate()

Expand All @@ -23,5 +26,8 @@ class App : Application() {
.penaltyLog()
.build()
)

SettingsService.init(this)
EmbeddedComponentManagerWrapper.init()
}
}
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()
}
}
}
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,
)
}
Loading

0 comments on commit b229ed0

Please sign in to comment.