From c24de874e60a184c0712cfdcdd16a509ab414c31 Mon Sep 17 00:00:00 2001 From: nythepegasus Date: Mon, 6 May 2024 10:14:05 -0400 Subject: [PATCH] Finish Riley's monster commit 3b38d725d7e8e45fb2c0cb465a3968828616c209 May the Gods have mercy on my soul. --- AltStore.xcodeproj/project.pbxproj | 46 +++- .../xcshareddata/swiftpm/Package.resolved | 113 -------- .../AuthenticationViewController.swift | 4 +- AltStore/Components/ToastView.swift | 45 +-- AltStore/Intents/IntentHandler.swift | 2 +- AltStore/Managing Apps/AppManager.swift | 56 ++-- AltStore/Managing Apps/AppManagerErrors.swift | 36 ++- .../Operations/AuthenticationOperation.swift | 27 +- .../BackgroundRefreshAppsOperation.swift | 17 +- AltStore/Operations/BackupAppOperation.swift | 7 +- .../Operations/DownloadAppOperation.swift | 38 +-- .../FetchProvisioningProfilesOperation.swift | 8 +- AltStore/Operations/OperationError.swift | 233 +++++++++++----- .../Patch App/PatchAppOperation.swift | 44 ++- .../Patch App/PatchViewController.swift | 2 +- AltStore/Operations/RefreshAppOperation.swift | 7 +- AltStore/Operations/SendAppOperation.swift | 2 +- AltStore/Operations/VerifyAppOperation.swift | 102 ++++--- .../ErrorDetailsViewController.swift | 53 ++++ .../Error Log/ErrorLogViewController.swift | 28 +- AltStore/Settings/Settings.storyboard | 91 +++++- AltStore/Sources/SourcesViewController.swift | 11 +- AltStoreCore/AltStoreCore.h | 1 + Shared/Categories/NSError+ALTServerError.m | 136 +++++++-- Shared/Errors/ALTLocalizedError.swift | 182 ++++++++++++ Shared/Errors/ALTWrappedError.h | 25 ++ Shared/Errors/ALTWrappedError.m | 73 +++++ .../ALTServerError+Conveniences.swift | 8 +- Shared/Extensions/NSError+AltStore.swift | 260 ++++++++++++------ Shared/Server Protocol/CodableError.swift | 249 +++++++++++++++++ .../Server Protocol/CodableServerError.swift | 126 --------- Shared/Server Protocol/ServerProtocol.swift | 8 +- 32 files changed, 1422 insertions(+), 618 deletions(-) delete mode 100644 AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 AltStore/Settings/Error Log/ErrorDetailsViewController.swift create mode 100644 Shared/Errors/ALTLocalizedError.swift create mode 100644 Shared/Errors/ALTWrappedError.h create mode 100644 Shared/Errors/ALTWrappedError.m create mode 100644 Shared/Server Protocol/CodableError.swift delete mode 100644 Shared/Server Protocol/CodableServerError.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index fca08c0b..40d9fdab 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -36,6 +36,13 @@ 0EA1667D2ADFE140003015C1 /* xplist.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166592ADFE0D2003015C1 /* xplist.c */; }; 0EA1667E2ADFE140003015C1 /* time64.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA1664C2ADFE0D1003015C1 /* time64.c */; }; 0EA4B9BC2AE4A414009209CE /* plist.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA4B9BB2AE4A3F6009209CE /* plist.c */; }; + 0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */; }; + 0EE7FDC62BE8CEA300D1E390 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */; }; + 0EE7FDC72BE8CF4100D1E390 /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7FDC02BE8BC2100D1E390 /* ALTWrappedError.m */; }; + 0EE7FDC82BE8CF4800D1E390 /* ALTWrappedError.h in Headers */ = {isa = PBXBuildFile; fileRef = 0EE7FDC22BE8BC4200D1E390 /* ALTWrappedError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; }; + 0EE7FDCB2BE8D12B00D1E390 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */; }; + 0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */; }; 19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; }; 19104DB52909C06D00C49C7B /* EmotionalDamage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19104DB42909C06D00C49C7B /* EmotionalDamage.swift */; }; 19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; }; @@ -162,7 +169,6 @@ BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; }; BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; }; BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF580497246A3D19008AE704 /* UIKit.framework */; }; - BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; }; BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; }; BF66EE822501AE50007EE018 /* AltStoreCore.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE802501AE50007EE018 /* AltStoreCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; }; @@ -208,7 +214,6 @@ BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EEE52501AED0007EE018 /* JSONDecoder+Properties.swift */; }; BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */; }; BF66EEEB2501AED0007EE018 /* UIApplication+AppExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EEE72501AED0007EE018 /* UIApplication+AppExtension.swift */; }; - BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; }; BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; }; BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; }; BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */; }; @@ -239,7 +244,7 @@ BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; }; BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; }; BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; }; - BFAECC522501B0A400528F27 /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; }; + BFAECC522501B0A400528F27 /* CodableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableError.swift */; }; BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; }; BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; }; BFAECC552501B0A400528F27 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF624858BDE00DD5981 /* Connection.swift */; }; @@ -298,7 +303,7 @@ BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */; }; BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; }; BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; }; - BFECAC8824FD950E0077C41F /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; }; + BFECAC8824FD950E0077C41F /* CodableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableError.swift */; }; BFECAC8924FD950E0077C41F /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF22485828200DD5981 /* ConnectionManager.swift */; }; BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.swift */; }; BFECAC8B24FD950E0077C41F /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; }; @@ -535,6 +540,10 @@ 0EA166662ADFE122003015C1 /* base64.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = base64.h; path = Dependencies/libplist/src/base64.h; sourceTree = SOURCE_ROOT; }; 0EA166672ADFE122003015C1 /* strbuf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = strbuf.h; path = Dependencies/libplist/src/strbuf.h; sourceTree = SOURCE_ROOT; }; 0EA4B9BB2AE4A3F6009209CE /* plist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = plist.c; path = Dependencies/libplist/src/plist.c; sourceTree = SOURCE_ROOT; }; + 0EE7FDC02BE8BC2100D1E390 /* ALTWrappedError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWrappedError.m; sourceTree = ""; }; + 0EE7FDC22BE8BC4200D1E390 /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = ""; }; + 0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = ""; }; + 0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = ""; }; 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libEmotionalDamage.a; sourceTree = BUILT_PRODUCTS_DIR; }; 19104DB42909C06D00C49C7B /* EmotionalDamage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmotionalDamage.swift; sourceTree = ""; }; 191E5FAB290A5D92001A3B7C /* libminimuxer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libminimuxer.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -785,7 +794,7 @@ BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = ""; }; BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = ""; }; - BFD44605241188C300EAB90A /* CodableServerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableServerError.swift; sourceTree = ""; }; + BFD44605241188C300EAB90A /* CodableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableError.swift; sourceTree = ""; }; BFD52BD222A06EFB000B7ED1 /* ALTConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTConstants.h; sourceTree = ""; }; BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; }; BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; }; @@ -936,6 +945,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0EE7FDBF2BE8BBBF00D1E390 /* Errors */ = { + isa = PBXGroup; + children = ( + 0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */, + 0EE7FDC02BE8BC2100D1E390 /* ALTWrappedError.m */, + 0EE7FDC22BE8BC4200D1E390 /* ALTWrappedError.h */, + ); + path = Errors; + sourceTree = ""; + }; 19104DB32909C06D00C49C7B /* EmotionalDamage */ = { isa = PBXGroup; children = ( @@ -1061,7 +1080,7 @@ isa = PBXGroup; children = ( BF1E3128229F474900370A3C /* ServerProtocol.swift */, - BFD44605241188C300EAB90A /* CodableServerError.swift */, + BFD44605241188C300EAB90A /* CodableError.swift */, ); path = "Server Protocol"; sourceTree = ""; @@ -1074,6 +1093,7 @@ BF18BFFF2485A75F00DD5981 /* Server Protocol */, BFF767CF2489AC240097E58C /* Connections */, BFF7C92D2578464D00E55F36 /* XPC */, + 0EE7FDBF2BE8BBBF00D1E390 /* Errors */, BFF767C32489A6800097E58C /* Extensions */, BFF767C42489A6980097E58C /* Categories */, ); @@ -1775,6 +1795,7 @@ children = ( D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */, D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */, + 0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */, ); path = "Error Log"; sourceTree = ""; @@ -1827,6 +1848,7 @@ BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */, BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */, BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */, + 0EE7FDC82BE8CF4800D1E390 /* ALTWrappedError.h in Headers */, BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2271,7 +2293,7 @@ BF1FE358251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, BFECAC8F24FD950E0077C41F /* Result+Conveniences.swift in Sources */, BF8CAE472489E772004D6CCE /* DaemonRequestHandler.swift in Sources */, - BFECAC8824FD950E0077C41F /* CodableServerError.swift in Sources */, + BFECAC8824FD950E0077C41F /* CodableError.swift in Sources */, BFC712C32512D5F100AB5EBE /* XPCConnection.swift in Sources */, BFC712C52512D5F100AB5EBE /* XPCConnectionHandler.swift in Sources */, BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */, @@ -2368,8 +2390,8 @@ BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */, BF580482246A28F7008AE704 /* ViewController.swift in Sources */, BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */, + 0EE7FDC62BE8CEA300D1E390 /* ALTLocalizedError.swift in Sources */, 03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */, - BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */, BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2385,7 +2407,7 @@ BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */, BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */, BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */, - BFAECC522501B0A400528F27 /* CodableServerError.swift in Sources */, + BFAECC522501B0A400528F27 /* CodableError.swift in Sources */, BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */, BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */, BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */, @@ -2398,6 +2420,7 @@ BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */, BFAECC552501B0A400528F27 /* Connection.swift in Sources */, BF66EEDA2501AECA007EE018 /* RefreshAttempt.swift in Sources */, + 0EE7FDCB2BE8D12B00D1E390 /* ALTLocalizedError.swift in Sources */, BF66EEA92501AEC5007EE018 /* Tier.swift in Sources */, BF66EEDB2501AECA007EE018 /* StoreApp.swift in Sources */, BF66EEDE2501AECA007EE018 /* AppID.swift in Sources */, @@ -2406,6 +2429,7 @@ BF66EEDD2501AECA007EE018 /* AppPermission.swift in Sources */, D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */, BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */, + 0EE7FDC72BE8CF4100D1E390 /* ALTWrappedError.m in Sources */, D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */, BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */, BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */, @@ -2430,6 +2454,7 @@ BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */, BF66EECC2501AECA007EE018 /* Source.swift in Sources */, BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */, + 0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */, BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */, BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */, @@ -2485,7 +2510,6 @@ BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */, - BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */, D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, @@ -2533,9 +2557,11 @@ BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, + 0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */, BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, + 0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */, BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */, diff --git a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index a4c18b78..00000000 --- a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,113 +0,0 @@ -{ - "pins" : [ - { - "identity" : "altsign", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SideStore/AltSign", - "state" : { - "branch" : "master", - "revision" : "cc6189f0f7cd8e5bd24943af9322e0ff9420e9f4" - } - }, - { - "identity" : "appcenter-sdk-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/microsoft/appcenter-sdk-apple.git", - "state" : { - "revision" : "b2dc99cfedead0bad4e6573d86c5228c89cff332", - "version" : "4.4.3" - } - }, - { - "identity" : "imobiledevice.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SideStore/iMobileDevice.swift", - "state" : { - "revision" : "74e481106dd155c0cd21bca6795fd9fe5f751654", - "version" : "1.0.5" - } - }, - { - "identity" : "keychainaccess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state" : { - "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", - "version" : "4.2.2" - } - }, - { - "identity" : "launchatlogin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sindresorhus/LaunchAtLogin.git", - "state" : { - "revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41", - "version" : "4.2.0" - } - }, - { - "identity" : "nuke", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kean/Nuke.git", - "state" : { - "revision" : "9318d02a8a6d20af56505c9673261c1fd3b3aebe", - "version" : "7.6.3" - } - }, - { - "identity" : "openssl", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/OpenSSL", - "state" : { - "revision" : "0faf71a188bcfdf0245cab42886b9b240ca71c52", - "version" : "1.1.2200" - } - }, - { - "identity" : "plcrashreporter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/microsoft/PLCrashReporter.git", - "state" : { - "revision" : "81cdec2b3827feb03286cb297f4c501a8eb98df1", - "version" : "1.10.2" - } - }, - { - "identity" : "semanticversion", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git", - "state" : { - "revision" : "a70840d5fca686ae3bd2fcf8aecc5ded0bd4f125", - "version" : "0.3.6" - } - }, - { - "identity" : "sparkle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle.git", - "state" : { - "revision" : "f0ceaf5cc9f3f23daa0ccb6dcebd79fc96ccc7d9", - "version" : "2.5.0" - } - }, - { - "identity" : "starscream", - "kind" : "remoteSourceControl", - "location" : "https://github.com/daltoniam/Starscream.git", - "state" : { - "revision" : "ac6c0fc9da221873e01bd1a0d4818498a71eef33", - "version" : "4.0.6" - } - }, - { - "identity" : "stprivilegedtask", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JoeMatt/STPrivilegedTask.git", - "state" : { - "branch" : "master", - "revision" : "10a9150ef32d444af326beba76356ae9af95a3e7" - } - } - ], - "version" : 2 -} diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift index fad4bead..d4514df3 100644 --- a/AltStore/Authentication/AuthenticationViewController.swift +++ b/AltStore/Authentication/AuthenticationViewController.swift @@ -108,8 +108,8 @@ private extension AuthenticationViewController case .failure(let error as NSError): DispatchQueue.main.async { - let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: "")) - + let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: "")) + let toastView = ToastView(error: error) toastView.textLabel.textColor = .altPink toastView.detailTextLabel.textColor = .altPink diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift index 6670efbd..5aba48e7 100644 --- a/AltStore/Components/ToastView.swift +++ b/AltStore/Components/ToastView.swift @@ -51,45 +51,28 @@ final class ToastView: RSTToastView var error = error as NSError var underlyingError = error.underlyingError - var preferredDuration: TimeInterval? - if let unwrappedUnderlyingError = underlyingError, error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue { - // Treat underlyingError as the primary error. - + // Treat underlyingError as the primary error, but keep localized title + failure. + let nsError = error as NSError error = unwrappedUnderlyingError as NSError + + if let localizedTitle = nsError.localizedTitle { + error = error.withLocalizedTitle(localizedTitle) + } + if let localizedFailure = nsError.localizedFailure { + error = error.withLocalizedFailure(localizedFailure) + } + underlyingError = nil - - preferredDuration = .longToastViewDuration } - - let text: String - let detailText: String? - - if let failure = error.localizedFailure - { - text = failure - detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription - } - else if let reason = error.localizedFailureReason - { - text = reason - detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription - } - else - { - text = error.localizedDescription - detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion - } - + let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "") + let detailText = error.localizedDescription + + self.init(text: text, detailText: detailText) - - if let preferredDuration = preferredDuration - { - self.preferredDuration = preferredDuration - } } required init(coder aDecoder: NSCoder) { diff --git a/AltStore/Intents/IntentHandler.swift b/AltStore/Intents/IntentHandler.swift index cad45e75..ff8db924 100644 --- a/AltStore/Intents/IntentHandler.swift +++ b/AltStore/Intents/IntentHandler.swift @@ -127,7 +127,7 @@ private extension IntentHandler self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) } - catch RefreshError.noInstalledApps + catch ~RefreshErrorCode.noInstalledApps { self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) } diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 310bba29..61611d56 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -369,7 +369,7 @@ extension AppManager case .success(let source): fetchedSources.insert(source) case .failure(let error): let source = managedObjectContext.object(with: source.objectID) as! Source - source.error = (error as NSError).sanitizedForCoreData() + source.error = (error as NSError).sanitizedForSerialization() errors[source] = error } @@ -457,7 +457,7 @@ extension AppManager group.completionHandler = { (results) in do { - guard let result = results.values.first else { throw context.error ?? OperationError.unknown } + guard let result = results.values.first else { throw context.error ?? OperationError.unknown() } completionHandler(result) } catch @@ -476,7 +476,7 @@ extension AppManager func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result) -> Void) -> Progress { guard let storeApp = app.storeApp else { - completionHandler(.failure(OperationError.appNotFound)) + completionHandler(.failure(OperationError.appNotFound(name: app.name))) return Progress.discreteProgress(totalUnitCount: 1) } @@ -484,7 +484,7 @@ extension AppManager group.completionHandler = { (results) in do { - guard let result = results.values.first else { throw OperationError.unknown } + guard let result = results.values.first else { throw OperationError.unknown() } completionHandler(result) } catch @@ -520,8 +520,8 @@ extension AppManager group.completionHandler = { (results) in do { - guard let result = results.values.first else { throw OperationError.unknown } - + guard let result = results.values.first else { throw OperationError.unknown() } + let installedApp = try result.get() assert(installedApp.managedObjectContext != nil) @@ -559,7 +559,7 @@ extension AppManager group.completionHandler = { (results) in do { - guard let result = results.values.first else { throw OperationError.unknown } + guard let result = results.values.first else { throw OperationError.unknown() } let installedApp = try result.get() assert(installedApp.managedObjectContext != nil) @@ -585,8 +585,8 @@ extension AppManager group.completionHandler = { (results) in do { - guard let result = results.values.first else { throw OperationError.unknown } - + guard let result = results.values.first else { throw OperationError.unknown() } + let installedApp = try result.get() assert(installedApp.managedObjectContext != nil) @@ -610,7 +610,7 @@ extension AppManager group.completionHandler = { (results) in do { - guard let result = results.values.first else { throw OperationError.unknown } + guard let result = results.values.first else { throw OperationError.unknown() } let installedApp = try result.get() assert(installedApp.managedObjectContext != nil) @@ -1269,7 +1269,7 @@ private extension AppManager case .success(let installedApp): completionHandler(.success(installedApp)) - case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound): + case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound(name: app.name)): // Fall back to installation if AltServer doesn't support newer provisioning profile requests, // OR if the cached app could not be found and we may need to redownload it. app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return. @@ -1543,7 +1543,7 @@ private extension AppManager } guard let application = ALTApplication(fileURL: app.fileURL) else { - completionHandler(.failure(OperationError.appNotFound)) + completionHandler(.failure(OperationError.appNotFound(name: app.name))) return progress } @@ -1555,8 +1555,8 @@ private extension AppManager let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString) try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) - guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound } - + guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: app.name) } + let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL) guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp } @@ -1694,8 +1694,32 @@ private extension AppManager do { try installedApp.managedObjectContext?.save() } catch { print("Error saving installed app.", error) } } - catch + catch let nsError as NSError { + var appName: String! + if let app = operation.app as? (NSManagedObject & AppProtocol) { + if let context = app.managedObjectContext { + context.performAndWait { + appName = app.name + } + } else { + appName = NSLocalizedString("Unknown App", comment: "") + } + } else { + appName = operation.app.name + } + + let localizedTitle: String + switch operation { + case .install: localizedTitle = String(format: NSLocalizedString("Failed to Install %@", comment: ""), appName) + case .refresh: localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@", comment: ""), appName) + case .update: localizedTitle = String(format: NSLocalizedString("Failed to Update %@", comment: ""), appName) + case .activate: localizedTitle = String(format: NSLocalizedString("Failed to Activate %@", comment: ""), appName) + case .deactivate: localizedTitle = String(format: NSLocalizedString("Failed to Deactivate %@", comment: ""), appName) + case .backup: localizedTitle = String(format: NSLocalizedString("Failed to Backup %@", comment: ""), appName) + case .restore: localizedTitle = String(format: NSLocalizedString("Failed to Restore %@ Backup", comment: ""), appName) + } + let error = nsError.withLocalizedTitle(localizedTitle) group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier) self.log(error, for: operation) @@ -1726,7 +1750,7 @@ private extension AppManager func log(_ error: Error, for operation: AppOperation) { // Sanitize NSError on same thread before performing background task. - let sanitizedError = (error as NSError).sanitizedForCoreData() + let sanitizedError = (error as NSError).sanitizedForSerialization() let loggedErrorOperation: LoggedError.Operation = { switch operation diff --git a/AltStore/Managing Apps/AppManagerErrors.swift b/AltStore/Managing Apps/AppManagerErrors.swift index 60fa34b9..3e51f80c 100644 --- a/AltStore/Managing Apps/AppManagerErrors.swift +++ b/AltStore/Managing Apps/AppManagerErrors.swift @@ -22,13 +22,27 @@ extension AppManager var managedObjectContext: NSManagedObjectContext? - var errorDescription: String? { - if let error = self.primaryError - { - return error.localizedDescription + var localizedTitle: String? { + var localizedTitle: String? + self.managedObjectContext?.performAndWait { + if self.sources?.count == 1 { + localizedTitle = NSLocalizedString("Failed to refresh Store", comment: "") + } else if self.errors.count == 1 { + guard let source = self.errors.keys.first else { return } + localizedTitle = String(format: NSLocalizedString("Failed to refresh Source '%@'", comment: ""), source.name) + } else { + localizedTitle = String(format: NSLocalizedString("Failed to refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count)) + } } - else - { + return localizedTitle + } + + var errorDescription: String? { + if let error = self.primaryError { + return error.localizedDescription + } else if let error = self.errors.values.first, self.errors.count == 1 { + return error.localizedDescription + } else { var localizedDescription: String? self.managedObjectContext?.performAndWait { @@ -67,8 +81,14 @@ extension AppManager } var errorUserInfo: [String : Any] { - guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] } - return [NSUnderlyingErrorKey: error] + let errors = Array(self.errors.values) + var userInfo = [String: Any]() + userInfo[ALTLocalizedTitleErrorKey] = self.localizedTitle + userInfo[NSUnderlyingErrorKey] = self.primaryError + if #available(iOS 14.5, *), !errors.isEmpty { + userInfo[NSMultipleUnderlyingErrorsKey] = errors + } + return userInfo } init(_ error: Error) diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index 6cecb5b0..ecc5e19b 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -14,7 +14,8 @@ import AltStoreCore import AltSign import minimuxer -enum AuthenticationError: LocalizedError +typealias AuthenticationError = AuthenticationErrorCode.Error +enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable { case noTeam case noCertificate @@ -23,11 +24,11 @@ enum AuthenticationError: LocalizedError case missingPrivateKey case missingCertificate - var errorDescription: String? { + var errorFailureReason: String { switch self { - case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "") + case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "") + case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "") case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "") - case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "") case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "") case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "") } @@ -213,8 +214,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A guard let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context), let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context) - else { throw AuthenticationError.noTeam } - + else { throw AuthenticationError(.noTeam) } + // Account account.isActiveAccount = true @@ -432,7 +433,7 @@ private extension AuthenticationOperation } else { - completionHandler(.failure(error ?? OperationError.unknown)) + completionHandler(.failure(error ?? OperationError.unknown())) } } } @@ -449,7 +450,7 @@ private extension AuthenticationOperation if let team = teams.first { return completionHandler(.success(team)) } else { - return completionHandler(.failure(AuthenticationError.noTeam)) + return completionHandler(.failure(AuthenticationError(.noTeam))) } } else { DispatchQueue.main.async { @@ -460,7 +461,7 @@ private extension AuthenticationOperation if !self.present(selectTeamViewController) { - return completionHandler(.failure(AuthenticationError.noTeam)) + return completionHandler(.failure(AuthenticationError(.noTeam))) } } } @@ -494,15 +495,15 @@ private extension AuthenticationOperation do { let certificate = try Result(certificate, error).get() - guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey } - + guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) } + ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in do { let certificates = try Result(certificates, error).get() guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else { - throw AuthenticationError.missingCertificate + throw AuthenticationError(.missingCertificate) } certificate.privateKey = privateKey @@ -523,7 +524,7 @@ private extension AuthenticationOperation func replaceCertificate(from certificates: [ALTCertificate]) { - guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) } + guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError(.noCertificate))) } ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in if let error = error, !success diff --git a/AltStore/Operations/BackgroundRefreshAppsOperation.swift b/AltStore/Operations/BackgroundRefreshAppsOperation.swift index 88a714a1..2437e83c 100644 --- a/AltStore/Operations/BackgroundRefreshAppsOperation.swift +++ b/AltStore/Operations/BackgroundRefreshAppsOperation.swift @@ -13,11 +13,12 @@ import AltStoreCore import EmotionalDamage import minimuxer -enum RefreshError: LocalizedError +typealias RefreshError = RefreshErrorCode.Error +enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable { case noInstalledApps - var errorDescription: String? { + var errorFailureReason: String { switch self { case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "") @@ -94,7 +95,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result let appName = installedApp.name self.appName = appName - let altstoreOpenURL = URL(string: "sidestore://")! + guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { + throw OperationError.appNotFound(name: appName) + } + let altstoreOpenURL = altstoreApp.openAppURL var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false) returnURLComponents?.host = "appBackupResponse" guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) } - + var openURLComponents = URLComponents() openURLComponents.scheme = installedApp.openAppURL.scheme openURLComponents.host = self.action.rawValue diff --git a/AltStore/Operations/DownloadAppOperation.swift b/AltStore/Operations/DownloadAppOperation.swift index 83820388..8295873c 100644 --- a/AltStore/Operations/DownloadAppOperation.swift +++ b/AltStore/Operations/DownloadAppOperation.swift @@ -12,29 +12,13 @@ import Roxas import AltStoreCore import AltSign -private extension DownloadAppOperation -{ - struct DependencyError: ALTLocalizedError - { - let dependency: Dependency - let error: Error - - var failure: String? { - return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename) - } - - var underlyingError: Error? { - return self.error - } - } -} - @objc(DownloadAppOperation) final class DownloadAppOperation: ResultOperation { let app: AppProtocol let context: AppOperationContext + private let appName: String private let bundleIdentifier: String private var sourceURL: URL? private let destinationURL: URL @@ -47,6 +31,7 @@ final class DownloadAppOperation: ResultOperation self.app = app self.context = context + self.appName = app.name self.bundleIdentifier = app.bundleIdentifier self.sourceURL = app.url self.destinationURL = destinationURL @@ -69,8 +54,8 @@ final class DownloadAppOperation: ResultOperation print("Downloading App:", self.bundleIdentifier) - guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) } - + guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) } + self.downloadApp(from: sourceURL) { result in do { @@ -138,8 +123,8 @@ private extension DownloadAppOperation let fileURL = try result.get() var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound } - + guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) } + try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil) let appBundleURL: URL @@ -252,7 +237,7 @@ private extension DownloadAppOperation let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data) var dependencyURLs = Set() - var dependencyError: DependencyError? + var dependencyError: Error? let dispatchGroup = DispatchGroup() let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1) @@ -285,7 +270,7 @@ private extension DownloadAppOperation } catch let error as DecodingError { - let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name)) + let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name)) completionHandler(.failure(nsError)) } catch @@ -294,7 +279,7 @@ private extension DownloadAppOperation } } - func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result) -> Void) + func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result) -> Void) { let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in do @@ -315,9 +300,10 @@ private extension DownloadAppOperation completionHandler(.success(destinationURL)) } - catch + catch let error as NSError { - completionHandler(.failure(DependencyError(dependency: dependency, error: error))) + let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename) + completionHandler(.failure(error.withLocalizedFailure(localizedFailure))) } } progress.addChild(downloadTask.progress, withPendingUnitCount: 1) diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift index 6cf75332..730db3d9 100644 --- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift +++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift @@ -45,8 +45,8 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv let session = self.context.session else { return self.finish(.failure(OperationError.invalidParameters)) } - guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) } - + guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) } + self.progress.totalUnitCount = Int64(1 + app.appExtensions.count) self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in @@ -260,7 +260,7 @@ extension FetchProvisioningProfilesOperation { if let expirationDate = sortedExpirationDates.first { - throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate) + throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate) } } } @@ -286,7 +286,7 @@ extension FetchProvisioningProfilesOperation { if let expirationDate = sortedExpirationDates.first { - throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate) + throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate) } else { diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index d05ea749..f2ad9382 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -8,81 +8,182 @@ import Foundation import AltSign +import AltStoreCore import minimuxer -enum OperationError: LocalizedError +extension OperationError { - static let domain = OperationError.unknown._domain - - case unknown - case unabletoconnectSideJIT - case unabletoconSideJITDevice - case wrongIP - case SideJITIssue(error: String) - case refreshsidejit - case unknownResult - case cancelled - case timedOut - - case notAuthenticated - case appNotFound - - case unknownUDID - - case invalidApp - case invalidParameters - - case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date) - - case noSources - - case openAppFailed(name: String) - case missingAppGroup - - case noWiFi - case tooNewError - case anisetteV1Error(message: String) - case provisioningError(result: String, message: String?) - case anisetteV3Error(message: String) - - case cacheClearError(errors: [String]) - - var failureReason: String? { - switch self { - case .unknown: return NSLocalizedString("An unknown error occured.", comment: "") + enum Code: Int, ALTErrorCode, CaseIterable { + typealias Error = OperationError + + // General + case unknown = 1000 + case unknownResult + case cancelled + case timedOut + case unabletoconnectSideJIT + case unabletoconSideJITDevice + case wrongIP + case SideJITIssue(error: String) + case refreshsidejit + case notAuthenticated + case appNotFound + case unknownUDID + case invalidApp + case invalidParameters + case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date) + case noSources + case openAppFailed//(name: String) + case missingAppGroup + + // Connection + case noWiFi = 1200 + case tooNewError + case anisetteV1Error//(message: String) + case provisioningError//(result: String, message: String?) + case anisetteV3Error//(message: String) + + case cacheClearError//(errors: [String]) + } + + static let unknownResult: OperationError = .init(code: .unknownResult) + static let cancelled: OperationError = .init(code: .cancelled) + static let timedOut: OperationError = .init(code: .timedOut) + static let notAuthenticated: OperationError = .init(code: .notAuthenticated) + static let unknownUDID: OperationError = .init(code: .unknownUDID) + static let invalidApp: OperationError = .init(code: .invalidApp) + static let invalidParameters: OperationError = .init(code: .invalidParameters) + static let noSources: OperationError = .init(code: .noSources) + static let missingAppGroup: OperationError = .init(code: .missingAppGroup) + + static let noWiFi: OperationError = .init(code: .missingAppGroup) + static let tooNewError: OperationError = .init(code: .tooNewError) + static let provisioningError: OperationError = .init(code: .provisioningError) + static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error) + static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error) + + static let cacheClearError: OperationError = .init(code: .cacheClearError) + + static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError { + OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line) + } + + static func appNotFound(name: String?) -> OperationError { + OperationError(code: .appNotFound, appName: name) + } + + static func openAppFailed(name: String?) -> OperationError { + OperationError(code: .openAppFailed, appName: name) + } + + static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError { + OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate) + } + + static func provisioningError(result: String, message: String?) -> OperationError { + var o = OperationError(code: .provisioningError, failureReason: result) + o.errorTitle = message + return o + } + + static func cacheClearError(errors: [String]) -> OperationError { + OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n")) + } + + static func anisetteV1Error(message: String) -> OperationError { + OperationError(code: .anisetteV1Error, failureReason: message) + } + + static func anisetteV3Error(message: String) -> OperationError { + OperationError(code: .anisetteV3Error, failureReason: message) + } + +} + + +struct OperationError: ALTLocalizedError { + + let code: Code + + var errorTitle: String? + var errorFailure: String? + + var appName: String? + + var requiredAppIDs: Int? + var availableAppIDs: Int? + var expirationDate: Date? + + var sourceFile: String? + var sourceLine: UInt? + + private var _failureReason: String? + + private init(code: Code, failureReason: String? = nil, + appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil, + expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){ + self.code = code + self._failureReason = failureReason + + self.appName = appName + self.requiredAppIDs = requiredAppIDs + self.availableAppIDs = availableAppIDs + self.expirationDate = expirationDate + self.sourceFile = sourceFile + self.sourceLine = sourceLine + } + + var errorFailureReason: String { + switch self.code { + case .unknown: + var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "") + guard let sourceFile, let sourceLine else { return failureReason } + failureReason += " (\(sourceFile) line \(sourceLine)" + return failureReason case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .timedOut: return NSLocalizedString("The operation timed out.", comment: "") case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "") - case .appNotFound: return NSLocalizedString("App not found.", comment: "") - case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "") - case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "") + case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "") + case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "") case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "") + case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "") case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "") - case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name) - case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "") - case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "") + case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "") + case .appNotFound: + let appName = self.appName ?? NSLocalizedString("The app", comment: "") + return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName) + case .openAppFailed: + let appName = self.appName ?? NSLocalizedString("The app", comment: "") + return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName) case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi!\nSideStore will never be able to install or refresh applications without WiFi.", comment: "") - case .unabletoconnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "") - case .unabletoconSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "") - case .wrongIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "") - case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "") case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without the use of SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "") case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message) case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "") case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message) case .cacheClearError(let errors): return String(format: NSLocalizedString("An error occurred while clearing cache: %@", comment: ""), errors.joined(separator: "\n")) case .SideJITIssue(let errors): return NSLocalizedString("SideJITServer Error: \(errors)", comment: "") + case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "") + case .unabletoconnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "") + case .unabletoconSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "") + case .wrongIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "") + case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "") + case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server. Try using another anisette server.", comment: "") + case .provisioningError: return NSLocalizedString("An error occurred when provisioning: @@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "") + case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server. Please try again. If the issue persists, report it on GitHub Issues!", comment: "") + case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "") } } var recoverySuggestion: String? { - switch self + switch self.code { - case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date): + case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "") + case .maximumAppIDLimitReached: let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "") - let message: String - + guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage } + var message: String + if requiredAppIDs > 1 { let availableText: String @@ -94,23 +195,23 @@ enum OperationError: LocalizedError default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs)) } - let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText) - message = prefixMessage + " " + baseMessage + let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText) + message = prefixMessage + " " + baseMessage + "\n\n" } else { - let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date) - - let dateComponentsFormatter = DateComponentsFormatter() - dateComponentsFormatter.maximumUnitCount = 1 - dateComponentsFormatter.unitsStyle = .full - - let remainingTime = dateComponentsFormatter.string(from: dateComponents)! - - let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime) - message = baseMessage + " " + remainingTimeMessage + message = baseMessage + " " } - + + let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate) + let dateFormatter = DateComponentsFormatter() + dateFormatter.maximumUnitCount = 1 + dateFormatter.unitsStyle = .full + + let remainingTime = dateFormatter.string(from: dateComponents)! + + message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime) + return message default: return nil diff --git a/AltStore/Operations/Patch App/PatchAppOperation.swift b/AltStore/Operations/Patch App/PatchAppOperation.swift index 0d06f6b8..6eb9b08a 100644 --- a/AltStore/Operations/Patch App/PatchAppOperation.swift +++ b/AltStore/Operations/Patch App/PatchAppOperation.swift @@ -25,22 +25,38 @@ protocol PatchAppContext var error: Error? { get } } -enum PatchAppError: LocalizedError +extension PatchAppError { - case unsupportedOperatingSystemVersion(OperatingSystemVersion) - - var errorDescription: String? { - switch self - { - case .unsupportedOperatingSystemVersion(let osVersion): - var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)" - if osVersion.patchVersion != 0 - { - osVersionString += ".\(osVersion.patchVersion)" + enum Code: Int, ALTErrorCode, CaseIterable { + typealias Error = PatchAppError + + case unsupportedOperatingSystemVersion + } + + static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError { + PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion) + } +} + +struct PatchAppError: ALTLocalizedError { + let code: Code + + var errorTitle: String? + var errorFailure: String? + + var osVersion: OperatingSystemVersion? + + var errorFailureReason: String { + switch self.code { + case .unsupportedOperatingSystemVersion: + let osVersionString: String + + if let osVersion = self.osVersion?.stringValue { + osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion + } else { + osVersionString = NSLocalizedString("your device's iOS version", comment: "") } - - let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString) - return errorDescription + return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString) } } } diff --git a/AltStore/Operations/Patch App/PatchViewController.swift b/AltStore/Operations/Patch App/PatchViewController.swift index 9e7afec9..0e44826b 100644 --- a/AltStore/Operations/Patch App/PatchViewController.swift +++ b/AltStore/Operations/Patch App/PatchViewController.swift @@ -439,7 +439,7 @@ private extension PatchViewController do { - guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown } + guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() } _ = try result.get() if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier) diff --git a/AltStore/Operations/RefreshAppOperation.swift b/AltStore/Operations/RefreshAppOperation.swift index 86a20b7a..8f7a17c1 100644 --- a/AltStore/Operations/RefreshAppOperation.swift +++ b/AltStore/Operations/RefreshAppOperation.swift @@ -38,8 +38,8 @@ final class RefreshAppOperation: ResultOperation if let error = self.context.error { return self.finish(.failure(error)) } guard let profiles = self.context.provisioningProfiles else { return self.finish(.failure(OperationError.invalidParameters)) } - guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) } - + guard let app = self.context.app else { return self.finish(.failure(OperationError(.appNotFound(name: nil)))) } + for p in profiles { do { let bytes = p.value.data.toRustByteSlice() @@ -49,14 +49,13 @@ final class RefreshAppOperation: ResultOperation } DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - print("Sending refresh app request...") self.progress.completedUnitCount += 1 let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier) self.managedObjectContext.perform { guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else { - self.finish(.failure(OperationError.appNotFound)) + self.finish(.failure(OperationError(.appNotFound(name: app.name)))) return } installedApp.update(provisioningProfile: p.value) diff --git a/AltStore/Operations/SendAppOperation.swift b/AltStore/Operations/SendAppOperation.swift index f900a0af..1ed6fd67 100644 --- a/AltStore/Operations/SendAppOperation.swift +++ b/AltStore/Operations/SendAppOperation.swift @@ -57,7 +57,7 @@ final class SendAppOperation: ResultOperation<()> } } else { print("IPA doesn't exist????") - self.finish(.failure(OperationError.appNotFound)) + self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name)))) } } } diff --git a/AltStore/Operations/VerifyAppOperation.swift b/AltStore/Operations/VerifyAppOperation.swift index c595d52f..9b262973 100644 --- a/AltStore/Operations/VerifyAppOperation.swift +++ b/AltStore/Operations/VerifyAppOperation.swift @@ -8,48 +8,71 @@ import Foundation +import AltStoreCore import AltSign import Roxas -enum VerificationError: ALTLocalizedError +extension VerificationError { - case privateEntitlements(ALTApplication, entitlements: [String: Any]) - case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String) - case iOSVersionNotSupported(ALTApplication) - - var app: ALTApplication { - switch self - { - case .privateEntitlements(let app, _): return app - case .mismatchedBundleIdentifiers(let app, _): return app - case .iOSVersionNotSupported(let app): return app - } + enum Code: Int, ALTErrorCode, CaseIterable { + typealias Error = VerificationError + + case privateEntitlements + case mismatchedBundleIdentifiers + case iOSVersionNotSupported } - - var failure: String? { - return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name) + + static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError { + VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements) } - - var failureReason: String? { - switch self + + static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError { + VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID) + } + + static func iOSVersionNotSupported(app: ALTApplication) -> VerificationError { + VerificationError(code: .iOSVersionNotSupported, app: app) + } +} + +struct VerificationError: ALTLocalizedError { + let code: Code + + var errorTitle: String? + var errorFailure: String? + + var app: ALTApplication? + var entitlements: [String: Any]? + var sourceBundleID: String? + + var errorFailureReason: String { + switch self.code { - case .privateEntitlements(let app, _): - return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name) - - case .mismatchedBundleIdentifiers(let app, let sourceBundleID): - return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID) - - case .iOSVersionNotSupported(let app): - let name = app.name - - var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)" - if app.minimumiOSVersion.patchVersion > 0 - { - version += ".\(app.minimumiOSVersion.patchVersion)" + case .privateEntitlements: + let appName = (self.app?.name as String?).map { String(format: NSLocalizedString("'%@'", comment: ""), $0) } ?? + NSLocalizedString("Unknown app", comment: "") + return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), appName) + + case .mismatchedBundleIdentifiers: + if let app, let sourceBundleID { + return String(format: NSLocalizedString("The bundle ID '%@' does not match the one specified by the source ('%@').", comment: ""), app.bundleIdentifier, sourceBundleID) + } else { + return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "") } - - let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version) - return localizedDescription + + case .iOSVersionNotSupported: + var failureReason: String! + if let app { + var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)" + if app.minimumiOSVersion.patchVersion > 0 { + version += ".\(app.minimumiOSVersion.patchVersion)" + } + failureReason = String(format: NSLocalizedString("%@ requires %@.", comment: ""), app.name, version) + } else { + let version = ProcessInfo.processInfo.operatingSystemVersionString + failureReason = String(format: NSLocalizedString("This app does not support iOS %@.", comment: ""), version) + } + return failureReason } } } @@ -81,11 +104,11 @@ final class VerifyAppOperation: ResultOperation guard let app = self.context.app else { throw OperationError.invalidParameters } guard app.bundleIdentifier == self.context.bundleIdentifier else { - throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier) + throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app) } guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else { - throw VerificationError.iOSVersionNotSupported(app) + throw VerificationError.iOSVersionNotSupported(app: app) } if #available(iOS 13.5, *) @@ -116,7 +139,7 @@ final class VerifyAppOperation: ResultOperation let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any] app.hasPrivateEntitlements = true - let error = VerificationError.privateEntitlements(app, entitlements: entitlements) + let error = VerificationError.privateEntitlements(entitlements, app: app) self.process(error) { (result) in self.finish(result.mapError { $0 as Error }) } @@ -145,9 +168,10 @@ private extension VerifyAppOperation guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) } DispatchQueue.main.async { - switch error + switch error.code { - case .privateEntitlements(_, let entitlements): + case .privateEntitlements: + guard let entitlements = error.entitlements else { return completion(.failure(error)) } let permissions = entitlements.keys.sorted().joined(separator: "\n") let message = String(format: NSLocalizedString(""" You must allow access to these private permissions before continuing: diff --git a/AltStore/Settings/Error Log/ErrorDetailsViewController.swift b/AltStore/Settings/Error Log/ErrorDetailsViewController.swift new file mode 100644 index 00000000..c3cd5a2c --- /dev/null +++ b/AltStore/Settings/Error Log/ErrorDetailsViewController.swift @@ -0,0 +1,53 @@ +// +// ErrorDetailsViewController.swift +// AltStore +// +// Created by Riley Testut on 10/5/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import UIKit + +import AltStoreCore + +class ErrorDetailsViewController: UIViewController +{ + var loggedError: LoggedError? + + @IBOutlet private var textView: UITextView! + + override func viewDidLoad() + { + super.viewDidLoad() + + if let error = self.loggedError?.error + { + self.title = error.localizedErrorCode + + let font = self.textView.font ?? UIFont.preferredFont(forTextStyle: .body) + let detailedDescription = error.formattedDetailedDescription(with: font) + self.textView.attributedText = detailedDescription + } + else + { + self.title = NSLocalizedString("Error Details", comment: "") + } + + self.navigationController?.navigationBar.tintColor = .altPrimary + + if #available(iOS 15, *), let sheetController = self.navigationController?.sheetPresentationController + { + sheetController.detents = [.medium(), .large()] + sheetController.selectedDetentIdentifier = .medium + sheetController.prefersGrabberVisible = true + } + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + self.textView.textContainerInset.left = self.view.layoutMargins.left + self.textView.textContainerInset.right = self.view.layoutMargins.right + } +} diff --git a/AltStore/Settings/Error Log/ErrorLogViewController.swift b/AltStore/Settings/Error Log/ErrorLogViewController.swift index 7a67a301..bde4aa91 100644 --- a/AltStore/Settings/Error Log/ErrorLogViewController.swift +++ b/AltStore/Settings/Error Log/ErrorLogViewController.swift @@ -39,6 +39,15 @@ final class ErrorLogViewController: UITableViewController self.tableView.dataSource = self.dataSource self.tableView.prefetchDataSource = self.dataSource } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return } + + let navigationController = segue.destination as! UINavigationController + + let errorDetailsViewController = navigationController.viewControllers.first as! ErrorDetailsViewController + errorDetailsViewController.loggedError = loggedError + } } private extension ErrorLogViewController @@ -60,14 +69,8 @@ private extension ErrorLogViewController let cell = cell as! ErrorLogTableViewCell cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date) cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "") - - switch loggedError.domain - { - case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code)) - case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code)) - default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode - } - + cell.errorCodeLabel.text = loggedError.error.localizedErrorCode + let nsError = loggedError.error as NSError let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") cell.errorDescriptionTextView.text = errorDescription @@ -93,6 +96,9 @@ private extension ErrorLogViewController }, UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in self?.searchFAQ(for: loggedError) + }, + UIAction(title: NSLocalizedString("View More Details", comment: ""), image: UIImage(systemName: "ellipsis.circle")) { [weak self] _ in + } ]) @@ -235,13 +241,17 @@ private extension ErrorLogViewController let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")! var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! - let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+") + let query = [loggedError.domain, "\(loggedError.error.displayCode)"].joined(separator: "+") components.queryItems = [URLQueryItem(name: "q", value: query)] let safariViewController = SFSafariViewController(url: components.url ?? baseURL) safariViewController.preferredControlTintColor = .altPrimary self.present(safariViewController, animated: true) } + + func viewMoreDetails(for loggedError: LoggedError) { + self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError) + } } extension ErrorLogViewController diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 02c8f716..999bcb52 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -21,7 +21,7 @@