Skip to content

Commit 083151a

Browse files
committed
feat(myaccount): add centralized User Profile screen
Replace separate Connect Phone/Email items in My Account and Unlink Phone/Email items in Labs with a single User Profile screen showing display name, phone, email, and linked social accounts. The screen supports connecting, replacing, and unlinking contact methods as well as unlinking X/Twitter accounts. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 3863852 commit 083151a

14 files changed

Lines changed: 714 additions & 166 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import com.flipcash.app.lab.LabsScreen
3737
import com.flipcash.app.lab.NavBarSettingsScreen
3838
import com.flipcash.app.login.OnboardingFlowScreen
3939
import com.flipcash.app.menu.MenuScreen
40+
import com.flipcash.app.myaccount.UserProfileScreen
4041
import com.flipcash.app.myaccount.MyAccountScreen
4142
import com.flipcash.app.scanner.ScannerScreen
4243
import com.flipcash.app.shareapp.ShareAppScreen
@@ -117,6 +118,7 @@ fun appEntryProvider(
117118
annotatedEntry<AppRoute.Menu.AppSettings> { AppSettingsScreen() }
118119
annotatedEntry<AppRoute.Menu.Lab> { LabsScreen() }
119120
annotatedEntry<AppRoute.Menu.NavBarSettings> { NavBarSettingsScreen() }
121+
annotatedEntry<AppRoute.Menu.UserProfile> { UserProfileScreen() }
120122
annotatedEntry<AppRoute.Menu.MyAccount> { MyAccountScreen() }
121123
annotatedEntry<AppRoute.Menu.BackupKey> { BackupKeyScreen() }
122124
annotatedEntry<AppRoute.Menu.AdvancedFeatures> { AdvancedFeaturesScreen() }

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ sealed interface AppRoute : NavKey, Parcelable {
236236
@Serializable
237237
data object DeviceLogs : Menu
238238
@Serializable
239+
data object UserProfile : Menu
240+
@Serializable
239241
data object Lab : Menu
240242
@Serializable
241243
data object NavBarSettings : Menu, com.getcode.navigation.Sheet, com.getcode.navigation.WrapContentSheet

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,24 @@
324324
<string name="action_unlinkPhone">Unlink Phone</string>
325325
<string name="action_unlinkEmail">Unlink Email</string>
326326

327+
<string name="title_userProfile">User Profile</string>
328+
<string name="title_sectionPhone">Phone Number</string>
329+
<string name="title_sectionEmail">Email Address</string>
330+
<string name="action_addPhoneNumber">Add Phone Number</string>
331+
<string name="action_addEmailAddress">Add Email Address</string>
332+
<string name="subtitle_linkedForPayments">Linked for payments</string>
333+
<string name="prompt_title_unlinkPhone">Unlink Phone Number?</string>
334+
<string name="prompt_description_unlinkPhone">Your phone number will be removed from your profile.</string>
335+
<string name="prompt_title_unlinkEmail">Unlink Email Address?</string>
336+
<string name="prompt_description_unlinkEmail">Your email address will be removed from your profile.</string>
337+
<string name="title_sectionDisplayName">Display Name</string>
338+
<string name="title_sectionSocialAccounts">Social Accounts</string>
339+
<string name="subtitle_noDisplayName">No display name set</string>
340+
<string name="subtitle_noSocialAccounts">No social accounts linked</string>
341+
<string name="prompt_title_unlinkSocialAccount">Unlink Account?</string>
342+
<string name="prompt_description_unlinkSocialAccount">This social account will be removed from your profile.</string>
343+
<string name="action_unlinkAccount">Unlink Account</string>
344+
327345
<string name="title_verificationFlow">Verify Your Phone Number And Email To Continue</string>
328346
<string name="subtitle_verificationFlowForOnramp">This will allow you to add funds from your debit card</string>
329347
"

apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenContent.kt

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ import androidx.compose.foundation.lazy.LazyColumn
1010
import androidx.compose.foundation.lazy.items
1111
import androidx.compose.foundation.lazy.rememberLazyListState
1212
import androidx.compose.material.icons.Icons
13-
import androidx.compose.material.icons.filled.MarkEmailUnread
14-
import androidx.compose.material.icons.filled.Navigation
15-
import androidx.compose.material.icons.filled.PhonelinkErase
13+
import androidx.compose.material.icons.filled.ContactMail
1614
import androidx.compose.material.icons.filled.Token
1715
import androidx.compose.material3.HorizontalDivider
1816
import androidx.compose.material3.Text
@@ -45,7 +43,6 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
4543
val betaFlagsController = LocalFeatureFlags.current
4644
val betaFlags by betaFlagsController.observe().collectAsStateWithLifecycle()
4745
val navigator = LocalCodeNavigator.current
48-
val isLoggedIn by viewModel.isLoggedIn.collectAsStateWithLifecycle()
4946
val isStaff by viewModel.isStaff.collectAsStateWithLifecycle()
5047

5148
val state = rememberLazyListState()
@@ -137,26 +134,6 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel) {
137134
}
138135
}
139136
}
140-
141-
if (isLoggedIn) {
142-
item { SectionHeader(stringResource(R.string.title_settingsSectionAccount)) }
143-
item {
144-
ListItem(
145-
headline = stringResource(R.string.action_unlinkPhone),
146-
icon = rememberVectorPainter(Icons.Default.PhonelinkErase),
147-
) {
148-
viewModel.unlinkPhone()
149-
}
150-
}
151-
item {
152-
ListItem(
153-
headline = stringResource(R.string.action_unlinkEmail),
154-
icon = rememberVectorPainter(Icons.Default.MarkEmailUnread),
155-
) {
156-
viewModel.unlinkEmail()
157-
}
158-
}
159-
}
160137
}
161138
}
162139

apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenViewModel.kt

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,19 @@ package com.flipcash.app.lab.internal
33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import com.flipcash.app.userflags.UserFlagsCoordinator
6-
import com.flipcash.features.lab.R
7-
import com.flipcash.services.controllers.ContactVerificationController
8-
import com.flipcash.services.models.UserFlags
9-
import com.flipcash.services.models.ContactMethod
10-
import com.flipcash.services.models.EmailVerificationError
11-
import com.flipcash.services.models.PhoneVerificationError
126
import com.flipcash.services.user.AuthState
137
import com.flipcash.services.user.UserManager
14-
import com.getcode.manager.BottomBarAction
15-
import com.getcode.manager.BottomBarManager
16-
import com.getcode.util.resources.ResourceHelper
178
import dagger.hilt.android.lifecycle.HiltViewModel
189
import kotlinx.coroutines.flow.SharingStarted
1910
import kotlinx.coroutines.flow.filterIsInstance
2011
import kotlinx.coroutines.flow.map
2112
import kotlinx.coroutines.flow.stateIn
22-
import kotlinx.coroutines.launch
2313
import javax.inject.Inject
2414

2515
@HiltViewModel
2616
class LabsScreenViewModel @Inject constructor(
27-
private val userManager: UserManager,
17+
userManager: UserManager,
2818
userFlags: UserFlagsCoordinator,
29-
private val contactController: ContactVerificationController,
30-
private val resources: ResourceHelper,
3119
) : ViewModel() {
3220

3321
val isLoggedIn = userManager
@@ -38,52 +26,4 @@ class LabsScreenViewModel @Inject constructor(
3826

3927
val isStaff = userFlags.resolvedFlags.map { it.isStaff.effectiveValue }
4028
.stateIn(viewModelScope, started = SharingStarted.Eagerly, initialValue = false)
41-
42-
fun unlinkEmail() = viewModelScope.launch {
43-
val email = userManager.profile?.verifiedEmailAddress
44-
if (email == null) {
45-
BottomBarManager.showAlert(
46-
title = resources.getString(R.string.error_title_failedToUnlinkEmail),
47-
message = resources.getString(R.string.error_description_failedToUnlinkEmailNonePresent)
48-
)
49-
return@launch
50-
}
51-
val method = ContactMethod.Email(email)
52-
contactController.unlink(method)
53-
.onFailure {
54-
BottomBarManager.showError(
55-
title = resources.getString(R.string.error_title_failedToUnlinkEmail),
56-
message = resources.getString(R.string.error_description_failedToUnlinkEmail)
57-
)
58-
}.onSuccess {
59-
BottomBarManager.showSuccess(
60-
title = resources.getString(R.string.prompt_title_emailUnlinked),
61-
message = resources.getString(R.string.prompt_description_emailUnlinked),
62-
)
63-
}
64-
}
65-
66-
fun unlinkPhone() = viewModelScope.launch {
67-
val phone = userManager.profile?.verifiedPhoneNumber
68-
if (phone == null) {
69-
BottomBarManager.showAlert(
70-
title = resources.getString(R.string.error_title_failedToUnlinkPhone),
71-
message = resources.getString(R.string.error_description_failedToUnlinkPhoneNonePresent)
72-
)
73-
return@launch
74-
}
75-
val method = ContactMethod.Phone(phone)
76-
contactController.unlink(method)
77-
.onFailure {
78-
BottomBarManager.showError(
79-
title = resources.getString(R.string.error_title_failedToUnlinkPhone),
80-
message = resources.getString(R.string.error_description_failedToUnlinkPhone)
81-
)
82-
}.onSuccess {
83-
BottomBarManager.showSuccess(
84-
title = resources.getString(R.string.prompt_title_phoneUnlinked),
85-
message = resources.getString(R.string.prompt_description_phoneUnlinked),
86-
)
87-
}
88-
}
89-
}
29+
}

apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/MyAccountScreen.kt

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,30 +77,8 @@ fun MyAccountScreen() {
7777

7878
LaunchedEffect(viewModel) {
7979
viewModel.eventFlow
80-
.filterIsInstance<MyAccountScreenViewModel.Event.ConnectPhoneClicked>()
81-
.onEach {
82-
val flow = AppRoute.Verification(
83-
origin = AppRoute.Menu.MyAccount,
84-
includePhone = true,
85-
includeEmail = false,
86-
linkForPayment = it.linkForPayment
87-
)
88-
89-
navigator.push(flow) }
90-
.launchIn(this)
91-
}
92-
93-
LaunchedEffect(viewModel) {
94-
viewModel.eventFlow
95-
.filterIsInstance<MyAccountScreenViewModel.Event.OnVerifyEmailClicked>()
96-
.onEach {
97-
val flow = AppRoute.Verification(
98-
origin = AppRoute.Menu.MyAccount,
99-
includePhone = false,
100-
includeEmail = true,
101-
)
102-
103-
navigator.push(flow) }
80+
.filterIsInstance<MyAccountScreenViewModel.Event.OnViewUserProfile>()
81+
.onEach { navigator.push(AppRoute.Menu.UserProfile) }
10482
.launchIn(this)
10583
}
10684
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.flipcash.app.myaccount
2+
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.getValue
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.res.stringResource
11+
import androidx.hilt.navigation.compose.hiltViewModel
12+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
13+
import com.flipcash.app.core.AppRoute
14+
import com.flipcash.app.myaccount.internal.UserProfileScreenContent
15+
import com.flipcash.app.myaccount.internal.UserProfileViewModel
16+
import com.flipcash.core.R
17+
import com.getcode.navigation.core.LocalCodeNavigator
18+
import com.getcode.ui.components.AppBarDefaults
19+
import com.getcode.ui.components.AppBarWithTitle
20+
import kotlinx.coroutines.flow.filterIsInstance
21+
import kotlinx.coroutines.flow.launchIn
22+
import kotlinx.coroutines.flow.onEach
23+
24+
@Composable
25+
fun UserProfileScreen() {
26+
val navigator = LocalCodeNavigator.current
27+
val viewModel = hiltViewModel<UserProfileViewModel>()
28+
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
29+
30+
Column(
31+
modifier = Modifier.fillMaxSize(),
32+
horizontalAlignment = Alignment.CenterHorizontally,
33+
) {
34+
AppBarWithTitle(
35+
title = {
36+
AppBarDefaults.Title(
37+
text = stringResource(R.string.title_userProfile),
38+
)
39+
},
40+
isInModal = true,
41+
titleAlignment = Alignment.CenterHorizontally,
42+
leftIcon = { AppBarDefaults.UpNavigation { navigator.pop() } },
43+
)
44+
UserProfileScreenContent(state = state, dispatch = viewModel::dispatchEvent)
45+
}
46+
47+
LaunchedEffect(viewModel) {
48+
viewModel.eventFlow
49+
.filterIsInstance<UserProfileViewModel.Event.NavigateToPhoneVerification>()
50+
.onEach {
51+
navigator.push(
52+
AppRoute.Verification(
53+
origin = AppRoute.Menu.UserProfile,
54+
includePhone = true,
55+
includeEmail = false,
56+
linkForPayment = state.phoneLinkedForPayment,
57+
)
58+
)
59+
}.launchIn(this)
60+
}
61+
62+
LaunchedEffect(viewModel) {
63+
viewModel.eventFlow
64+
.filterIsInstance<UserProfileViewModel.Event.NavigateToEmailVerification>()
65+
.onEach {
66+
navigator.push(
67+
AppRoute.Verification(
68+
origin = AppRoute.Menu.UserProfile,
69+
includePhone = false,
70+
includeEmail = true,
71+
)
72+
)
73+
}.launchIn(this)
74+
}
75+
}

apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/MyAccountMenuItems.kt

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package com.flipcash.app.myaccount.internal
22

33
import androidx.compose.material.icons.Icons
4-
import androidx.compose.material.icons.filled.Email
5-
import androidx.compose.material.icons.filled.Phone
4+
import androidx.compose.material.icons.filled.ContactMail
65
import androidx.compose.runtime.Composable
76
import androidx.compose.ui.graphics.painter.Painter
87
import androidx.compose.ui.graphics.vector.ImageVector
98
import androidx.compose.ui.graphics.vector.rememberVectorPainter
109
import androidx.compose.ui.res.painterResource
1110
import androidx.compose.ui.res.stringResource
1211
import com.flipcash.app.menu.FullMenuItem
12+
import com.flipcash.app.menu.StaffMenuItem
13+
import com.flipcash.core.R as CoreR
1314
import com.flipcash.features.myaccount.R
1415
import com.getcode.util.resources.icons.Delete
1516

@@ -21,20 +22,12 @@ internal data object AccessKey : FullMenuItem<MyAccountScreenViewModel.Event>()
2122
override val action: MyAccountScreenViewModel.Event = MyAccountScreenViewModel.Event.OnAccessKeyClicked
2223
}
2324

