From 22dd2ce75388d41054468a31e6b23b41187c4786 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 6 Oct 2026 13:25:42 -0400 Subject: [PATCH 1/3] feat(profile): call it a profile card in user-facing copy Scan prompts, the tutorial step, the scan error alert, the setup card screen and the unnamed export file now say "Profile Card". Identifiers and analytics event names keep tip card; the code rename is a later chore. Delete UsernameProgressCard: Edit Profile's Username card replaced it and nothing references it. --- .../Deep Links/DeepLinkController.swift | 2 +- .../Core/Screens/Main/CameraPromptView.swift | 6 +- .../Screens/Main/Home/NewUserTutorial.swift | 2 +- .../Core/Screens/Main/Home/WalletScreen.swift | 2 +- Flipcash/Core/Screens/Main/Tips/TipFlow.swift | 2 +- .../Screens/Main/Tips/TipcardScreen.swift | 4 +- .../Main/Username/UsernameProgressCard.swift | 112 ------------------ .../Core/Screens/Main/You/TipCardExport.swift | 2 +- FlipcashTests/TipCardExportTests.swift | 2 +- FlipcashTests/TipFlowFailureDialogTests.swift | 2 +- 10 files changed, 12 insertions(+), 124 deletions(-) delete mode 100644 Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift diff --git a/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift b/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift index 9d58e58f5..089cee138 100644 --- a/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift +++ b/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift @@ -326,7 +326,7 @@ struct DeepLinkAction { private static func showLookupFailed(isScan: Bool, on session: Session) { session.dialogItem = .error( - title: isScan ? "Couldn't Open Tip Card" : "Couldn't Open Profile", + title: isScan ? "Couldn't Open Profile Card" : "Couldn't Open Profile", subtitle: "Please check your connection and try again" ) } diff --git a/Flipcash/Core/Screens/Main/CameraPromptView.swift b/Flipcash/Core/Screens/Main/CameraPromptView.swift index 0a013823a..01f951b58 100644 --- a/Flipcash/Core/Screens/Main/CameraPromptView.swift +++ b/Flipcash/Core/Screens/Main/CameraPromptView.swift @@ -49,13 +49,13 @@ nonisolated enum CameraPrompt: Equatable { func message(embedded: Bool) -> String { switch self { case .requestPermission: - embedded ? "Start your camera to scan a Tip Card" + embedded ? "Start your camera to scan a Profile Card" : "Flipcash uses your camera to scan and grab cash" case .openSettings: - embedded ? "Turn on Camera in Settings to scan a Tip Card" + embedded ? "Turn on Camera in Settings to scan a Profile Card" : "You need to turn on Camera in Settings to scan Codes" case .startCamera: - embedded ? "Start your camera to scan a Tip Card" + embedded ? "Start your camera to scan a Profile Card" : "You need to start your camera to grab cash" } } diff --git a/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift b/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift index 7f216b0a2..36fbf15c8 100644 --- a/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift +++ b/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift @@ -23,7 +23,7 @@ nonisolated enum TutorialItem: TutorialItemPresentable { var title: String { switch self { case .addMoney: return "Add Money" - case .scanTipCard: return "Scan a Tip Card" + case .scanTipCard: return "Scan a Profile Card" } } diff --git a/Flipcash/Core/Screens/Main/Home/WalletScreen.swift b/Flipcash/Core/Screens/Main/Home/WalletScreen.swift index 773d11c02..412f070da 100644 --- a/Flipcash/Core/Screens/Main/Home/WalletScreen.swift +++ b/Flipcash/Core/Screens/Main/Home/WalletScreen.swift @@ -15,7 +15,7 @@ struct WalletScreen: View { @Environment(SessionContainer.self) private var sessionContainer - /// Invoked by the tutorial's "Scan a Tip Card" step to switch to the Scan tab. + /// Invoked by the tutorial's "Scan a Profile Card" step to switch to the Scan tab. let onScanTipCard: () -> Void var body: some View { diff --git a/Flipcash/Core/Screens/Main/Tips/TipFlow.swift b/Flipcash/Core/Screens/Main/Tips/TipFlow.swift index 9c0f01e48..be912356b 100644 --- a/Flipcash/Core/Screens/Main/Tips/TipFlow.swift +++ b/Flipcash/Core/Screens/Main/Tips/TipFlow.swift @@ -172,7 +172,7 @@ final class TipFlow { /// read, so a miss is a fetch that didn't land rather than a wrong address. static var failureDialog: DialogItem { .error( - title: "Couldn't Open Tip Card", + title: "Couldn't Open Profile Card", subtitle: "Please check your connection and try again" ) } diff --git a/Flipcash/Core/Screens/Main/Tips/TipcardScreen.swift b/Flipcash/Core/Screens/Main/Tips/TipcardScreen.swift index 86583f51c..50dee51ff 100644 --- a/Flipcash/Core/Screens/Main/Tips/TipcardScreen.swift +++ b/Flipcash/Core/Screens/Main/Tips/TipcardScreen.swift @@ -28,7 +28,7 @@ struct TipcardScreen: View { Background(color: .backgroundMain) { legacyContent } - .navigationTitle("My Tip Card") + .navigationTitle("My Profile Card") .toolbarTitleDisplayMode(.inline) .task(id: profilePicture?.thumbnailBlobID) { previewCache.warm(TipCode.Payload(userID: sessionContainer.session.userID)) @@ -39,7 +39,7 @@ struct TipcardScreen: View { private var legacyContent: some View { VStack(spacing: 0) { - Text("Share Your Tip Card to Get Tipped") + Text("Share Your Profile Card to Get Tipped") .font(.appTextLarge) .foregroundStyle(Color.textMain) .multilineTextAlignment(.center) diff --git a/Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift b/Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift deleted file mode 100644 index 97a494ed4..000000000 --- a/Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// UsernameProgressCard.swift -// Flipcash -// - -import SwiftUI -import FlipcashUI - -/// The You-tab card offering a username, in the tip-card action group below -/// Share/Download (Figma nodes 9536:4336 / 9537:1845). Superseded the -/// settings-row entry point: `YouScreen` shows it only while -/// `shouldPromptForUsername` holds, in place of the row it used to draw. -/// -/// Takes plain values rather than reading `Session`/`UsernameGate` itself, so -/// it can be previewed and tested without a signed-in account — the caller -/// (`YouScreen`) is the one place that has both. -struct UsernameProgressCard: View { - - /// Which of the two states the card draws. The title never changes; the - /// state governs the bar's colour, the chevron, and where the tap lands. - enum State: Equatable { - /// Below the server's minimum (node 9536:4336): white bar, the - /// shortfall in place of a chevron. Tapping raises the balance gate. - case incomplete - /// At or above it (node 9537:1845): green bar, chevron. Tapping opens - /// the claim screen. - case complete - } - - private static let title = "Get a custom @username" - - let state: State - let subtitle: String - /// The shortfall, shown only in `.incomplete`. `nil` in `.complete`. - let trailing: String? - /// The balance's progress toward the minimum, `0...1`. - let fraction: Double - /// Runs on tap. Both states are tappable: the card is the entry point to - /// the flow either way, and below the minimum that means being told the - /// rule rather than being left with an inert bar. - let action: VoidAction - - var body: some View { - Button(action: action) { - chrome - } - .buttonStyle(.plain) - .accessibilityLabel(Self.title) - // The shortfall reads as part of the hint rather than as a value: it - // is the same sentence the subtitle starts, not a separate reading. - .accessibilityHint(trailing.map { "\(subtitle). \($0)" } ?? subtitle) - } - - private var chrome: some View { - VStack(alignment: .leading, spacing: 6) { - HStack(alignment: .firstTextBaseline, spacing: 8) { - Text(Self.title) - .font(.default(size: 14, weight: .semibold)) - .foregroundStyle(Color.textMain) - - if let trailing { - Spacer(minLength: 8) - Text(trailing) - .font(.default(size: 14, weight: .semibold)) - .foregroundStyle(Color.textMain.opacity(0.5)) - } - - if state == .complete { - Spacer(minLength: 8) - Image.system(.chevronRight) - .renderingMode(.template) - .frame(width: 20, height: 20) - .foregroundStyle(Color.textMain) - } - } - - Text(subtitle) - .font(.default(size: 12, weight: .medium)) - .foregroundStyle(Color.textMain.opacity(0.5)) - - Spacer(minLength: 0) - - ProgressTrack(fraction: fraction, fill: state == .complete ? .success : Color.white) - } - .padding(.top, 12) - .padding(.bottom, 15) - .padding(.horizontal, 15) - .frame(height: 88) - .background(Color.backgroundRow, in: RoundedRectangle(cornerRadius: 6, style: .continuous)) - } -} - -/// The card's progress bar: a 10%-white track with a fill sized to `fraction`. -private struct ProgressTrack: View { - - let fraction: Double - let fill: Color - - var body: some View { - GeometryReader { proxy in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 2, style: .continuous) - .fill(Color.white.opacity(0.1)) - - RoundedRectangle(cornerRadius: 2, style: .continuous) - .fill(fill) - .frame(width: proxy.size.width * CGFloat(max(0, min(1, fraction)))) - } - } - .frame(height: 6) - } -} diff --git a/Flipcash/Core/Screens/Main/You/TipCardExport.swift b/Flipcash/Core/Screens/Main/You/TipCardExport.swift index 6d74d8db0..63ea018c2 100644 --- a/Flipcash/Core/Screens/Main/You/TipCardExport.swift +++ b/Flipcash/Core/Screens/Main/You/TipCardExport.swift @@ -101,7 +101,7 @@ enum TipCardExport { .replacingOccurrences(of: ":", with: " ") .trimmingCharacters(in: .whitespacesAndNewlines) - let stem = if let subject, !subject.isEmpty { "Chat with \(subject)" } else { "Tip Card" } + let stem = if let subject, !subject.isEmpty { "Chat with \(subject)" } else { "Profile Card" } return "\(stem).\(format.rawValue)" } diff --git a/FlipcashTests/TipCardExportTests.swift b/FlipcashTests/TipCardExportTests.swift index fa419c618..448a6d735 100644 --- a/FlipcashTests/TipCardExportTests.swift +++ b/FlipcashTests/TipCardExportTests.swift @@ -45,7 +45,7 @@ struct TipCardExportTests { let url = try #require(TipCardExport.file(for: .svg, codeData: .placeholder35, name: nil)) defer { TipCardExport.discard(url) } - #expect(url.lastPathComponent == "Tip Card.svg") + #expect(url.lastPathComponent == "Profile Card.svg") } /// A display name is server-supplied text landing in a path component. diff --git a/FlipcashTests/TipFlowFailureDialogTests.swift b/FlipcashTests/TipFlowFailureDialogTests.swift index df3db0651..5a7fc05a2 100644 --- a/FlipcashTests/TipFlowFailureDialogTests.swift +++ b/FlipcashTests/TipFlowFailureDialogTests.swift @@ -17,7 +17,7 @@ struct TipFlowFailureDialogTests { func failedResolve_genericError() { let item = TipFlow.failureDialog #expect(item.style == .destructive) - #expect(item.title == "Couldn't Open Tip Card") + #expect(item.title == "Couldn't Open Profile Card") #expect(item.subtitle == "Please check your connection and try again") } } From 2729b52b5e256f997949e70373cd691046116f91 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 6 Oct 2026 13:28:20 -0400 Subject: [PATCH 2/3] feat(wallet): say "Send your first message" on the scan tutorial step --- Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift b/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift index 36fbf15c8..b867fae2d 100644 --- a/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift +++ b/Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift @@ -30,7 +30,7 @@ nonisolated enum TutorialItem: TutorialItemPresentable { var subtitle: String { switch self { case .addMoney: return "Add money to your account" - case .scanTipCard: return "Give your first tip" + case .scanTipCard: return "Send your first message" } } From 8d3cd64557271f44ac53c2b3aac79a7494fa6e2a Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 6 Oct 2026 14:12:57 -0400 Subject: [PATCH 3/3] revert(username): keep UsernameProgressCard for possible reuse It has no callers after the You tab redesign, but it may come back. --- .../Main/Username/UsernameProgressCard.swift | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift diff --git a/Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift b/Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift new file mode 100644 index 000000000..97a494ed4 --- /dev/null +++ b/Flipcash/Core/Screens/Main/Username/UsernameProgressCard.swift @@ -0,0 +1,112 @@ +// +// UsernameProgressCard.swift +// Flipcash +// + +import SwiftUI +import FlipcashUI + +/// The You-tab card offering a username, in the tip-card action group below +/// Share/Download (Figma nodes 9536:4336 / 9537:1845). Superseded the +/// settings-row entry point: `YouScreen` shows it only while +/// `shouldPromptForUsername` holds, in place of the row it used to draw. +/// +/// Takes plain values rather than reading `Session`/`UsernameGate` itself, so +/// it can be previewed and tested without a signed-in account — the caller +/// (`YouScreen`) is the one place that has both. +struct UsernameProgressCard: View { + + /// Which of the two states the card draws. The title never changes; the + /// state governs the bar's colour, the chevron, and where the tap lands. + enum State: Equatable { + /// Below the server's minimum (node 9536:4336): white bar, the + /// shortfall in place of a chevron. Tapping raises the balance gate. + case incomplete + /// At or above it (node 9537:1845): green bar, chevron. Tapping opens + /// the claim screen. + case complete + } + + private static let title = "Get a custom @username" + + let state: State + let subtitle: String + /// The shortfall, shown only in `.incomplete`. `nil` in `.complete`. + let trailing: String? + /// The balance's progress toward the minimum, `0...1`. + let fraction: Double + /// Runs on tap. Both states are tappable: the card is the entry point to + /// the flow either way, and below the minimum that means being told the + /// rule rather than being left with an inert bar. + let action: VoidAction + + var body: some View { + Button(action: action) { + chrome + } + .buttonStyle(.plain) + .accessibilityLabel(Self.title) + // The shortfall reads as part of the hint rather than as a value: it + // is the same sentence the subtitle starts, not a separate reading. + .accessibilityHint(trailing.map { "\(subtitle). \($0)" } ?? subtitle) + } + + private var chrome: some View { + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(Self.title) + .font(.default(size: 14, weight: .semibold)) + .foregroundStyle(Color.textMain) + + if let trailing { + Spacer(minLength: 8) + Text(trailing) + .font(.default(size: 14, weight: .semibold)) + .foregroundStyle(Color.textMain.opacity(0.5)) + } + + if state == .complete { + Spacer(minLength: 8) + Image.system(.chevronRight) + .renderingMode(.template) + .frame(width: 20, height: 20) + .foregroundStyle(Color.textMain) + } + } + + Text(subtitle) + .font(.default(size: 12, weight: .medium)) + .foregroundStyle(Color.textMain.opacity(0.5)) + + Spacer(minLength: 0) + + ProgressTrack(fraction: fraction, fill: state == .complete ? .success : Color.white) + } + .padding(.top, 12) + .padding(.bottom, 15) + .padding(.horizontal, 15) + .frame(height: 88) + .background(Color.backgroundRow, in: RoundedRectangle(cornerRadius: 6, style: .continuous)) + } +} + +/// The card's progress bar: a 10%-white track with a fill sized to `fraction`. +private struct ProgressTrack: View { + + let fraction: Double + let fill: Color + + var body: some View { + GeometryReader { proxy in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2, style: .continuous) + .fill(Color.white.opacity(0.1)) + + RoundedRectangle(cornerRadius: 2, style: .continuous) + .fill(fill) + .frame(width: proxy.size.width * CGFloat(max(0, min(1, fraction)))) + } + } + .frame(height: 6) + } +}