|
|
@@ -1,23 +1,156 @@
|
|
1
|
1
|
import Foundation
|
|
|
2
|
+import StoreKit
|
|
2
|
3
|
|
|
3
|
|
-/// Maps backend and network errors to short, safe copy for the UI.
|
|
|
4
|
+/// Maps backend, StoreKit, and file errors to short, safe copy for the UI (always via `L()`).
|
|
4
|
5
|
enum UserFacingErrorMessage {
|
|
|
6
|
+ enum PurchaseContext {
|
|
|
7
|
+ case purchase
|
|
|
8
|
+ case restore
|
|
|
9
|
+ }
|
|
|
10
|
+
|
|
5
|
11
|
static func jobSearchFailure(_ error: Error) -> String {
|
|
|
12
|
+ let ns = error as NSError
|
|
|
13
|
+ if ns.domain == "OpenAIJobSearchService", ns.code == 1 {
|
|
|
14
|
+ return L("Job search is unavailable.")
|
|
|
15
|
+ }
|
|
|
16
|
+ if let urlError = error as? URLError, urlError.code == .cancelled {
|
|
|
17
|
+ return L("The search was cancelled. Try again when you're ready.")
|
|
|
18
|
+ }
|
|
|
19
|
+ if let message = connectivityFailure(for: error) {
|
|
|
20
|
+ return message
|
|
|
21
|
+ }
|
|
|
22
|
+ return L("Something went wrong while searching. Please try again in a moment.")
|
|
|
23
|
+ }
|
|
|
24
|
+
|
|
|
25
|
+ static func purchaseFailure(_ error: Error, context: PurchaseContext = .purchase) -> String {
|
|
|
26
|
+ if let subscription = error as? SubscriptionStoreError {
|
|
|
27
|
+ return subscription.userFacingMessage
|
|
|
28
|
+ }
|
|
|
29
|
+ if let storeKit = error as? StoreKitError {
|
|
|
30
|
+ return message(for: storeKit, context: context)
|
|
|
31
|
+ }
|
|
|
32
|
+ if let purchase = error as? Product.PurchaseError {
|
|
|
33
|
+ return message(for: purchase)
|
|
|
34
|
+ }
|
|
|
35
|
+ if let message = connectivityFailure(for: error) {
|
|
|
36
|
+ return message
|
|
|
37
|
+ }
|
|
|
38
|
+ switch context {
|
|
|
39
|
+ case .purchase:
|
|
|
40
|
+ return L("We couldn't complete your purchase. Please try again.")
|
|
|
41
|
+ case .restore:
|
|
|
42
|
+ return L("We couldn't restore your purchases. Please try again.")
|
|
|
43
|
+ }
|
|
|
44
|
+ }
|
|
|
45
|
+
|
|
|
46
|
+ static func pdfSaveFailure(_ error: Error) -> String {
|
|
|
47
|
+ if let cocoa = error as? CocoaError {
|
|
|
48
|
+ switch cocoa.code {
|
|
|
49
|
+ case .fileWriteNoPermission:
|
|
|
50
|
+ return L("We don't have permission to save to that folder. Choose another location or grant access.")
|
|
|
51
|
+ case .fileWriteVolumeReadOnly:
|
|
|
52
|
+ return L("This disk is read-only. Choose another folder to save your PDF.")
|
|
|
53
|
+ case .fileWriteOutOfSpace:
|
|
|
54
|
+ return L("The disk is full. Free up space, then try again.")
|
|
|
55
|
+ default:
|
|
|
56
|
+ break
|
|
|
57
|
+ }
|
|
|
58
|
+ }
|
|
6
|
59
|
if let urlError = error as? URLError {
|
|
7
|
60
|
switch urlError.code {
|
|
8
|
|
- case .notConnectedToInternet,
|
|
9
|
|
- .networkConnectionLost,
|
|
10
|
|
- .timedOut,
|
|
11
|
|
- .cannotFindHost,
|
|
12
|
|
- .cannotConnectToHost,
|
|
13
|
|
- .dnsLookupFailed:
|
|
14
|
|
- return L("We couldn't reach the server. Check your internet connection and try again.")
|
|
15
|
|
- case .cancelled:
|
|
16
|
|
- return L("The search was cancelled. Try again when you're ready.")
|
|
|
61
|
+ case .fileDoesNotExist, .noPermissionsToReadFile:
|
|
|
62
|
+ return L("We don't have permission to save to that folder. Choose another location or grant access.")
|
|
17
|
63
|
default:
|
|
18
|
64
|
break
|
|
19
|
65
|
}
|
|
20
|
66
|
}
|
|
21
|
|
- return L("Something went wrong while searching. Please try again in a moment.")
|
|
|
67
|
+ let ns = error as NSError
|
|
|
68
|
+ if ns.domain == NSCocoaErrorDomain,
|
|
|
69
|
+ ns.code == NSFileWriteOutOfSpaceError || ns.code == NSFileWriteVolumeReadOnlyError {
|
|
|
70
|
+ if ns.code == NSFileWriteOutOfSpaceError {
|
|
|
71
|
+ return L("The disk is full. Free up space, then try again.")
|
|
|
72
|
+ }
|
|
|
73
|
+ return L("This disk is read-only. Choose another folder to save your PDF.")
|
|
|
74
|
+ }
|
|
|
75
|
+ if let message = connectivityFailure(for: error) {
|
|
|
76
|
+ return message
|
|
|
77
|
+ }
|
|
|
78
|
+ return L("We couldn't save the PDF. Try again or choose a different folder.")
|
|
|
79
|
+ }
|
|
|
80
|
+
|
|
|
81
|
+ private static func connectivityFailure(for error: Error) -> String? {
|
|
|
82
|
+ if let storeKit = error as? StoreKitError,
|
|
|
83
|
+ case .networkError(let urlError) = storeKit {
|
|
|
84
|
+ return connectivityFailure(for: urlError)
|
|
|
85
|
+ }
|
|
|
86
|
+ if let urlError = error as? URLError {
|
|
|
87
|
+ return connectivityFailure(for: urlError)
|
|
|
88
|
+ }
|
|
|
89
|
+ return nil
|
|
|
90
|
+ }
|
|
|
91
|
+
|
|
|
92
|
+ private static func connectivityFailure(for urlError: URLError) -> String? {
|
|
|
93
|
+ switch urlError.code {
|
|
|
94
|
+ case .notConnectedToInternet,
|
|
|
95
|
+ .networkConnectionLost,
|
|
|
96
|
+ .timedOut,
|
|
|
97
|
+ .cannotFindHost,
|
|
|
98
|
+ .cannotConnectToHost,
|
|
|
99
|
+ .dnsLookupFailed:
|
|
|
100
|
+ return L("We couldn't reach the server. Check your internet connection and try again.")
|
|
|
101
|
+ case .cancelled:
|
|
|
102
|
+ return L("The request was cancelled. Try again when you're ready.")
|
|
|
103
|
+ default:
|
|
|
104
|
+ return nil
|
|
|
105
|
+ }
|
|
|
106
|
+ }
|
|
|
107
|
+
|
|
|
108
|
+ private static func message(for error: StoreKitError, context: PurchaseContext) -> String {
|
|
|
109
|
+ switch error {
|
|
|
110
|
+ case .userCancelled:
|
|
|
111
|
+ return L("Purchase was cancelled.")
|
|
|
112
|
+ case .networkError(let urlError):
|
|
|
113
|
+ return connectivityFailure(for: urlError)
|
|
|
114
|
+ ?? (context == .restore
|
|
|
115
|
+ ? L("We couldn't restore your purchases. Please try again.")
|
|
|
116
|
+ : L("We couldn't complete your purchase. Please try again."))
|
|
|
117
|
+ case .notAvailableInStorefront:
|
|
|
118
|
+ return L("This subscription isn't available in your App Store region.")
|
|
|
119
|
+ case .notEntitled:
|
|
|
120
|
+ return L("The purchase couldn't be verified. Please try again.")
|
|
|
121
|
+ case .unsupported, .unknown, .systemError:
|
|
|
122
|
+ return context == .restore
|
|
|
123
|
+ ? L("We couldn't restore your purchases. Please try again.")
|
|
|
124
|
+ : L("We couldn't complete your purchase. Please try again.")
|
|
|
125
|
+ @unknown default:
|
|
|
126
|
+ return context == .restore
|
|
|
127
|
+ ? L("We couldn't restore your purchases. Please try again.")
|
|
|
128
|
+ : L("We couldn't complete your purchase. Please try again.")
|
|
|
129
|
+ }
|
|
|
130
|
+ }
|
|
|
131
|
+
|
|
|
132
|
+ private static func message(for error: Product.PurchaseError) -> String {
|
|
|
133
|
+ switch error {
|
|
|
134
|
+ case .productUnavailable:
|
|
|
135
|
+ return L("That subscription isn’t available from the App Store right now.")
|
|
|
136
|
+ case .purchaseNotAllowed:
|
|
|
137
|
+ return L("Purchases aren't allowed on this account or device.")
|
|
|
138
|
+ case .ineligibleForOffer:
|
|
|
139
|
+ return L("This introductory offer isn't available for your account.")
|
|
|
140
|
+ case .invalidQuantity, .invalidOfferIdentifier, .invalidOfferPrice,
|
|
|
141
|
+ .invalidOfferSignature, .missingOfferParameters:
|
|
|
142
|
+ return L("We couldn't complete your purchase. Please try again.")
|
|
|
143
|
+ @unknown default:
|
|
|
144
|
+ return L("We couldn't complete your purchase. Please try again.")
|
|
|
145
|
+ }
|
|
|
146
|
+ }
|
|
|
147
|
+}
|
|
|
148
|
+
|
|
|
149
|
+private extension SubscriptionStoreError {
|
|
|
150
|
+ var userFacingMessage: String {
|
|
|
151
|
+ switch self {
|
|
|
152
|
+ case .productUnavailable:
|
|
|
153
|
+ return L("That subscription isn’t available from the App Store right now.")
|
|
|
154
|
+ }
|
|
22
|
155
|
}
|
|
23
|
156
|
}
|