24-
internal data object VerifyEmail : FullMenuItem<MyAccountScreenViewModel.Event>() {
25+
internal data object UserProfile : StaffMenuItem<MyAccountScreenViewModel.Event>() {
2526
override val icon: Painter
26-
@Composable get() = rememberVectorPainter(Icons.Default.Email)
27+
@Composable get() = rememberVectorPainter(Icons.Default.ContactMail)
2728
override val name: String
28-
@Composable get() = stringResource(R.string.title_connectEmailAddress)
29-
override val action: MyAccountScreenViewModel.Event = MyAccountScreenViewModel.Event.OnVerifyEmailClicked
30-
}
31-
32-
internal data object VerifyPhone : FullMenuItem<MyAccountScreenViewModel.Event>() {
33-
override val icon: Painter
34-
@Composable get() = rememberVectorPainter(Icons.Default.Phone)
35-
override val name: String
36-
@Composable get() = stringResource(R.string.title_connectPhoneNumber)
37-
override val action: MyAccountScreenViewModel.Event = MyAccountScreenViewModel.Event.OnVerifyPhoneClicked
29+
@Composable get() = stringResource(CoreR.string.title_userProfile)
30+
override val action: MyAccountScreenViewModel.Event = MyAccountScreenViewModel.Event.OnContactMethodsClicked
3831
}
3932

4033
internal data object LogOut : FullMenuItem<MyAccountScreenViewModel.Event>() {

0 commit comments

Comments
 (0)