Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions Flipcash/Core/Navigation/AppRouter+Destination.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,26 @@ extension AppRouter {
case usdcDepositAddress

// Settings flow
case settingsMyAccount
/// The display name on its own, edited from My Account. The full
/// The Settings list, opened from the gear on the You tab.
case settings
/// The account's phone, email, user ID and public key, opened from Settings.
case accountInfo
/// The display name on its own, edited from Settings. 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
/// The profile picture on its own, changed from the You tab's checklist.
/// The full profile-setup flow uses `.profilePhoto`, which carries on to
/// the tip card; this one returns to the screen that opened it.
case changeProfilePicture
/// The public handle, claimed from the You page or changed from My
/// Account. One destination for both: the screen seeds itself from the
/// The public handle, claimed from the You page or changed from
/// Settings. One destination for both: the screen seeds itself from the
/// handle already on the profile, so there is nothing to distinguish.
case username(Username?)
/// The minimum a tipper must pay to open a DM. `isSetupStep` marks the
/// You tab's checklist apart from a lone edit in My Account; it reaches
/// You tab's checklist apart from a lone edit in Settings; it reaches
/// the payload only, since the screen is the same from either.
case setMinimumTip(isSetupStep: Bool)
case settingsAdvancedFeatures
case settingsAdvancedBetaFeatures
case settingsAppSettings
case settingsAccountSelection
Expand Down Expand Up @@ -141,8 +143,8 @@ extension AppRouter {
.buyCurrency, .convertCurrency,
.withdrawCurrency, .usdcDepositEducation, .usdcDepositAddress:
return .balance
case .settingsMyAccount, .changeDisplayName, .changeProfilePicture, .username,
.setMinimumTip, .settingsAdvancedFeatures,
case .settings, .accountInfo, .changeDisplayName, .changeProfilePicture, .username,
.setMinimumTip,
.settingsAdvancedBetaFeatures, .settingsAppSettings, .settingsAccountSelection,
.settingsApplicationLogs, .blockedUsers, .accessKey, .withdraw:
return .you
Expand Down Expand Up @@ -176,12 +178,12 @@ extension AppRouter {
case .withdrawCurrency: "withdrawCurrency"
case .usdcDepositEducation: "usdcDepositEducation"
case .usdcDepositAddress: "usdcDepositAddress"
case .settingsMyAccount: "settingsMyAccount"
case .settings: "settings"
case .accountInfo: "accountInfo"
case .changeDisplayName: "changeDisplayName"
case .changeProfilePicture: "changeProfilePicture"
case .username: "username"
case .setMinimumTip: "setMinimumTip"
case .settingsAdvancedFeatures: "settingsAdvancedFeatures"
case .settingsAdvancedBetaFeatures: "settingsAdvancedBetaFeatures"
case .settingsAppSettings: "settingsAppSettings"
case .settingsAccountSelection: "settingsAccountSelection"
Expand Down Expand Up @@ -243,8 +245,7 @@ extension AppRouter {
case .activity,
.discoverCurrencies, .currencyCreationSummary, .currencyCreationWizard,
.usdcDepositEducation, .usdcDepositAddress,
.settingsMyAccount, .changeDisplayName, .changeProfilePicture,
.settingsAdvancedFeatures,
.settings, .accountInfo, .changeDisplayName, .changeProfilePicture,
.settingsAdvancedBetaFeatures, .settingsAppSettings, .settingsAccountSelection,
.settingsApplicationLogs, .blockedUsers, .accessKey, .withdraw,
.profileName, .profilePhoto, .tipcard, .usernameLookup, .newChat, .newPublicGroup,
Expand Down
8 changes: 4 additions & 4 deletions Flipcash/Core/Navigation/AppRouter+DestinationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ struct DestinationView: View {

// MARK: - Settings flow

case .settingsMyAccount:
SettingsMyAccountScreen()
case .settings:
SettingsScreen()
case .accountInfo:
AccountInfoScreen()

case .changeDisplayName:
ChangeDisplayNameScreen(currentName: sessionContainer.session.profile?.displayName ?? "")
Expand All @@ -106,8 +108,6 @@ struct DestinationView: View {
)
)

case .settingsAdvancedFeatures:
SettingsAdvancedFeaturesScreen()

case .settingsAdvancedBetaFeatures:
SettingsAdvancedBetaFeaturesScreen()
Expand Down
103 changes: 34 additions & 69 deletions Flipcash/Core/Screens/Main/You/YouScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,30 @@ import FlipcashUI

/// The "You" tab (Figma node 9276:4634): the user's tip card, a "Full Screen"
/// affordance, the copyable public link, the Share/Download pair, and the
/// settings list — the tab-bar entry into My Account / Advanced.
/// gear that pushes Settings.
///
/// Tapping the card (or "Full Screen") expands it here on the page: everything
/// under it slides out, the tab bar hides, and the card grows into the middle of
/// the screen with a close button at the bottom. The page owns that transition —
/// nothing is pushed or presented — so the card never leaves this screen.
/// Pulling the expanded card up walks it back to its slot under the finger, so
/// it can be put away without reaching for the close button.
/// Settings rows push onto the tab's `.you` stack, so they never touch the v1
/// Settings pushes onto the tab's `.you` stack, so it never touches the v1
/// scanner's Settings sheet.
///
/// A profile with no display name has no card: the page then shows the
/// add-your-name invitation in the card's place and drops the link and share
/// affordances, but still renders — the settings rows are this account's only
/// way to reach My Account, and with it Log Out.
///
/// The two settings rows carry Android's own drawables (`MenuItems.kt`) rather
/// than SF Symbols: `person.text.rectangle` is already the Display Name row's
/// glyph one screen down, so the nearest system symbol would read as a repeat.
/// affordances, but still renders — the gear is this account's only way to
/// reach Settings, and with it Log Out.
struct YouScreen: View {

@Environment(SessionContainer.self) private var sessionContainer
@Environment(AppRouter.self) private var router
@Environment(BetaFlags.self) private var betaFlags
@Environment(TipCardPresentation.self) private var presentation

/// Warms the share-sheet preview image ahead of the share tap so it never
/// lands on the tap; keyed by user.
@State private var previewCache = TipCodePreviewCache()
/// The version footer's beta-access easter egg — the tap count and the line
/// it shows for the last few taps.
@State private var versionUnlock = VersionTapUnlock()
@Environment(ToastController.self) private var toasts
@State private var isShowingDownloadOptions = false

/// The format tapped in the download sheet, held until the sheet is gone so
Expand Down Expand Up @@ -94,9 +85,7 @@ struct YouScreen: View {
/// the card picks up from where it stands instead of jumping that distance.
private static let dragActivationSlack: CGFloat = 10

private let rowInsets = EdgeInsets(top: 25, leading: 0, bottom: 25, trailing: 0)

/// The gap the page keeps between the version footer and the tab bar.
/// The gap the page keeps between its last row and the tab bar.
private static let tabBarGap: CGFloat = 24

/// How far below the safe area the top fade runs out. Matches `WalletScreen`.
Expand Down Expand Up @@ -157,6 +146,13 @@ struct YouScreen: View {
}
.onGeometryChange(for: CGFloat.self) { $0.safeAreaInsets.top } action: { safeAreaTop = $0 }

// Sibling of the scroll view, not an overlay on it: the ZStack
// already sits below the status bar, so no `safeAreaTop` offset.
settingsGear
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.opacity(isExpanded ? 0 : 1)
.allowsHitTesting(!isExpanded)

if isExpanded {
closeButton
.transition(.opacity)
Expand Down Expand Up @@ -191,7 +187,7 @@ struct YouScreen: View {
///
/// A profile without a display name has no card to draw, so the slot carries
/// the invitation to add one instead. The rest of the page stays where it is
/// either way — settings included, which is the only route to logging out.
/// either way — the gear included, which is the only route to logging out.
@ViewBuilder
private var cardSection: some View {
if let name = displayName {
Expand Down Expand Up @@ -348,14 +344,32 @@ struct YouScreen: View {
.opacity(0.5)
}

// MARK: - Settings gear -

private var settingsGear: some View {
Button {
router.push(.settings)
} label: {
Image(systemName: "gearshape")
.font(.system(size: 20))
.foregroundStyle(Color.textMain)
.frame(width: 44, height: 44)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.padding(.top, 4)
.padding(.trailing, 8)
.accessibilityLabel("Settings")
.accessibilityIdentifier("you-settings")
}

// MARK: - Page -

/// Everything below the card — the part that clears out when the card expands.
///
/// The link and the Share/Download pair all address a card that a name-less
/// profile does not have, so they sit out until it does; the settings rows
/// take up the slack. The checklist shows either way, since naming the
/// profile is one of its chores.
/// profile does not have, so they sit out until it does. The checklist
/// shows either way, since naming the profile is one of its chores.
private var pageContent: some View {
VStack(spacing: 0) {
if profileTutorialState.isVisible {
Expand Down Expand Up @@ -395,30 +409,7 @@ struct YouScreen: View {
.accessibilityIdentifier("you-username-progress-card")
}
}

settingsList
.padding(.top, displayName == nil && !profileTutorialState.isVisible ? 48 : 19)

// v2 scrolls the version string with the content rather than
// pinning it above the tab bar (the v1 Settings sheet pinned it).
versionFooter
.padding(.top, 32)
}
}

// MARK: - Settings list -

private var settingsList: some View {
VStack(alignment: .leading, spacing: 0) {
SettingsRow(asset: .myAccount, title: "My Account", insets: rowInsets) {
router.push(.settingsMyAccount)
}
SettingsRow(asset: .advanced, title: "Advanced", insets: rowInsets) {
router.push(.settingsAdvancedFeatures)
}
}
.font(.appDisplayXS)
.foregroundStyle(Color.textMain)
}

// MARK: - Username progress -
Expand Down Expand Up @@ -456,32 +447,6 @@ struct YouScreen: View {
}
}

private var versionFooter: some View {
Button {
let message = versionUnlock.registerTap(isUnlocked: betaFlags.accessGranted) {
betaFlags.setAccessGranted(!betaFlags.accessGranted)
}
if let message {
// Two seconds and swapped in place, so consecutive taps read as one countdown. Without an action the toast
// lets taps through, so it never blocks the version string at the bottom of the list.
toasts.show(
.init(message, messageIdentifier: "you-version-toast", width: .fit, duration: .seconds(2)),
inPlace: true
)
}
} label: {
Text("Version \(AppMeta.version) • Build \(AppMeta.build)")
.lineLimit(1)
.font(.appTextHeading)
.foregroundStyle(Color.textSecondary)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityIdentifier("you-version-footer")
}

// MARK: - Content -

private var isExpanded: Bool { presentation.isExpanded }
Expand Down
63 changes: 63 additions & 0 deletions Flipcash/Core/Screens/Settings/AccountInfoDetails.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// AccountInfoDetails.swift
// Flipcash
//

import Foundation
import FlipcashCore

/// The rows on the Account Info screen, built from the account's identity.
///
/// Named `AccountInfoDetails` because `AccountInfo` is already the on-chain
/// token-account model in FlipcashCore.
nonisolated struct AccountInfoDetails {

/// One copyable line on Account Info: what the row shows, and what a tap
/// puts on the clipboard when the two differ.
struct Row: Hashable {
let title: String
let display: String
let copyValue: String
let accessibilityID: String
}

/// Phone and email only when the profile has them, then the account ID and
/// the owner authority's public key.
static func rows(profile: Profile?, userID: UserID, authority: PublicKey) -> [Row] {
var rows: [Row] = []

if let phone = profile?.phone {
rows.append(Row(
title: "Phone",
display: phone.national,
copyValue: phone.e164,
accessibilityID: "account-info-phone"
))
}

if let email = profile?.email, !email.isEmpty {
rows.append(Row(
title: "Email",
display: email,
copyValue: email,
accessibilityID: "account-info-email"
))
}

rows.append(Row(
title: "Account ID",
display: userID.uuidString,
copyValue: userID.uuidString,
accessibilityID: "account-info-account-id"
))

rows.append(Row(
title: "Public Key",
display: authority.base58,
copyValue: authority.base58,
accessibilityID: "account-info-public-key"
))

return rows
}
}
61 changes: 61 additions & 0 deletions Flipcash/Core/Screens/Settings/AccountInfoScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// AccountInfoScreen.swift
// Flipcash
//

import SwiftUI
import UIKit
import FlipcashUI
import FlipcashCore

/// The account's phone, email, user ID and owner public key. Tapping a row
/// copies its value.
struct AccountInfoScreen: View {

@Environment(SessionContainer.self) private var sessionContainer
@Environment(ToastController.self) private var toasts

var body: some View {
let session = sessionContainer.session
let rows = AccountInfoDetails.rows(
profile: session.profile,
userID: session.userID,
authority: session.owner.authorityPublicKey
)

Background(color: .backgroundMain) {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
ForEach(rows, id: \.self) { row in
Button {
copy(row)
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(row.title)
.font(.appTextHeading)
.foregroundStyle(Color.textSecondary)
Text(row.display)
.font(.appTextMedium)
.foregroundStyle(Color.textMain)
.multilineTextAlignment(.leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 16)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityIdentifier(row.accessibilityID)
}
}
}
.padding(.horizontal, 20)
}
.navigationTitle("Account Info")
.toolbarTitleDisplayMode(.inline)
}

private func copy(_ row: AccountInfoDetails.Row) {
UIPasteboard.general.string = row.copyValue
toasts.show(.init("Copied", systemImage: "checkmark.circle.fill", duration: .seconds(2)))
}
}
Loading
Loading