|
@@ -1,23 +1,156 @@
|
|
|
import Foundation
|
|
import Foundation
|
|
|
|
|
+import StoreKit
|
|
|
|
|
|
|
|
-/// Maps backend and network errors to short, safe copy for the UI.
|
|
|
|
|
|
|
+/// Maps backend, StoreKit, and file errors to short, safe copy for the UI (always via `L()`).
|
|
|
enum UserFacingErrorMessage {
|
|
enum UserFacingErrorMessage {
|
|
|
|
|
+ enum PurchaseContext {
|
|
|
|
|
+ case purchase
|
|
|
|
|
+ case restore
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
static func jobSearchFailure(_ error: Error) -> String {
|
|
static func jobSearchFailure(_ error: Error) -> String {
|
|
|
|
|
+ let ns = error as NSError
|
|
|
|
|
+ if ns.domain == "OpenAIJobSearchService", ns.code == 1 {
|
|
|
|
|
+ return L("Job search is unavailable.")
|
|
|
|
|
+ }
|
|
|
|
|
+ if let urlError = error as? URLError, urlError.code == .cancelled {
|
|
|
|
|
+ return L("The search was cancelled. Try again when you're ready.")
|
|
|
|
|
+ }
|
|
|
|
|
+ if let message = connectivityFailure(for: error) {
|
|
|
|
|
+ return message
|
|
|
|
|
+ }
|
|
|
|
|
+ return L("Something went wrong while searching. Please try again in a moment.")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static func purchaseFailure(_ error: Error, context: PurchaseContext = .purchase) -> String {
|
|
|
|
|
+ if let subscription = error as? SubscriptionStoreError {
|
|
|
|
|
+ return subscription.userFacingMessage
|
|
|
|
|
+ }
|
|
|
|
|
+ if let storeKit = error as? StoreKitError {
|
|
|
|
|
+ return message(for: storeKit, context: context)
|
|
|
|
|
+ }
|
|
|
|
|
+ if let purchase = error as? Product.PurchaseError {
|
|
|
|
|
+ return message(for: purchase)
|
|
|
|
|
+ }
|
|
|
|
|
+ if let message = connectivityFailure(for: error) {
|
|
|
|
|
+ return message
|
|
|
|
|
+ }
|
|
|
|
|
+ switch context {
|
|
|
|
|
+ case .purchase:
|
|
|
|
|
+ return L("We couldn't complete your purchase. Please try again.")
|
|
|
|
|
+ case .restore:
|
|
|
|
|
+ return L("We couldn't restore your purchases. Please try again.")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static func pdfSaveFailure(_ error: Error) -> String {
|
|
|
|
|
+ if let cocoa = error as? CocoaError {
|
|
|
|
|
+ switch cocoa.code {
|
|
|
|
|
+ case .fileWriteNoPermission:
|
|
|
|
|
+ return L("We don't have permission to save to that folder. Choose another location or grant access.")
|
|
|
|
|
+ case .fileWriteVolumeReadOnly:
|
|
|
|
|
+ return L("This disk is read-only. Choose another folder to save your PDF.")
|
|
|
|
|
+ case .fileWriteOutOfSpace:
|
|
|
|
|
+ return L("The disk is full. Free up space, then try again.")
|
|
|
|
|
+ default:
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if let urlError = error as? URLError {
|
|
if let urlError = error as? URLError {
|
|
|
switch urlError.code {
|
|
switch urlError.code {
|
|
|
- case .notConnectedToInternet,
|
|
|
|
|
- .networkConnectionLost,
|
|
|
|
|
- .timedOut,
|
|
|
|
|
- .cannotFindHost,
|
|
|
|
|
- .cannotConnectToHost,
|
|
|
|
|
- .dnsLookupFailed:
|
|
|
|
|
- return L("We couldn't reach the server. Check your internet connection and try again.")
|
|
|
|
|
- case .cancelled:
|
|
|
|
|
- return L("The search was cancelled. Try again when you're ready.")
|
|
|
|
|
|
|
+ case .fileDoesNotExist, .noPermissionsToReadFile:
|
|
|
|
|
+ return L("We don't have permission to save to that folder. Choose another location or grant access.")
|
|
|
default:
|
|
default:
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return L("Something went wrong while searching. Please try again in a moment.")
|
|
|
|
|
|
|
+ let ns = error as NSError
|
|
|
|
|
+ if ns.domain == NSCocoaErrorDomain,
|
|
|
|
|
+ ns.code == NSFileWriteOutOfSpaceError || ns.code == NSFileWriteVolumeReadOnlyError {
|
|
|
|
|
+ if ns.code == NSFileWriteOutOfSpaceError {
|
|
|
|
|
+ return L("The disk is full. Free up space, then try again.")
|
|
|
|
|
+ }
|
|
|
|
|
+ return L("This disk is read-only. Choose another folder to save your PDF.")
|
|
|
|
|
+ }
|
|
|
|
|
+ if let message = connectivityFailure(for: error) {
|
|
|
|
|
+ return message
|
|
|
|
|
+ }
|
|
|
|
|
+ return L("We couldn't save the PDF. Try again or choose a different folder.")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func connectivityFailure(for error: Error) -> String? {
|
|
|
|
|
+ if let storeKit = error as? StoreKitError,
|
|
|
|
|
+ case .networkError(let urlError) = storeKit {
|
|
|
|
|
+ return connectivityFailure(for: urlError)
|
|
|
|
|
+ }
|
|
|
|
|
+ if let urlError = error as? URLError {
|
|
|
|
|
+ return connectivityFailure(for: urlError)
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func connectivityFailure(for urlError: URLError) -> String? {
|
|
|
|
|
+ switch urlError.code {
|
|
|
|
|
+ case .notConnectedToInternet,
|
|
|
|
|
+ .networkConnectionLost,
|
|
|
|
|
+ .timedOut,
|
|
|
|
|
+ .cannotFindHost,
|
|
|
|
|
+ .cannotConnectToHost,
|
|
|
|
|
+ .dnsLookupFailed:
|
|
|
|
|
+ return L("We couldn't reach the server. Check your internet connection and try again.")
|
|
|
|
|
+ case .cancelled:
|
|
|
|
|
+ return L("The request was cancelled. Try again when you're ready.")
|
|
|
|
|
+ default:
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func message(for error: StoreKitError, context: PurchaseContext) -> String {
|
|
|
|
|
+ switch error {
|
|
|
|
|
+ case .userCancelled:
|
|
|
|
|
+ return L("Purchase was cancelled.")
|
|
|
|
|
+ case .networkError(let urlError):
|
|
|
|
|
+ return connectivityFailure(for: urlError)
|
|
|
|
|
+ ?? (context == .restore
|
|
|
|
|
+ ? L("We couldn't restore your purchases. Please try again.")
|
|
|
|
|
+ : L("We couldn't complete your purchase. Please try again."))
|
|
|
|
|
+ case .notAvailableInStorefront:
|
|
|
|
|
+ return L("This subscription isn't available in your App Store region.")
|
|
|
|
|
+ case .notEntitled:
|
|
|
|
|
+ return L("The purchase couldn't be verified. Please try again.")
|
|
|
|
|
+ case .unsupported, .unknown, .systemError:
|
|
|
|
|
+ return context == .restore
|
|
|
|
|
+ ? L("We couldn't restore your purchases. Please try again.")
|
|
|
|
|
+ : L("We couldn't complete your purchase. Please try again.")
|
|
|
|
|
+ @unknown default:
|
|
|
|
|
+ return context == .restore
|
|
|
|
|
+ ? L("We couldn't restore your purchases. Please try again.")
|
|
|
|
|
+ : L("We couldn't complete your purchase. Please try again.")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func message(for error: Product.PurchaseError) -> String {
|
|
|
|
|
+ switch error {
|
|
|
|
|
+ case .productUnavailable:
|
|
|
|
|
+ return L("That subscription isn’t available from the App Store right now.")
|
|
|
|
|
+ case .purchaseNotAllowed:
|
|
|
|
|
+ return L("Purchases aren't allowed on this account or device.")
|
|
|
|
|
+ case .ineligibleForOffer:
|
|
|
|
|
+ return L("This introductory offer isn't available for your account.")
|
|
|
|
|
+ case .invalidQuantity, .invalidOfferIdentifier, .invalidOfferPrice,
|
|
|
|
|
+ .invalidOfferSignature, .missingOfferParameters:
|
|
|
|
|
+ return L("We couldn't complete your purchase. Please try again.")
|
|
|
|
|
+ @unknown default:
|
|
|
|
|
+ return L("We couldn't complete your purchase. Please try again.")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private extension SubscriptionStoreError {
|
|
|
|
|
+ var userFacingMessage: String {
|
|
|
|
|
+ switch self {
|
|
|
|
|
+ case .productUnavailable:
|
|
|
|
|
+ return L("That subscription isn’t available from the App Store right now.")
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|