Commit bc2dcb7e82

bc2dcb7e825f5d814a6f456eca7b47e2f08b9110

parent: 948330fe88

Unsigned

cmc <hello@cleberg.net> · 2026-03-23 01:40 UTC

feat: surface an alert when a deep link fails to resolve

Layout: unified · split

Hutch/App/AppState.swift +10
@@ -54,6 +54,7 @@ final class AppState {
54 /// Set by the deep link handler; consumed by RootView to drive navigation. 54 /// Set by the deep link handler; consumed by RootView to drive navigation.
55 var pendingDeepLink: DeepLink? 55 var pendingDeepLink: DeepLink?
56 var pendingTabNavigation: TabNavigationTarget? 56 var pendingTabNavigation: TabNavigationTarget?
57 var deepLinkError: String?
57 58
58 // MARK: - Init 59 // MARK: - Init
59 60
@@ -182,6 +183,14 @@ final class AppState {
182 selectedTab = .more 183 selectedTab = .more
183 } 184 }
184 185
186 func presentRepositoryDeepLinkError() {
187 deepLinkError = "The repository could not be found or is inaccessible."
188 }
189
190 func presentTicketDeepLinkError() {
191 deepLinkError = "The ticket could not be found or is inaccessible."
192 }
193
185 // MARK: - Private 194 // MARK: - Private
186 195
187 private static let meQuery = """ 196 private static let meQuery = """
@@ -257,6 +266,7 @@ final class AppState {
257 currentUser = nil 266 currentUser = nil
258 pendingDeepLink = nil 267 pendingDeepLink = nil
259 pendingTabNavigation = nil 268 pendingTabNavigation = nil
269 deepLinkError = nil
260 selectedTab = .home 270 selectedTab = .home
261 } 271 }
262 272
Hutch/App/RootView.swift +21 −2
@@ -12,6 +12,8 @@ struct RootView: View {
12 @State private var isResolvingDeepLink = false 12 @State private var isResolvingDeepLink = false
13 13
14 var body: some View { 14 var body: some View {
15 @Bindable var appState = appState
16
15 Group { 17 Group {
16 switch appState.authPhase { 18 switch appState.authPhase {
17 case .launching: 19 case .launching:
@@ -37,6 +39,23 @@ struct RootView: View {
37 .onChange(of: appState.pendingTabNavigation) { _, newValue in 39 .onChange(of: appState.pendingTabNavigation) { _, newValue in
38 consumePendingTabNavigationIfPossible(newValue) 40 consumePendingTabNavigationIfPossible(newValue)
39 } 41 }
42 .alert(
43 "Couldn't Open Link",
44 isPresented: Binding(
45 get: { appState.deepLinkError != nil },
46 set: { isPresented in
47 if !isPresented {
48 appState.deepLinkError = nil
49 }
50 }
51 )
52 ) {
53 Button("OK") {
54 appState.deepLinkError = nil
55 }
56 } message: {
57 Text(appState.deepLinkError ?? "")
58 }
40 } 59 }
41 60
42 // MARK: - Tab View 61 // MARK: - Tab View
@@ -198,7 +217,7 @@ struct RootView: View {
198 await settleNavigationTransition() 217 await settleNavigationTransition()
199 repoPath.append(summary) 218 repoPath.append(summary)
200 } catch { 219 } catch {
201 // Silently fail — the repo may not exist or be inaccessible 220 appState.presentRepositoryDeepLinkError()
202 } 221 }
203 } 222 }
204 } 223 }
@@ -221,7 +240,7 @@ struct RootView: View {
221 ticketId: ticketId 240 ticketId: ticketId
222 )) 241 ))
223 } catch { 242 } catch {
224 // Silently fail 243 appState.presentTicketDeepLinkError()
225 } 244 }
226 } 245 }
227 } 246 }
HutchTests/AppStateTests.swift added +25
@@ -0,0 +1,25 @@
1import Testing
2@testable import Hutch
3
4struct AppStateTests {
5
6 @Test
7 @MainActor
8 func presentRepositoryDeepLinkErrorSetsUserFacingMessage() {
9 let appState = AppState()
10
11 appState.presentRepositoryDeepLinkError()
12
13 #expect(appState.deepLinkError == "The repository could not be found or is inaccessible.")
14 }
15
16 @Test
17 @MainActor
18 func presentTicketDeepLinkErrorSetsUserFacingMessage() {
19 let appState = AppState()
20
21 appState.presentTicketDeepLinkError()
22
23 #expect(appState.deepLinkError == "The ticket could not be found or is inaccessible.")
24 }
25}