forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUserFacingErrorPresentation.swift
More file actions
147 lines (138 loc) · 5.54 KB
/
Copy pathUserFacingErrorPresentation.swift
File metadata and controls
147 lines (138 loc) · 5.54 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
143
144
145
146
147
import Foundation
/// Maps transport and system failures into concise copy for non-chat UI.
///
/// Chat and agent transcripts deliberately keep their richer error context. This
/// mapper is only for app chrome outside active chat and agent transcripts, such
/// as dashboards, pages, sheets, and alerts, where a raw server detail or
/// decoding failure is distracting and unhelpful.
enum UserFacingErrorPresentation {
enum Context {
case dashboard
case chatSessions
case conversations
case conversationSearch
case conversationMerge
case tasks
case memories
case memoryVisibility
case memoryDeletion
case screenshots
case goals
case persona
case signIn
case onboarding
case integration(String)
case browserExtension
case memoryExport
case storageSync
case transcription
case accountDeletion
fileprivate var action: String {
switch self {
case .dashboard: return "refresh the dashboard"
case .chatSessions: return "load chats"
case .conversations: return "load conversations"
case .conversationSearch: return "search conversations"
case .conversationMerge: return "merge conversations"
case .tasks: return "update tasks"
case .memories: return "load memories"
case .memoryVisibility: return "update memory visibility"
case .memoryDeletion: return "delete memories"
case .screenshots: return "load screenshots"
case .goals: return "load goals"
case .persona: return "load your persona"
case .signIn: return "sign in"
case .onboarding: return "save that step"
case .integration(let name): return "connect to \(name)"
case .browserExtension: return "connect the browser extension"
case .memoryExport: return "prepare that export"
case .storageSync: return "sync device storage"
case .transcription: return "start transcription"
case .accountDeletion: return "delete your account"
}
}
fileprivate var isSignIn: Bool {
if case .signIn = self { return true }
return false
}
}
static func message(for error: Error, while context: Context) -> String {
if let apiError = error as? APIError {
switch apiError {
case .httpError(let statusCode, _):
switch statusCode {
case 401:
return context.isSignIn
? "Couldn't sign in. Try again."
: "Please sign in again, then try once more."
case 403:
return "You don't have permission to do that."
case 409:
return "This changed while Omi was updating. Refresh and try again."
case 429:
return "Omi is busy right now. Try again in a moment."
case 500...599:
return "Omi's service is unavailable right now. Try again."
default:
return fallback(for: context)
}
case .invalidResponse, .decodingError:
return "Omi received an unexpected response. Try again."
case .syncRateLimited:
return "Omi is busy right now. Try again in a moment."
case .unsupportedTierScopedBulkMutation:
return "That option isn't available yet."
case .syncUploadRejected, .accountCutoverOfflineQueueBlocked:
return fallback(for: context)
case .unauthorized:
return context.isSignIn
? "Couldn't sign in. Try again."
: "Please sign in again, then try once more."
}
}
if let urlError = error as? URLError {
switch urlError.code {
case .cannotConnectToHost, .cannotFindHost, .dnsLookupFailed, .networkConnectionLost,
.notConnectedToInternet, .timedOut:
return "Check your connection and try again."
default:
return fallback(for: context)
}
}
return fallback(for: context)
}
/// Sanitizes already-materialized error copy at display time when the original
/// `Error` is not available (for example, values stored on invariant-owned types).
static func message(from raw: String, while context: Context) -> String {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return fallback(for: context) }
if shouldPreserveCuratedCopy(trimmed) {
return trimmed
}
return fallback(for: context)
}
/// Raw-system-error fingerprints. Copy that carries none of them is copy this
/// app wrote, and is shown verbatim.
///
/// This deliberately does not judge by sentence shape. A previous version also
/// required a trailing period and fewer than 120 characters, which flattened
/// five real connector messages into the generic fallback — including the
/// Calendar not-signed-in guidance, at exactly 120 characters, which is the
/// single most common Google connect failure and the one that most needs its
/// next step shown. Length and punctuation do not separate curated copy from
/// raw system text; the fingerprints below do.
private static func shouldPreserveCuratedCopy(_ text: String) -> Bool {
let lowered = text.lowercased()
if text.count > 160 { return false }
if text.contains("://") { return false }
// Catches `Error Domain=` and every bare `NS*ErrorDomain` spelling.
if lowered.contains("errordomain") { return false }
if lowered.contains("cfstream") { return false }
if lowered.range(of: #"\b[45]\d{2}\b"#, options: .regularExpression) != nil { return false }
if text.filter({ $0 == ":" }).count > 1 { return false }
return true
}
private static func fallback(for context: Context) -> String {
"Couldn't \(context.action). Try again."
}
}