-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSampleAppExample.swift
More file actions
142 lines (125 loc) · 4.55 KB
/
Copy pathSampleAppExample.swift
File metadata and controls
142 lines (125 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// MARK: - SampleAppExample.swift
// InnoRouter - integrated sample exercising multi-feature surface
// Copyright © 2026 Inno Squad. All rights reserved.
//
// This example combines the headline feature surface — deep-link
// pipeline with auth gating, FlowStore push+modal projection, and
// DebouncingNavigator search debouncing — into one file so adopters
// can see how the pieces compose without flipping between the
// standalone examples.
//
// It is a single-file SwiftUI scene composition rather than a full
// Xcode project. Other examples (`AppShellExample`,
// `DeepLinkExample`, `SplitCoordinatorExample`) cover host wiring
// in isolation; this file's job is to show the feature surfaces
// composed together.
import Foundation
import OSLog
import SwiftUI
import Synchronization
import InnoRouter
import InnoRouterEffects
// MARK: - Routes
@Routable
enum SampleRoute {
case home
case detail(id: String)
case profile
case search(query: String)
case kycReview
}
// MARK: - App authority
@Observable
@MainActor
final class SampleAppAuthority {
let store: NavigationStore<SampleRoute>
let modal = ModalStore<SampleRoute>()
let flow = FlowStore<SampleRoute>()
let debouncedSearch: DebouncingNavigator<NavigationStore<SampleRoute>, ContinuousClock>
let deepLinkHandler: DeepLinkEffectHandler<SampleRoute>
/// In a real app this would be backed by Keychain or a session
/// service. The sample uses a small Sendable wrapper around a
/// `Mutex<Bool>` so the @Sendable `isAuthenticated` closure
/// inside the pipeline can read it from any executor while the
/// UI flips it on @MainActor.
private let session: AuthSession
var isAuthenticated: Bool {
get { session.isAuthenticated }
set { session.isAuthenticated = newValue }
}
init() {
let store = NavigationStore<SampleRoute>()
let session = AuthSession()
self.store = store
self.session = session
self.debouncedSearch = DebouncingNavigator(
wrapping: store,
interval: .milliseconds(250)
)
self.deepLinkHandler = DeepLinkEffectHandler(
pipeline: Self.makeDeepLinkPipeline(session: session),
navigator: store
)
}
func handleDeepLink(_ url: URL) {
switch deepLinkHandler.handle(url) {
case .pending(let pending):
// The handler retains `pending` in memory until sign-in completes
// and the app asks it to resume.
Logger(subsystem: "io.innosquad.innorouter.sample", category: "deeplink")
.info("deferred deep link until auth: \(pending.url.absoluteString, privacy: .private)")
case .executed, .executionFailed, .applicationRejected,
.rejected, .unhandled, .invalidURL, .missingDeepLinkURL,
.noPendingDeepLink:
break
}
}
func searchTyped(_ query: String) async {
// Debouncing collapses rapid keystrokes into a single
// navigation per quiet window.
await debouncedSearch.debouncedExecute(.replace([.search(query: query)]))
}
nonisolated private static func searchQuery(from url: URL) -> String {
URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.first { $0.name == "q" || $0.name == "query" }?
.value ?? ""
}
nonisolated private static func makeDeepLinkPipeline(
session: AuthSession
) -> DeepLinkPipeline<SampleRoute> {
DeepLinkPipeline(
originPolicy: .allowlisted(
schemes: ["app"],
hosts: ["sample"]
),
customResolver: { url in
switch url.path {
case "/profile": .profile
case "/kyc": .kycReview
case "/search": .search(query: searchQuery(from: url))
default: nil
}
},
authenticationPolicy: .required(
shouldRequireAuthentication: { route in
route == .profile || route == .kycReview
},
isAuthenticated: { [session] in
session.isAuthenticated
}
)
)
}
}
// MARK: - Sendable session wrapper
private final class AuthSession: Sendable {
private let storage: Mutex<Bool>
init(initial: Bool = false) {
self.storage = Mutex(initial)
}
var isAuthenticated: Bool {
get { storage.withLock { $0 } }
set { storage.withLock { $0 = newValue } }
}
}