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
8 changes: 2 additions & 6 deletions Flipcash/Core/Navigation/AppRouter+NestedSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,11 @@ private struct BuySheetRoot: View {
}

/// Root view for the `.tips` sheet — owns the `NavigationStack` bound to
/// `router[.tips]` and the profile-creation state.
///
/// The state lives here, not on a screen: the name entered on one push has to
/// survive the next, and an upload must outlive the screen that started it.
/// `router[.tips]`.
struct TipsSheetRoot: View {

@Environment(AppRouter.self) private var router
@State private var creationState = ProfileCreationState()
@Environment(ProfileCreationState.self) private var creationState

var body: some View {
@Bindable var router = router
Expand All @@ -123,7 +120,6 @@ struct TipsSheetRoot: View {
}
}
}
.environment(creationState)
// A rejected blob is terminal and a reserved upload is signed against
// one byte count, so dismissing mid-upload loses real work.
.interactiveDismissDisabled(creationState.isUploading)
Expand Down
5 changes: 1 addition & 4 deletions Flipcash/Core/Screens/Main/Home/HomeTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,20 +371,17 @@ extension HomeTab {

/// The Chat tab — the tip conversations surface. Mirrors `TipsSheetRoot` (the
/// `.tips` sheet) as embedded tab chrome: the same `NavigationStack` bound to
/// `router[.tips]` and the same profile-creation state, minus the sheet's close
/// button.
/// `router[.tips]`, minus the sheet's close button.
private struct ChatTab: View {

@Environment(AppRouter.self) private var router
@State private var creationState = ProfileCreationState()

var body: some View {
@Bindable var router = router
NavigationStack(path: $router[.tips]) {
TipsScreen(isEmbedded: true)
.appRouterDestinations()
}
.environment(creationState)
}
}

Expand Down
6 changes: 3 additions & 3 deletions Flipcash/Core/Screens/Main/Profile/ProfileCreationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ private let logger = Logger(label: "flipcash.profile-creation")

/// Drives profile creation across the name and photo screens.
///
/// Owned by the Tips sheet root so the name entered on one screen survives the
/// push to the next, and so a resumable upload outlives the screen that
/// started it.
/// Owned by the session container, so every stack that can push the name and
/// photo screens finds it, the name entered on one screen survives the push to
/// the next, and a resumable upload outlives the screen that started it.
@Observable
final class ProfileCreationState {

Expand Down
3 changes: 3 additions & 0 deletions Flipcash/Core/Session/SessionAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ final class SessionContainer {
let profileAvatars: ProfileAvatarStore
/// Identities for chat senders the chat's own roster leaves out — see ``KnownAuthorDirectory``.
let knownAuthors: KnownAuthorDirectory
/// The in-progress profile setup, shared by every stack that can push its screens.
let profileCreationState = ProfileCreationState()
/// Fills cash link cards in for chat transcripts. Container-scoped so a link shared into two
/// conversations is looked up once, and holding a `GiftCardAccountReading` rather than the
/// `Client` it was built from — rendering a card must never reach a path that claims it.
Expand Down Expand Up @@ -789,6 +791,7 @@ final class SessionContainer {
.environment(onrampDeeplinkInbox)
.environment(conversationController)
.environment(blocklistController)
.environment(profileCreationState)
}
}

Expand Down
47 changes: 47 additions & 0 deletions FlipcashTests/ProfileSetupEnvironmentTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// ProfileSetupEnvironmentTests.swift
// FlipcashTests
//

import SwiftUI
import Testing
@testable import Flipcash

/// The profile-setup screens read `ProfileCreationState` from the environment.
/// It once came only from the tips stack's roots, so pushing either screen onto
/// any other stack aborted on the missing object.
///
/// Each test renders the destination with only the app and session environment,
/// which is all a non-tips stack provides. A regression aborts the test run
/// rather than failing one test: a missing environment object is a fatal error.
@MainActor
@Suite("Profile Setup Environment Tests")
struct ProfileSetupEnvironmentTests {

@Test("The name step renders with only the session environment")
func nameStepRendersWithoutStackState() throws {
let window = try Self.host(.profileName)
#expect(window.rootViewController?.view.subviews.isEmpty == false)
}

@Test("The photo step renders with only the session environment")
func photoStepRendersWithoutStackState() throws {
let window = try Self.host(.profilePhoto)
#expect(window.rootViewController?.view.subviews.isEmpty == false)
}

private static func host(_ destination: AppRouter.Destination) throws -> UIWindow {
let sessionContainer = try SessionContainer.makeTest(holdings: [])
let view = NavigationStack {
DestinationView(destination: destination)
}
.injectingEnvironment(from: sessionContainer)
.injectingEnvironment(from: Container.mock)

let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
window.rootViewController = UIHostingController(rootView: view)
window.makeKeyAndVisible()
window.layoutIfNeeded()
return window
}
}
Loading