diff --git a/Flipcash/Core/Navigation/AppRouter+Destination.swift b/Flipcash/Core/Navigation/AppRouter+Destination.swift index 0a4a5e5eb..5e5b82053 100644 --- a/Flipcash/Core/Navigation/AppRouter+Destination.swift +++ b/Flipcash/Core/Navigation/AppRouter+Destination.swift @@ -50,6 +50,10 @@ extension AppRouter { // Settings flow case settingsMyAccount + /// The display name on its own, edited from My Account. The full + /// profile-setup flow starts on the same screen but carries on to the + /// tip card; this one returns to the settings list. + case changeDisplayName case settingsAdvancedFeatures case settingsAdvancedBetaFeatures case settingsAppSettings @@ -85,8 +89,8 @@ extension AppRouter { .transactionHistory, .activity, .give, .buyCurrency, .convertCurrency, .withdrawCurrency, .usdcDepositEducation, .usdcDepositAddress: return .balance - case .settingsMyAccount, .settingsAdvancedFeatures, .settingsAdvancedBetaFeatures, - .settingsAppSettings, .settingsAccountSelection, + case .settingsMyAccount, .changeDisplayName, .settingsAdvancedFeatures, + .settingsAdvancedBetaFeatures, .settingsAppSettings, .settingsAccountSelection, .settingsApplicationLogs, .blockedUsers, .accessKey, .withdraw: return .settings case .profileName, .profilePhoto, .tipcard, @@ -116,6 +120,7 @@ extension AppRouter { case .usdcDepositEducation: "usdcDepositEducation" case .usdcDepositAddress: "usdcDepositAddress" case .settingsMyAccount: "settingsMyAccount" + case .changeDisplayName: "changeDisplayName" case .settingsAdvancedFeatures: "settingsAdvancedFeatures" case .settingsAdvancedBetaFeatures: "settingsAdvancedBetaFeatures" case .settingsAppSettings: "settingsAppSettings" @@ -155,8 +160,8 @@ extension AppRouter { case .activity, .discoverCurrencies, .currencyCreationSummary, .currencyCreationWizard, .usdcDepositEducation, .usdcDepositAddress, - .settingsMyAccount, .settingsAdvancedFeatures, .settingsAdvancedBetaFeatures, - .settingsAppSettings, .settingsAccountSelection, + .settingsMyAccount, .changeDisplayName, .settingsAdvancedFeatures, + .settingsAdvancedBetaFeatures, .settingsAppSettings, .settingsAccountSelection, .settingsApplicationLogs, .blockedUsers, .accessKey, .withdraw, .profileName, .profilePhoto, .tipcard: return nil diff --git a/Flipcash/Core/Navigation/AppRouter+DestinationView.swift b/Flipcash/Core/Navigation/AppRouter+DestinationView.swift index ddf69c900..0a9079150 100644 --- a/Flipcash/Core/Navigation/AppRouter+DestinationView.swift +++ b/Flipcash/Core/Navigation/AppRouter+DestinationView.swift @@ -70,6 +70,9 @@ struct DestinationView: View { case .settingsMyAccount: SettingsMyAccountScreen() + case .changeDisplayName: + ChangeDisplayNameScreen(currentName: sessionContainer.session.profile?.displayName ?? "") + case .settingsAdvancedFeatures: SettingsAdvancedFeaturesScreen() diff --git a/Flipcash/Core/Screens/Main/Profile/ProfileNameScreen.swift b/Flipcash/Core/Screens/Main/Profile/ProfileNameScreen.swift index df66eea33..6ed31592e 100644 --- a/Flipcash/Core/Screens/Main/Profile/ProfileNameScreen.swift +++ b/Flipcash/Core/Screens/Main/Profile/ProfileNameScreen.swift @@ -11,6 +11,16 @@ private let logger = Logger(label: "flipcash.profile-name") struct ProfileNameScreen: View { + /// Where a saved name leads. + enum Completion { + /// Profile setup: on to the tip card the name unlocks. + case tipcard + /// A lone edit: back to the screen that opened this one. + case back + } + + var completion: Completion = .tipcard + @Environment(Container.self) private var container @Environment(SessionContainer.self) private var sessionContainer @Environment(AppRouter.self) private var router @@ -96,13 +106,20 @@ struct ProfileNameScreen: View { ) try await sessionContainer.session.updateProfile() - // `push` resolves the stack when it runs, and this runs after two - // RPCs — by now the user may have swapped to another sheet, whose - // stack has no profile-creation state to mount against. - guard !Task.isCancelled, router.presentedSheet?.stack == .tips else { return } - // The photo-capture step is skipped for now — the card omits the - // profile photo, so setup goes straight from the name to the card. - router.push(.tipcard) + guard !Task.isCancelled else { return } + + switch completion { + case .tipcard: + // `push` resolves the stack when it runs, and this runs after two + // RPCs — by now the user may have swapped to another sheet, whose + // stack has no profile-creation state to mount against. + guard router.presentedSheet?.stack == .tips else { return } + // The photo-capture step is skipped for now — the card omits the + // profile photo, so setup goes straight from the name to the card. + router.push(.tipcard) + case .back: + router.popTopmost() + } } catch ErrorProfile.moderated(let category) { logger.info("Display name moderation denied", metadata: ["category": "\(category)"]) diff --git a/Flipcash/Core/Screens/Settings/ChangeDisplayNameScreen.swift b/Flipcash/Core/Screens/Settings/ChangeDisplayNameScreen.swift new file mode 100644 index 000000000..853489f05 --- /dev/null +++ b/Flipcash/Core/Screens/Settings/ChangeDisplayNameScreen.swift @@ -0,0 +1,29 @@ +// +// ChangeDisplayNameScreen.swift +// Flipcash +// + +import SwiftUI + +/// Editing the display name on its own, reached from My Account (node +/// 9277:121893). Hosts the profile-setup name step, which returns here once the +/// name saves instead of carrying on to the tip card. +struct ChangeDisplayNameScreen: View { + + /// The name step reads its text from the environment: profile setup owns + /// that state at its sheet root so the photo step can read the name back. + /// A lone edit has no following step, so it owns the state itself, seeded + /// with the name already on the profile. + @State private var creationState: ProfileCreationState + + init(currentName: String) { + let state = ProfileCreationState() + state.displayName = currentName + _creationState = State(initialValue: state) + } + + var body: some View { + ProfileNameScreen(completion: .back) + .environment(creationState) + } +} diff --git a/Flipcash/Core/Screens/Settings/SettingsMyAccountScreen.swift b/Flipcash/Core/Screens/Settings/SettingsMyAccountScreen.swift index f2a73e9d2..c1930fb36 100644 --- a/Flipcash/Core/Screens/Settings/SettingsMyAccountScreen.swift +++ b/Flipcash/Core/Screens/Settings/SettingsMyAccountScreen.swift @@ -33,6 +33,10 @@ struct SettingsMyAccountScreen: View { @ViewBuilder private func list() -> some View { VStack(alignment: .leading, spacing: 0) { + SettingsRow(asset: .profile, title: "Change Display Name", insets: insets) { + router.push(.changeDisplayName) + } + SettingsRow(systemImage: "nosign", title: "Blocked", insets: insets) { router.push(.blockedUsers) } diff --git a/FlipcashTests/Navigation/AppRouterCrossStackTests.swift b/FlipcashTests/Navigation/AppRouterCrossStackTests.swift index 4e5c0d88e..cd72387f8 100644 --- a/FlipcashTests/Navigation/AppRouterCrossStackTests.swift +++ b/FlipcashTests/Navigation/AppRouterCrossStackTests.swift @@ -96,6 +96,7 @@ struct AppRouterCrossStackTests { (AppRouter.Destination.usdcDepositEducation, AppRouter.Stack.balance), (AppRouter.Destination.usdcDepositAddress, AppRouter.Stack.balance), (AppRouter.Destination.settingsMyAccount, AppRouter.Stack.settings), + (AppRouter.Destination.changeDisplayName, AppRouter.Stack.settings), (AppRouter.Destination.settingsAdvancedFeatures, AppRouter.Stack.settings), (AppRouter.Destination.settingsAppSettings, AppRouter.Stack.settings), (AppRouter.Destination.settingsAdvancedBetaFeatures, AppRouter.Stack.settings), diff --git a/FlipcashTests/Navigation/YouTabRoutingTests.swift b/FlipcashTests/Navigation/YouTabRoutingTests.swift index a8ff36b41..7dcaf0406 100644 --- a/FlipcashTests/Navigation/YouTabRoutingTests.swift +++ b/FlipcashTests/Navigation/YouTabRoutingTests.swift @@ -26,4 +26,17 @@ struct YouTabRoutingTests { #expect(router[.you] == AppRouter.navigationPath(.settingsMyAccount)) #expect(router[.settings].isEmpty) // never leaks onto the Settings sheet's stack } + + @Test("changing the display name pushes onto the You tab, and saving pops back") + func changeDisplayName_pushesAndPopsOnYouStack() { + let router = AppRouter() + router.activeTabStack = .you + router.push(.settingsMyAccount) + router.push(.changeDisplayName) + #expect(router[.you] == AppRouter.navigationPath(.settingsMyAccount, .changeDisplayName)) + + // What `ProfileNameScreen(completion: .back)` runs once the name saves. + router.popTopmost() + #expect(router[.you] == AppRouter.navigationPath(.settingsMyAccount)) + } }