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
2 changes: 1 addition & 1 deletion Flipcash/Core/ContainerScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct ContainerScreen: View {

case .loggedIn(let sessionContainer):
ZStack {
HomeTabView(initialTab: sessionAuthenticator.landingTab)
HomeTabView()
.modifier(OnrampHostModifier())

// Bills / tipcards render at the app root, over the tab bar,
Expand Down
29 changes: 29 additions & 0 deletions Flipcash/Core/Screens/Main/Home/AddMoneyRowState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// AddMoneyRowState.swift
// Flipcash
//

/// Whether the wallet draws its Add Money row.
///
/// `hasAddedMoney` reads a *local* cache that starts empty on every fresh login —
/// switching accounts included — so it cannot be trusted until the history has
/// been reconciled with the server at least once. Without that wait, an
/// established account signing in is greeted with the row for as long as its
/// history takes to arrive. Mirrors Android's `WalletViewModel.State.isAwaitingActivity`.
struct AddMoneyRowState: Equatable {

let hasAddedMoney: Bool
let historySyncState: HistoryController.SyncState
/// Whether the local history already holds activity. Stored rows short-circuit
/// the wait: history that is already here is nothing to mistake for a new
/// account.
let hasStoredActivity: Bool

var isAwaitingHistory: Bool {
historySyncState == .unknown && !hasStoredActivity
}

/// Withheld while `hasAddedMoney` is still unknown — the row is never the
/// thing the wallet guesses at.
var isVisible: Bool { !isAwaitingHistory && !hasAddedMoney }
}
9 changes: 3 additions & 6 deletions Flipcash/Core/Screens/Main/Home/HomeTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import SwiftUI

/// The tabs of the v2 tab-bar UI, in display order (left → right). Chat sits
/// second, beside the scanner that feeds it. Declaration order is the bar's
/// order, not the launch tab; see ``initial`` and ``postOnboarding``. Matches Android's
/// order, not the launch tab; see ``initial``. Matches Android's
/// `NavBarButton.tabs`.
///
/// Icons are the Figma tab-bar glyphs (`Nav*` template imagesets, from the same
Expand All @@ -20,13 +20,10 @@ enum HomeTab: Int, CaseIterable, Identifiable, Hashable {

var id: Int { rawValue }

/// The tab selected on cold launch and after signing in to an existing
/// account.
/// The tab selected on cold launch, after signing in to an existing
/// account, and when onboarding finishes creating a new one.
static let initial: HomeTab = .chat

/// The tab selected when onboarding finishes creating a new account.
static let postOnboarding: HomeTab = .wallet

/// The asset-catalog name of the tab's template glyph. Selected tabs use a
/// filled glyph and the rest an outline, per the tab bar spec.
///
Expand Down
10 changes: 5 additions & 5 deletions Flipcash/Core/Screens/Main/Home/HomeTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import SwiftUI
import FlipcashCore
import FlipcashUI

/// The post-login root. Hosts the four tabs, opens on the tab it is given, and owns the
/// The post-login root. Hosts the four tabs, opens on `HomeTab.initial`, and owns the
/// app-level `router.rootSheet` host so `router.present(_:)` works from any tab.
///
/// On iOS 26 the tabs live in a native `TabView`, which renders the system
Expand Down Expand Up @@ -37,9 +37,9 @@ struct HomeTabView: View {
/// than by either bar, because both bars want the same download.
@State private var profilePhoto = TabBarProfilePhoto()

init(initialTab: HomeTab = .initial) {
_selection = State(initialValue: initialTab)
_hasOpenedChat = State(initialValue: initialTab == .chat)
init() {
_selection = State(initialValue: HomeTab.initial)
_hasOpenedChat = State(initialValue: HomeTab.initial == .chat)

// Color the tab glyphs white in both states, as iOS 27 already does; the
// outline-vs-filled glyph carries the selection. The iOS 26 tab bar
Expand Down Expand Up @@ -400,7 +400,7 @@ struct HomeTabView: View {
Color.backgroundMain
}
case .wallet:
WalletScreen(onScanTipCard: { selection = .scan })
WalletScreen()
.environment(cardExpansion)
case .chat:
// Building the conversation list costs 300–550 ms, so when the app
Expand Down
77 changes: 0 additions & 77 deletions Flipcash/Core/Screens/Main/Home/NewUserTutorial.swift

This file was deleted.

77 changes: 40 additions & 37 deletions Flipcash/Core/Screens/Main/Home/WalletScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ struct WalletScreen: View {

@Environment(SessionContainer.self) private var sessionContainer

/// Invoked by the tutorial's "Scan a Profile Card" step to switch to the Scan tab.
let onScanTipCard: () -> Void

var body: some View {
WalletScreenContent(sessionContainer: sessionContainer, onScanTipCard: onScanTipCard)
WalletScreenContent(sessionContainer: sessionContainer)
}
}

Expand All @@ -33,13 +30,10 @@ private struct WalletScreenContent: View {
@Environment(WalletCardExpansion.self) private var cardExpansion

let session: Session
let onScanTipCard: () -> Void

@State private var cards: [TokenCardData] = []
@State private var total: ExchangedFiat
@State private var appreciation: (amount: FiatAmount, isPositive: Bool)
@State private var hasAddedMoney: Bool
@State private var hasTipped: Bool
/// The unified recent-activity preview (newest first).
@State private var recentActivities: [Activity]

Expand Down Expand Up @@ -139,9 +133,8 @@ private struct WalletScreenContent: View {
return Self.tabBarGap + HomeTabView.legacyPillClearance
}

init(sessionContainer: SessionContainer, onScanTipCard: @escaping () -> Void) {
init(sessionContainer: SessionContainer) {
self.session = sessionContainer.session
self.onScanTipCard = onScanTipCard
let rate = sessionContainer.ratesController.rateForBalanceCurrency()
// Seed synchronously so the first render shows real balances, not an
// empty-state flash.
Expand All @@ -157,16 +150,14 @@ private struct WalletScreenContent: View {
_arrivalProgress = State(initialValue: landing?.isNewToken == true ? 0 : 1)
_appreciation = State(initialValue: seed.appreciation)
_hasAddedMoney = State(initialValue: sessionContainer.session.hasEverAddedMoney())
_hasTipped = State(initialValue: sessionContainer.session.hasEverTipped())
_recentActivities = State(initialValue: sessionContainer.session.recentActivities(limit: Self.recentPreviewCount))
}

private var rate: Rate { ratesController.rateForBalanceCurrency() }

private var tutorialState: NewUserTutorialState {
NewUserTutorialState(
private var addMoneyRowState: AddMoneyRowState {
AddMoneyRowState(
hasAddedMoney: hasAddedMoney,
hasTipped: hasTipped,
historySyncState: historyController.syncState,
hasStoredActivity: !recentActivities.isEmpty
)
Expand Down Expand Up @@ -247,7 +238,7 @@ private struct WalletScreenContent: View {
.onChange(of: rate) { _, _ in refresh() }
// Activities land in the DB independently of a balance change (e.g.
// a synced history page), and both the recent preview and the
// onboarding milestones are derived from them — so reload both on
// add-money milestone are derived from them — so reload both on
// any DB change rather than only when a balance moves.
.onReceive(NotificationCenter.default.publisher(for: .databaseDidChange)) { _ in
reloadHistoryState()
Expand Down Expand Up @@ -496,7 +487,7 @@ private struct WalletScreenContent: View {
private var walletContent: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
// Header + tutorial scroll off first; the card stack then reaches the
// Header + Add Money row scroll off first; the card stack then reaches the
// top and collapses into its back card. The stack measures the
// scroll itself (via `.visualEffect`), so no height plumbing here.
VStack(spacing: 0) {
Expand All @@ -506,13 +497,9 @@ private struct WalletScreenContent: View {
.padding(.top, headerTopPadding)
.padding(.bottom, 44)

if tutorialState.isVisible {
TutorialChecklistCard(
title: "Send Your First Tip",
items: tutorialState.items,
onTap: handleTutorialTap
)
.padding(.bottom, 20)
if addMoneyRowState.isVisible {
addMoneyRow
.padding(.bottom, 20)
}
}
// The stack is deliberately outside the fade: its cards carry
Expand Down Expand Up @@ -542,8 +529,8 @@ private struct WalletScreenContent: View {
}

// Returning users (already funded) get the tile shortcuts
// below the activity; new users use the tutorial's own steps
// instead.
// below the activity; new users get the Add Money row
// above instead.
if hasAddedMoney {
walletTiles
.padding(.top, 24)
Expand Down Expand Up @@ -621,9 +608,7 @@ private struct WalletScreenContent: View {
/// A tile-style entry point: icon top-leading, label pinned bottom-leading,
/// inside a translucent rounded card. They tile two-up per row.
///
/// `identifier` is what tests target: several of these titles also appear on
/// the new-user tutorial rows above, where a completed milestone renders
/// disabled, so matching on the label alone can resolve to an untappable row.
/// `identifier` is what tests target, so they do not depend on the label.
private func walletTile(icon: TileGlyph, title: String, identifier: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
VStack(alignment: .leading, spacing: 0) {
Expand Down Expand Up @@ -671,13 +656,34 @@ private struct WalletScreenContent: View {
.padding(.top, 24)
}

private func handleTutorialTap(_ item: TutorialItem) {
switch item {
case .addMoney:
private var addMoneyRow: some View {
Button {
router.presentAddMoney(.general, source: .balance)
case .scanTipCard:
onScanTipCard()
} label: {
HStack(alignment: .top, spacing: 12) {
Image(systemName: "plus.circle")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.foregroundStyle(Color.textSecondary)

VStack(alignment: .leading, spacing: 4) {
Text("Add Money")
.font(.appTextMedium)
.foregroundStyle(Color.textMain)
Text("Add money to your account")
.font(.appTextSmall)
.foregroundStyle(Color.textSecondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(16)
.background(Color.backgroundRow)
.clipShape(RoundedRectangle(cornerRadius: Metrics.buttonRadius, style: .continuous))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityIdentifier("wallet-add-money-row")
}

// MARK: - Deposit
Expand Down Expand Up @@ -748,24 +754,21 @@ private struct WalletScreenContent: View {
}

/// Re-reads the history-derived state: the recent-activity preview and the
/// two onboarding milestones. All three come from the activity and message
/// tables, which a history sync writes without touching balances — so they
/// add-money milestone. Both come from the activity table, which a history
/// sync writes without touching balances — so they
/// cannot ride on ``refresh()``'s balance trigger, and a deposit's activity
/// row routinely lands after the balance rise that preceded it.
private func reloadHistoryState() {
let activities = session.recentActivities(limit: Self.recentPreviewCount)
let addedMoney = session.hasEverAddedMoney()
let tipped = session.hasEverTipped()

guard activities != recentActivities
|| addedMoney != hasAddedMoney
|| tipped != hasTipped
else { return }

withAnimation(.default) {
recentActivities = activities
hasAddedMoney = addedMoney
hasTipped = tipped
}
}

Expand Down
Loading
Loading