krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

3ed2bb4da64d7cc9b67ef86a3892ab41b14fcc51

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-03-23T01:40:53Z

feat: surface an alert when a deep link fails to resolve
 Hutch/App/AppState.swift       | 10 ++++++++++
 Hutch/App/RootView.swift       | 23 +++++++++++++++++++++--
 HutchTests/AppStateTests.swift | 25 +++++++++++++++++++++++++
 3 files changed, 56 insertions(+), 2 deletions(-)

diff --git a/Hutch/App/AppState.swift b/Hutch/App/AppState.swift
index d5a86fd..e74a58b 100644
--- a/Hutch/App/AppState.swift
+++ b/Hutch/App/AppState.swift
@@ -54,6 +54,7 @@ final class AppState {
     /// Set by the deep link handler; consumed by RootView to drive navigation.
     var pendingDeepLink: DeepLink?
     var pendingTabNavigation: TabNavigationTarget?
+    var deepLinkError: String?
 
     // MARK: - Init
 
@@ -182,6 +183,14 @@ final class AppState {
         selectedTab = .more
     }
 
+    func presentRepositoryDeepLinkError() {
+        deepLinkError = "The repository could not be found or is inaccessible."
+    }
+
+    func presentTicketDeepLinkError() {
+        deepLinkError = "The ticket could not be found or is inaccessible."
+    }
+
     // MARK: - Private
 
     private static let meQuery = """
@@ -257,6 +266,7 @@ final class AppState {
         currentUser = nil
         pendingDeepLink = nil
         pendingTabNavigation = nil
+        deepLinkError = nil
         selectedTab = .home
     }
 
diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift
index f615541..9c4e71a 100644
--- a/Hutch/App/RootView.swift
+++ b/Hutch/App/RootView.swift
@@ -12,6 +12,8 @@ struct RootView: View {
     @State private var isResolvingDeepLink = false
 
     var body: some View {
+        @Bindable var appState = appState
+
         Group {
             switch appState.authPhase {
             case .launching:
@@ -37,6 +39,23 @@ struct RootView: View {
         .onChange(of: appState.pendingTabNavigation) { _, newValue in
             consumePendingTabNavigationIfPossible(newValue)
         }
+        .alert(
+            "Couldn't Open Link",
+            isPresented: Binding(
+                get: { appState.deepLinkError != nil },
+                set: { isPresented in
+                    if !isPresented {
+                        appState.deepLinkError = nil
+                    }
+                }
+            )
+        ) {
+            Button("OK") {
+                appState.deepLinkError = nil
+            }
+        } message: {
+            Text(appState.deepLinkError ?? "")
+        }
     }
 
     // MARK: - Tab View
@@ -198,7 +217,7 @@ struct RootView: View {
                 await settleNavigationTransition()
                 repoPath.append(summary)
             } catch {
-                // Silently fail — the repo may not exist or be inaccessible
+                appState.presentRepositoryDeepLinkError()
             }
         }
     }
@@ -221,7 +240,7 @@ struct RootView: View {
                     ticketId: ticketId
                 ))
             } catch {
-                // Silently fail
+                appState.presentTicketDeepLinkError()
             }
         }
     }
diff --git a/HutchTests/AppStateTests.swift b/HutchTests/AppStateTests.swift
new file mode 100644
index 0000000..ca34df2
--- /dev/null
+++ b/HutchTests/AppStateTests.swift
@@ -0,0 +1,25 @@
+import Testing
+@testable import Hutch
+
+struct AppStateTests {
+
+    @Test
+    @MainActor
+    func presentRepositoryDeepLinkErrorSetsUserFacingMessage() {
+        let appState = AppState()
+
+        appState.presentRepositoryDeepLinkError()
+
+        #expect(appState.deepLinkError == "The repository could not be found or is inaccessible.")
+    }
+
+    @Test
+    @MainActor
+    func presentTicketDeepLinkErrorSetsUserFacingMessage() {
+        let appState = AppState()
+
+        appState.presentTicketDeepLinkError()
+
+        #expect(appState.deepLinkError == "The ticket could not be found or is inaccessible.")
+    }
+}