diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index fca08c0b..4308528c 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; }; + 0E05025A2BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */; }; + 0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E05025B2BEC947000879B5C /* String+SideStore.swift */; }; 0E1A1F912AE36A9700364CAD /* bytearray.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E1A1F902AE36A9600364CAD /* bytearray.c */; }; 0E764E172ADFF5740043DD4E /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = 0E764E162ADFF5740043DD4E /* AltBackup.ipa */; }; 0EA1665B2ADFE0D2003015C1 /* out-limd.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166472ADFE0D1003015C1 /* out-limd.c */; }; @@ -35,13 +37,20 @@ 0EA1667B2ADFE140003015C1 /* Structure.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 0EA1665A2ADFE0D2003015C1 /* Structure.cpp */; }; 0EA1667D2ADFE140003015C1 /* xplist.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166592ADFE0D2003015C1 /* xplist.c */; }; 0EA1667E2ADFE140003015C1 /* time64.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA1664C2ADFE0D1003015C1 /* time64.c */; }; + 0EA4263A2C2230150026D7FB /* AnisetteServerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA426392C2230150026D7FB /* AnisetteServerList.swift */; }; 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 */; }; 191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; }; 191E5FDC290AFA5C001A3B7C /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 191E5FDB290AFA5C001A3B7C /* OpenSSL */; }; - 1920B04F2924AC8300744F60 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1920B04E2924AC8300744F60 /* Settings.bundle */; }; 19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */; }; 4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; @@ -162,7 +171,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 +216,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 +246,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 +305,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 */; }; @@ -499,6 +506,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = ""; }; + 0E05025B2BEC947000879B5C /* String+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SideStore.swift"; sourceTree = ""; }; 0E1A1F902AE36A9600364CAD /* bytearray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = bytearray.c; path = src/bytearray.c; sourceTree = ""; }; 0E764E162ADFF5740043DD4E /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; name = AltBackup.ipa; path = AltStore/Resources/AltBackup.ipa; sourceTree = SOURCE_ROOT; }; 0EA166412ADFE0D1003015C1 /* jplist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = jplist.c; path = Dependencies/libplist/src/jplist.c; sourceTree = SOURCE_ROOT; }; @@ -534,11 +543,15 @@ 0EA166652ADFE122003015C1 /* hashtable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = hashtable.h; path = Dependencies/libplist/src/hashtable.h; sourceTree = SOURCE_ROOT; }; 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; }; + 0EA426392C2230150026D7FB /* AnisetteServerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnisetteServerList.swift; sourceTree = ""; }; 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; }; - 1920B04E2924AC8300744F60 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = ""; }; 9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "minimuxer-helpers.swift"; path = "Dependencies/minimuxer/minimuxer-helpers.swift"; sourceTree = SOURCE_ROOT; }; 99F87D1629D8E4C900B40039 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftBridgeCore.swift; path = Dependencies/minimuxer/SwiftBridgeCore.swift; sourceTree = SOURCE_ROOT; }; @@ -785,7 +798,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 +949,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 +1084,7 @@ isa = PBXGroup; children = ( BF1E3128229F474900370A3C /* ServerProtocol.swift */, - BFD44605241188C300EAB90A /* CodableServerError.swift */, + BFD44605241188C300EAB90A /* CodableError.swift */, ); path = "Server Protocol"; sourceTree = ""; @@ -1074,6 +1097,7 @@ BF18BFFF2485A75F00DD5981 /* Server Protocol */, BFF767CF2489AC240097E58C /* Connections */, BFF7C92D2578464D00E55F36 /* XPC */, + 0EE7FDBF2BE8BBBF00D1E390 /* Errors */, BFF767C32489A6800097E58C /* Extensions */, BFF767C42489A6980097E58C /* Categories */, ); @@ -1396,6 +1420,8 @@ BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */, BF66EEE42501AED0007EE018 /* UserDefaults+AltStore.swift */, BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */, + 0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */, + 0E05025B2BEC947000879B5C /* String+SideStore.swift */, ); path = Extensions; sourceTree = ""; @@ -1575,7 +1601,6 @@ BFD247962284D7C100981D42 /* Resources */, BF6C8FA8242935CA00125131 /* Dependencies */, BFD247972284D7D800981D42 /* Supporting Files */, - 1920B04E2924AC8300744F60 /* Settings.bundle */, ); path = AltStore; sourceTree = ""; @@ -1659,6 +1684,7 @@ children = ( BFE60737231ADF49002B0E8E /* Settings.storyboard */, BFE60739231ADF82002B0E8E /* SettingsViewController.swift */, + 0EA426392C2230150026D7FB /* AnisetteServerList.swift */, BFE6073F231AFD2A002B0E8E /* InsetGroupTableViewCell.swift */, BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */, BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */, @@ -1775,6 +1801,7 @@ children = ( D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */, D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */, + 0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */, ); path = "Error Log"; sourceTree = ""; @@ -1827,6 +1854,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; @@ -2210,7 +2238,6 @@ BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */, BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */, BFD247772284B9A700981D42 /* Assets.xcassets in Resources */, - 1920B04F2924AC8300744F60 /* Settings.bundle in Resources */, BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */, BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */, BFD247752284B9A500981D42 /* Main.storyboard in Resources */, @@ -2271,7 +2298,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 +2395,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 +2412,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 */, @@ -2393,11 +2420,14 @@ BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */, BFC712C42512D5F100AB5EBE /* XPCConnection.swift in Sources */, D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */, + 0E05025A2BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift in Sources */, BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */, BF66EED42501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel in Sources */, BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */, BFAECC552501B0A400528F27 /* Connection.swift in Sources */, BF66EEDA2501AECA007EE018 /* RefreshAttempt.swift in Sources */, + 0E05025C2BEC947000879B5C /* String+SideStore.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 +2436,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 +2461,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 +2517,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 */, @@ -2508,6 +2539,7 @@ BF41B808233433C100C593A3 /* LoadingState.swift in Sources */, BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */, D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */, + 0EA4263A2C2230150026D7FB /* AnisetteServerList.swift in Sources */, D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */, B39F16132918D7C5002E9404 /* Consts.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, @@ -2533,9 +2565,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/App Detail/AppContentViewController.swift b/AltStore/App Detail/AppContentViewController.swift index 6dc52b87..2aacf37c 100644 --- a/AltStore/App Detail/AppContentViewController.swift +++ b/AltStore/App Detail/AppContentViewController.swift @@ -81,7 +81,7 @@ final class AppContentViewController: UITableViewController self.subtitleLabel.text = self.app.subtitle self.descriptionTextView.text = self.app.localizedDescription - if let version = self.app.latestVersion + if let version = self.app.latestAvailableVersion { self.versionDescriptionTextView.text = version.localizedDescription self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version) diff --git a/AltStore/App Detail/AppViewController.swift b/AltStore/App Detail/AppViewController.swift index ffba2ba1..6ea8a81f 100644 --- a/AltStore/App Detail/AppViewController.swift +++ b/AltStore/App Detail/AppViewController.swift @@ -384,7 +384,7 @@ private extension AppViewController button.progress = progress } - if let versionDate = self.app.latestVersion?.date, versionDate > Date() + if let versionDate = self.app.latestAvailableVersion?.date, versionDate > Date() { self.bannerView.button.countdownDate = versionDate self.navigationBarDownloadButton.countdownDate = versionDate @@ -510,7 +510,7 @@ extension AppViewController catch { DispatchQueue.main.async { - let toastView = ToastView(error: error) + let toastView = ToastView(error: error, opensLog: true) toastView.show(in: self) } } diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 64d08e7c..eeaabaf1 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -382,7 +382,7 @@ private extension AppDelegate for update in updates { guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue } - guard let storeApp = update.storeApp, let version = storeApp.version else { continue } + guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue } let content = UNMutableNotificationContent() content.title = NSLocalizedString("New Update Available", comment: "") diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift index fad4bead..6c38d2f2 100644 --- a/AltStore/Authentication/AuthenticationViewController.swift +++ b/AltStore/Authentication/AuthenticationViewController.swift @@ -108,11 +108,9 @@ 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 toastView.show(in: self) self.toastView = toastView diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index a23fbbe9..1addee55 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -114,9 +114,9 @@ private extension BrowseViewController let progress = AppManager.shared.installationProgress(for: app) cell.bannerView.button.progress = progress - if let versionDate = app.latestVersion?.date, versionDate > Date() + if let versionDate = app.latestSupportedVersion?.date, versionDate > Date() { - cell.bannerView.button.countdownDate = app.versionDate + cell.bannerView.button.countdownDate = versionDate } else { @@ -278,7 +278,7 @@ private extension BrowseViewController { case .failure(OperationError.cancelled): break // Ignore case .failure(let error): - let toastView = ToastView(error: error) + let toastView = ToastView(error: error, opensLog: true) toastView.show(in: self) case .success: print("Installed app:", app.bundleIdentifier) diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift index 6670efbd..4cbe6db2 100644 --- a/AltStore/Components/ToastView.swift +++ b/AltStore/Components/ToastView.swift @@ -18,8 +18,17 @@ extension TimeInterval final class ToastView: RSTToastView { + static let openErrorLogNotification = Notification.Name("ALTOpenErrorLogNotification") + var preferredDuration: TimeInterval - + + var opensErrorLog: Bool = false + + convenience init(text: String, detailText: String?, opensLog: Bool = false) { + self.init(text: text, detailText: detailText) + self.opensErrorLog = opensLog + } + override init(text: String, detailText detailedText: String?) { if detailedText == nil @@ -43,53 +52,43 @@ final class ToastView: RSTToastView // RSTToastView does not expose stack view containing labels, // so we access it indirectly as the labels' superview. stackView.spacing = (detailedText != nil) ? 4.0 : 0.0 + stackView.alignment = .leading } + self.addTarget(self, action: #selector(ToastView.showErrorLog), for: .touchUpInside) } - + + convenience init(error: Error, opensLog: Bool = false) { + self.init(error: error) + self.opensErrorLog = opensLog + } + convenience init(error: Error) { 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) { @@ -112,6 +111,18 @@ final class ToastView: RSTToastView override func show(in view: UIView, duration: TimeInterval) { + if opensErrorLog, #available(iOS 13.0, *), case let configuration = UIImage.SymbolConfiguration(font: self.textLabel.font), + let icon = UIImage(systemName: "chevron.right.circle", withConfiguration: configuration) { + let tintedIcon = icon.withTintColor(.white, renderingMode: .alwaysOriginal) + let moreIconImageView = UIImageView(image: tintedIcon) + moreIconImageView.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(moreIconImageView) + NSLayoutConstraint.activate([ + moreIconImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -self.layoutMargins.right), + moreIconImageView.centerYAnchor.constraint(equalTo: self.textLabel.centerYAnchor), + moreIconImageView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.textLabel.trailingAnchor, multiplier: 1.0) + ]) + } super.show(in: view, duration: duration) let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "") @@ -127,4 +138,10 @@ final class ToastView: RSTToastView { self.show(in: view, duration: self.preferredDuration) } + + @objc + func showErrorLog() { + guard self.opensErrorLog else { return } + NotificationCenter.default.post(name: ToastView.openErrorLogNotification, object: self) + } } diff --git a/AltStore/Intents/IntentHandler.swift b/AltStore/Intents/IntentHandler.swift index cad45e75..641b00b3 100644 --- a/AltStore/Intents/IntentHandler.swift +++ b/AltStore/Intents/IntentHandler.swift @@ -8,6 +8,7 @@ import Foundation +import minimuxer import AltStoreCore @available(iOS 14, *) @@ -39,8 +40,12 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling // Give ourselves 9 extra seconds before starting handle() timeout timer. // 10 seconds or longer results in timeout regardless. - self.queue.asyncAfter(deadline: .now() + 9.0) { - self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil)) + self.queue.asyncAfter(deadline: .now() + 8.0) { + if minimuxer.ready() { + self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) + } else { + self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil)) + } } if !DatabaseManager.shared.isStarted @@ -52,12 +57,14 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling } else { + self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil)) self.refreshApps(intent: intent) } } } else { + self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil)) self.refreshApps(intent: intent) } } @@ -83,6 +90,11 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling // We took too long to finish and return the final result, // so we'll now present a normal notification when finished. operation.presentsFinishedNotification = true + if minimuxer.ready() { + self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) + } else { + self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil)) + } } self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil)) @@ -106,6 +118,8 @@ private extension IntentHandler { // Queue response in case refreshing finishes after confirm() but before handle(). self.queuedResponses[intent] = response + + UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) } } } @@ -126,10 +140,12 @@ private extension IntentHandler } self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) + UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) } - catch RefreshError.noInstalledApps + catch ~RefreshErrorCode.noInstalledApps { self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) + UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) } catch let error as NSError { diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 310bba29..46480934 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -14,6 +14,7 @@ import Intents import Combine import WidgetKit +import minimuxer import AltStoreCore import AltSign import Roxas @@ -37,11 +38,6 @@ final class AppManagerPublisher: ObservableObject fileprivate(set) var refreshProgress = [String: Progress]() } -private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool -{ - return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion) -} - final class AppManager { static let shared = AppManager() @@ -317,6 +313,35 @@ extension AppManager self.run([clearAppCacheOperation], context: nil) } + + func log(_ error: Error, operation: LoggedError.Operation, app: AppProtocol) + { + switch error { + case ~OperationError.Code.cancelled: return // Don't log cancelled events + default: break + } + // Sanitize NSError on same thread before performing background task. + let sanitizedError = (error as NSError).sanitizedForSerialization() + + DatabaseManager.shared.persistentContainer.performBackgroundTask { context in + var app = app + if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol + { + app = tempApp + } + + do + { + _ = LoggedError(error: sanitizedError, app: app, operation: operation, context: context) + try context.save() + } + catch let saveError + { + print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError) + } + } + } + } extension AppManager @@ -369,7 +394,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 +482,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 +501,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 +509,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 +545,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 +584,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 +610,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 +635,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) @@ -680,13 +705,20 @@ extension AppManager var installedApp: InstalledApp? } + let appName = installedApp.name let context = Context() context.installedApp = installedApp let enableJITOperation = EnableJITOperation(context: context) enableJITOperation.resultHandler = { (result) in - completionHandler(result) + switch result { + case .success: completionHandler(.success(())) + case .failure(let nsError as NSError): + let localizedTitle = String(format: NSLocalizedString("Failed to enable JIT for %@", comment: ""), appName) + let error = nsError.withLocalizedTitle(localizedTitle) + self.log(error, operation: .enableJIT, app: installedApp) + } } self.run([enableJITOperation], context: context, requiresSerialQueue: true) @@ -822,6 +854,18 @@ private extension AppManager return bundleIdentifier } + + var loggedErrorOperation: LoggedError.Operation { + switch self { + case .install: return .install + case .update: return .update + case .refresh: return .refresh + case .activate: return .activate + case .deactivate: return .deactivate + case .backup: return .backup + case .restore: return .restore + } + } } @discardableResult @@ -1062,7 +1106,9 @@ private extension AppManager switch result { case .failure(let error): context.error = error - case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles + case .success(let provisioningProfiles): + context.provisioningProfiles = provisioningProfiles + print("PROVISIONING PROFILES \(context.provisioningProfiles)") } } fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation) @@ -1269,14 +1315,21 @@ private extension AppManager case .success(let installedApp): completionHandler(.success(installedApp)) - case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound): + case .failure(MinimuxerError.ProfileInstall): + completionHandler(.failure(OperationError.noWiFi)) + + 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. - let installProgress = self._install(app, operation: operation, group: group) { (result) in - completionHandler(result) + if minimuxer.ready() { + let installProgress = self._install(app, operation: operation, group: group) { (result) in + completionHandler(result) + } + progress.addChild(installProgress, withPendingUnitCount: 40) + } else { + completionHandler(.failure(OperationError.noWiFi)) } - progress.addChild(installProgress, withPendingUnitCount: 40) } case .failure(let error): @@ -1543,7 +1596,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 +1608,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,11 +1747,35 @@ 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) + self.log(error, operation: operation.loggedErrorOperation, app: operation.app) } } @@ -1723,43 +1800,7 @@ private extension AppManager UNUserNotificationCenter.current().add(request) } - func log(_ error: Error, for operation: AppOperation) - { - // Sanitize NSError on same thread before performing background task. - let sanitizedError = (error as NSError).sanitizedForCoreData() - - let loggedErrorOperation: LoggedError.Operation = { - switch operation - { - case .install: return .install - case .update: return .update - case .refresh: return .refresh - case .activate: return .activate - case .deactivate: return .deactivate - case .backup: return .backup - case .restore: return .restore - } - }() - - DatabaseManager.shared.persistentContainer.performBackgroundTask { context in - var app = operation.app - if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol - { - app = tempApp - } - - do - { - _ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context) - try context.save() - } - catch let saveError - { - print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError) - } - } - } - + func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false) { // Find "Install AltStore" operation if it already exists in `context` 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/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 61a12c99..d6a337cb 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -155,6 +155,13 @@ final class MyAppsViewController: UICollectionViewController @IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue) { } + var minimuxerStatus: Bool { + guard minimuxer.ready() else { + ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self) + return false + } + return true + } } private extension MyAppsViewController @@ -188,7 +195,7 @@ private extension MyAppsViewController func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource { let fetchRequest = InstalledApp.updatesFetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true), + fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestSupportedVersion?.date, ascending: false), NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)] fetchRequest.returnsObjectsAsFaults = false @@ -197,21 +204,21 @@ private extension MyAppsViewController dataSource.cellIdentifierHandler = { _ in "UpdateCell" } dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in guard let self = self else { return } - guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return } + guard let app = installedApp.storeApp, let latestSupportedVersion = app.latestSupportedVersion else { return } let cell = cell as! UpdateCollectionViewCell cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.right = self.view.layoutMargins.right cell.tintColor = app.tintColor ?? .altPrimary - cell.versionDescriptionTextView.text = app.versionDescription + cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription cell.bannerView.iconImageView.image = nil cell.bannerView.iconImageView.isIndicatingActivity = true cell.bannerView.configure(for: app) - let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter) + let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter) cell.bannerView.subtitleLabel.text = versionDate let appName: String @@ -225,7 +232,7 @@ private extension MyAppsViewController appName = app.name } - cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate) + cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.version, versionDate) cell.bannerView.button.isIndicatingActivity = false cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) @@ -528,11 +535,9 @@ private extension MyAppsViewController guard !failures.isEmpty else { return } - let toastView: ToastView - if let failure = failures.first, results.count == 1 { - toastView = ToastView(error: failure.value) + ToastView(error: failure.value).show(in: self) } else { @@ -550,11 +555,10 @@ private extension MyAppsViewController let error = failures.first?.value as NSError? let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription - toastView = ToastView(text: localizedText, detailText: detailText) + let toastView = ToastView(text: localizedText, detailText: detailText, opensLog: true) toastView.preferredDuration = 4.0 + toastView.show(in: self) } - - toastView.show(in: self) } self.refreshGroup = nil @@ -645,11 +649,7 @@ private extension MyAppsViewController @IBAction func refreshAllApps(_ sender: UIBarButtonItem) { - if !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) - return - } + guard minimuxerStatus else { return } self.isRefreshingAllApps = true self.collectionView.collectionViewLayout.invalidateLayout() @@ -694,8 +694,7 @@ private extension MyAppsViewController self.collectionView.reloadItems(at: [indexPath]) case .failure(let error): - let toastView = ToastView(error: error) - toastView.show(in: self) + ToastView(error: error, opensLog: true).show(in: self) self.collectionView.reloadItems(at: [indexPath]) @@ -713,11 +712,7 @@ private extension MyAppsViewController @IBAction func sideloadApp(_ sender: UIBarButtonItem) { - if !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) - return - } + guard minimuxerStatus else { return } let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil) @@ -900,9 +895,8 @@ private extension MyAppsViewController completion(.failure((OperationError.cancelled))) case .failure(let error): - let toastView = ToastView(error: error) - toastView.show(in: self) - + ToastView(error: error, opensLog: true).show(in: self) + completion(.failure(error)) } } @@ -1016,18 +1010,13 @@ private extension MyAppsViewController UIApplication.shared.open(installedApp.openAppURL) { success in guard !success else { return } - let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name)) - toastView.show(in: self) + ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self) } } func refresh(_ installedApp: InstalledApp) { - if !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) - return - } + guard minimuxerStatus else { return } let previousProgress = AppManager.shared.refreshProgress(for: installedApp) guard previousProgress == nil else { @@ -1050,11 +1039,7 @@ private extension MyAppsViewController func activate(_ installedApp: InstalledApp) { - if !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) - return - } + guard minimuxerStatus else { return } func finish(_ result: Result) { @@ -1076,8 +1061,7 @@ private extension MyAppsViewController DispatchQueue.main.async { installedApp.isActive = false - let toastView = ToastView(error: error) - toastView.show(in: self) + ToastView(error: error, opensLog: true).show(in: self) } } } @@ -1131,12 +1115,8 @@ private extension MyAppsViewController func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result) -> Void)? = nil) { - guard installedApp.isActive else { return } - if !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) - return - } + guard installedApp.isActive, minimuxerStatus else { return } + installedApp.isActive = false AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in @@ -1149,13 +1129,12 @@ private extension MyAppsViewController } catch { - print("Failed to activate app:", error) + print("Failed to deactivate app:", error) DispatchQueue.main.async { installedApp.isActive = true - let toastView = ToastView(error: error) - toastView.show(in: self) + ToastView(error: error, opensLog: true).show(in: self) } } @@ -1186,8 +1165,7 @@ private extension MyAppsViewController case .success: break case .failure(let error): DispatchQueue.main.async { - let toastView = ToastView(error: error) - toastView.show(in: self) + ToastView(error: error, opensLog: true).show(in: self) } } } @@ -1198,11 +1176,8 @@ private extension MyAppsViewController func backup(_ installedApp: InstalledApp) { - if !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) - return - } + guard minimuxerStatus else { return } + let title = NSLocalizedString("Start Backup?", comment: "") let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "") @@ -1224,9 +1199,8 @@ private extension MyAppsViewController print("Failed to back up app:", error) DispatchQueue.main.async { - let toastView = ToastView(error: error) - toastView.show(in: self) - + ToastView(error: error, opensLog: true).show(in: self) + self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue]) } } @@ -1242,11 +1216,8 @@ private extension MyAppsViewController func restore(_ installedApp: InstalledApp) { - if !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) - return - } + guard minimuxerStatus else { return } + let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name) let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet) alertController.addAction(.cancel) @@ -1264,8 +1235,7 @@ private extension MyAppsViewController print("Failed to restore app:", error) DispatchQueue.main.async { - let toastView = ToastView(error: error) - toastView.show(in: self) + ToastView(error: error, opensLog: true).show(in: self) } } } @@ -1340,8 +1310,7 @@ private extension MyAppsViewController print("Failed to change app icon.", error) DispatchQueue.main.async { - let toastView = ToastView(error: error) - toastView.show(in: self) + ToastView(error: error, opensLog: true).show(in: self) } } } @@ -1350,14 +1319,11 @@ private extension MyAppsViewController @available(iOS 14, *) func enableJIT(for installedApp: InstalledApp) { - if #available(iOS 17, *), !UserDefaults.standard.sidejitenable { - let toastView = ToastView(error: OperationError.tooNewError) - toastView.show(in: self) - return - } - if #unavailable(iOS 17), !minimuxer.ready() { - let toastView = ToastView(error: MinimuxerError.NoConnection) - toastView.show(in: self) + guard minimuxerStatus else { return } + + if #available(iOS 17, *) { + ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self) + AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp) return } @@ -1367,8 +1333,8 @@ private extension MyAppsViewController { case .success: break case .failure(let error): - let toastView = ToastView(error: error) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 5) + ToastView(error: error, opensLog: true).show(in: self) + AppManager.shared.log(error, operation: .enableJIT, app: installedApp) } } } diff --git a/AltStore/News/NewsViewController.swift b/AltStore/News/NewsViewController.swift index 945a0822..c6b55cb6 100644 --- a/AltStore/News/NewsViewController.swift +++ b/AltStore/News/NewsViewController.swift @@ -313,9 +313,8 @@ private extension NewsViewController { case .failure(OperationError.cancelled): break // Ignore case .failure(let error): - let toastView = ToastView(error: error) - toastView.show(in: self) - + ToastView(error: error, opensLog: true).show(in: self) + case .success: print("Installed app:", storeApp.bundleIdentifier) } @@ -391,9 +390,9 @@ extension NewsViewController let progress = AppManager.shared.installationProgress(for: storeApp) footerView.bannerView.button.progress = progress - if let versionDate = storeApp.latestVersion?.date, versionDate > Date() + if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date() { - footerView.bannerView.button.countdownDate = storeApp.versionDate + footerView.bannerView.button.countdownDate = versionDate } else { diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index 6cecb5b0..d305808d 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))) } } } @@ -489,20 +490,20 @@ private extension AuthenticationOperation { func requestCertificate() { - let machineName = "AltStore - " + UIDevice.current.name + let machineName = "SideStore - " + UIDevice.current.name ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in 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: "SideStore") == true }) ?? certificates.first else { return completionHandler(.failure(OperationError.notAuthenticated)) } 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..d4e975c5 100644 --- a/AltStore/Operations/DownloadAppOperation.swift +++ b/AltStore/Operations/DownloadAppOperation.swift @@ -12,66 +12,108 @@ 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 - + private let session = URLSession(configuration: .default) private let temporaryDirectory = FileManager.default.uniqueTemporaryURL() - + init(app: AppProtocol, destinationURL: URL, context: AppOperationContext) { self.app = app self.context = context - + + self.appName = app.name self.bundleIdentifier = app.bundleIdentifier - self.sourceURL = app.url self.destinationURL = destinationURL - + super.init() - + // App = 3, Dependencies = 1 self.progress.totalUnitCount = 4 } - + override func main() { super.main() - + if let error = self.context.error { self.finish(.failure(error)) return } - + print("Downloading App:", self.bundleIdentifier) - - guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) } - - self.downloadApp(from: sourceURL) { result in + + self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName) + + guard let storeApp = self.app as? StoreApp else { return self.download(self.app) } + storeApp.managedObjectContext?.perform { + do { + let latestVersion = try self.verify(storeApp) + self.download(latestVersion) + } catch let error as VerificationError where error.code == .iOSVersionNotSupported { + guard let presentingViewController = self.context.presentingViewController, + let latestSupportedVersion = storeApp.latestSupportedVersion, + case let version = latestSupportedVersion.version, + version != storeApp.installedApp?.version else { + return self.finish(.failure(error)) + } + let title = NSLocalizedString("Unsupported iOS Version", comment: "") + let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "") + + DispatchQueue.main.async { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in + self.finish(.failure(OperationError.cancelled)) + }) + alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in + self.download(latestSupportedVersion) + }) + presentingViewController.present(alertController, animated: true) + } + } catch { + self.finish(.failure(error)) + } + } + } + + override func finish(_ result: Result) { + do { + try FileManager.default.removeItem(at: self.temporaryDirectory) + } catch { + print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error) + } + super.finish(result) + } +} + +private extension DownloadAppOperation { + func verify(_ storeApp: StoreApp) throws -> AppVersion { + guard let version = storeApp.latestAvailableVersion else { + let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName) + throw OperationError.unknown(failureReason: failureReason) + } + if let minOSVersion = version.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) { + throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: minOSVersion) + } else if let maxOSVersion = version.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion { + throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: maxOSVersion) + } + + return version + } + + func download(@Managed _ app: AppProtocol) { + guard let sourceURL = $app.url else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) } + + self.downloadIPA(from: sourceURL) { result in do { let application = try result.get() @@ -112,24 +154,7 @@ final class DownloadAppOperation: ResultOperation } } - override func finish(_ result: Result) - { - do - { - try FileManager.default.removeItem(at: self.temporaryDirectory) - } - catch - { - print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error) - } - - super.finish(result) - } -} - -private extension DownloadAppOperation -{ - func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result) -> Void) + func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result) -> Void) { func finishOperation(_ result: Result) { @@ -138,8 +163,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 @@ -178,6 +203,9 @@ private extension DownloadAppOperation let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in do { + if let response = response as? HTTPURLResponse { + guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) } + } let (fileURL, _) = try Result((fileURL, response), error).get() finishOperation(.success(fileURL)) @@ -252,7 +280,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 +313,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 +322,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 +343,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/EnableJITOperation.swift b/AltStore/Operations/EnableJITOperation.swift index e8a4538e..fd2cd3cb 100644 --- a/AltStore/Operations/EnableJITOperation.swift +++ b/AltStore/Operations/EnableJITOperation.swift @@ -61,12 +61,10 @@ final class EnableJITOperation: ResultOperation switch result { case .failure(let error): switch error { - case .invalidURL: - self.finish(.failure(OperationError.unabletoconnectSideJIT)) - case .errorConnecting: - self.finish(.failure(OperationError.unabletoconnectSideJIT)) + case .invalidURL, .errorConnecting: + self.finish(.failure(OperationError.unableToConnectSideJIT)) case .deviceNotFound: - self.finish(.failure(OperationError.unabletoconSideJITDevice)) + self.finish(.failure(OperationError.unableToRespondSideJITDevice)) case .other(let message): if let startRange = message.range(of: "

"), let endRange = message.range(of: "

", range: startRange.upperBound.., WebSoc return } - self.url = AnisetteManager.currentURL + self.url = URL(string: UserDefaults.standard.menuAnisetteURL) print("Anisette URL: \(self.url!.absoluteString)") if let identifier = Keychain.shared.identifier, @@ -408,6 +408,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc func fetchAnisetteV3(_ identifier: String, _ adiPb: String) { fetchClientInfo { print("Fetching anisette V3") + let url = UserDefaults.standard.menuAnisetteURL var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers")) request.httpMethod = "POST" request.httpBody = try! JSONSerialization.data(withJSONObject: [ 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/Operation.swift b/AltStore/Operations/Operation.swift index baba0384..b0caaea1 100644 --- a/AltStore/Operations/Operation.swift +++ b/AltStore/Operations/Operation.swift @@ -12,7 +12,10 @@ import Roxas class ResultOperation: Operation { var resultHandler: ((Result) -> Void)? - + + // Should only be set by subclasses + var localizedFailure: String? + @available(*, unavailable) override func finish() { @@ -22,16 +25,20 @@ class ResultOperation: Operation func finish(_ result: Result) { guard !self.isFinished else { return } - + + var result = result + if self.isCancelled { - self.resultHandler?(.failure(OperationError.cancelled)) + result = .failure(OperationError.cancelled) } - else - { - self.resultHandler?(result) + else if case .failure(let nsError as NSError) = result, let localizedFailure, nsError.localizedFailure == nil { + // Error doesn't have its own localizedFailure, so we give it the Operation's (if it exists) + let error = nsError.withLocalizedFailure(localizedFailure) + result = .failure(error) } - + self.resultHandler?(result) + super.finish() } } diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index d05ea749..d652fa24 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -8,81 +8,186 @@ import Foundation import AltSign +import AltStoreCore import minimuxer -enum OperationError: LocalizedError +extension OperationError { - static let domain = OperationError.unknown._domain + enum Code: Int, ALTErrorCode, CaseIterable { + typealias Error = OperationError + + // General + case unknown = 1000 + case unknownResult + case cancelled + case timedOut + case unableToConnectSideJIT + case unableToRespondSideJITDevice + case wrongSideJITIP + 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 unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT) + static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice) + static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP) + 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: .noWiFi) + 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) + } - case unknown - case unabletoconnectSideJIT - case unabletoconSideJITDevice - case wrongIP - case SideJITIssue(error: String) - case refreshsidejit - case unknownResult - case cancelled - case timedOut + static func SideJITIssue(error: String?) -> OperationError { + var o = OperationError(code: .SideJITIssue) + o.errorFailure = error + return o + } - 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: "") + 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 .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 .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 and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", 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 .unableToRespondSideJITDevice: 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 .wrongSideJITIP: 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 .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: "") + case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", 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 +199,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..5fc94972 100644 --- a/AltStore/Operations/VerifyAppOperation.swift +++ b/AltStore/Operations/VerifyAppOperation.swift @@ -8,48 +8,87 @@ 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) + enum Code: Int, ALTErrorCode, CaseIterable { + typealias Error = VerificationError + + case privateEntitlements + case mismatchedBundleIdentifiers + case iOSVersionNotSupported + } + + static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError { + VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements) + } + + static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError { + VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID) + } + + static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError { + VerificationError(code: .iOSVersionNotSupported, app: app) + } +} + +struct VerificationError: ALTLocalizedError { + let code: Code + + var errorTitle: String? + var errorFailure: String? + + @Managed var app: AppProtocol? + var entitlements: [String: Any]? + var sourceBundleID: String? + var deviceOSVersion: OperatingSystemVersion? + var requiredOSVersion: OperatingSystemVersion? - var app: ALTApplication { - switch self - { - case .privateEntitlements(let app, _): return app - case .mismatchedBundleIdentifiers(let app, _): return app - case .iOSVersionNotSupported(let app): return app + var errorDescription: String? { + switch self.code { + case .iOSVersionNotSupported: + guard let deviceOSVersion else { return nil } + + var failureReason = self.errorFailureReason + if self.app == nil { + let firstLetter = failureReason.prefix(1).lowercased() + failureReason = firstLetter + failureReason.dropFirst() + } + + return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason) + default: return nil } } - - var failure: String? { - return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name) - } - - var failureReason: String? { - switch self + + 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 ?? NSLocalizedString("The app", comment: "") + return String(formatted: "“%@” requires private permissions.", appName) + + case .mismatchedBundleIdentifiers: + if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID { + return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID) + } else { + return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "") + } + + case .iOSVersionNotSupported: + let appName = self.$app.name ?? NSLocalizedString("The app", comment: "") + let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion + + guard let requiredOSVersion else { + return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue) + } + if deviceOSVersion > requiredOSVersion { + return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue) + } else { + return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue) } - - let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version) - return localizedDescription } } } @@ -80,12 +119,14 @@ 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) + if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) { + guard app.bundleIdentifier == self.context.bundleIdentifier else { + 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, requiredOSVersion: app.minimumiOSVersion) } if #available(iOS 13.5, *) @@ -116,7 +157,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 +186,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: @@ -166,8 +208,7 @@ private extension VerifyAppOperation })) presentingViewController.present(alertController, animated: true, completion: nil) - case .mismatchedBundleIdentifiers: return completion(.failure(error)) - case .iOSVersionNotSupported: return completion(.failure(error)) + case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error)) } } } diff --git a/AltStore/Settings.bundle/Root.plist b/AltStore/Settings.bundle/Root.plist deleted file mode 100644 index 026148e0..00000000 --- a/AltStore/Settings.bundle/Root.plist +++ /dev/null @@ -1,111 +0,0 @@ - - - - - StringsTable - Root - ApplicationGroupContainerIdentifier - group.$(APP_GROUP_IDENTIFIER) - PreferenceSpecifiers - - - Type - PSMultiValueSpecifier - Title - Anisette Server - Key - customAnisetteURL - DefaultValue - https://ani.sidestore.io - Titles - - SideStore - SideStore (.zip) - SideStore (.xyz) - Macley (US) - Jawshoeadan - WesleyBryie - Stossy11 - - Values - - https://ani.sidestore.io - https://ani.sidestore.zip - https://ani.846969.xyz - http://5.249.163.88:6969/ - https://anisette.jawshoeadan.me - https://ani.wesbryie.com - https://anisette.stossy11.com - - - - Type - PSGroupSpecifier - Title - Danger Zone - FooterText - If you disable the toggle then app will use the server you input into the "Anisette URL" box rather than one selected from the above selector. - - - Type - PSToggleSwitchSpecifier - Title - Use preferred servers - Key - textServer - DefaultValue - - FooterText - chicken - - - Type - PSTextFieldSpecifier - Title - Anisette URL - Key - textInputAnisetteURL - AutocapitalizationType - None - AutocorrectionType - No - KeyboardType - URL - - - Type - PSGroupSpecifier - Title - SideJITServer - FooterText - If you disable the toggle then the app will not be able to access and use SideJITServer on iOS 17+. "SideJITServer IP" is not needed but is recommended for stability. - - - Type - PSToggleSwitchSpecifier - Title - Enable SideJIT Support - Key - sidejitenable - DefaultValue - - FooterText - chicken - - - Type - PSTextFieldSpecifier - Title - SideJITServer IP - Key - textInputSideJITServerurl - AutocapitalizationType - None - AutocorrectionType - No - KeyboardType - URL - - - - diff --git a/AltStore/Settings.bundle/en.lproj/Root.strings b/AltStore/Settings.bundle/en.lproj/Root.strings deleted file mode 100644 index 8cd87b9d..00000000 Binary files a/AltStore/Settings.bundle/en.lproj/Root.strings and /dev/null differ diff --git a/AltStore/Settings/AnisetteManager.swift b/AltStore/Settings/AnisetteManager.swift index 4edc4617..fa269825 100644 --- a/AltStore/Settings/AnisetteManager.swift +++ b/AltStore/Settings/AnisetteManager.swift @@ -10,6 +10,11 @@ import Foundation public struct AnisetteManager { + var menuURL: String { + var url: String + url = UserDefaults.standard.menuAnisetteURL + return url + } /// User defined URL from Settings/UserDefaults static var userURL: String? { var urlString: String? diff --git a/AltStore/Settings/AnisetteServerList.swift b/AltStore/Settings/AnisetteServerList.swift new file mode 100644 index 00000000..c53a1094 --- /dev/null +++ b/AltStore/Settings/AnisetteServerList.swift @@ -0,0 +1,179 @@ +// +// AnisetteServerList.swift +// SideStore +// +// Created by ny on 6/18/24. +// Copyright © 2024 SideStore. All rights reserved. +// + +import UIKit +import SwiftUI +import AltStoreCore + +typealias SUIButton = SwiftUI.Button + +// MARK: - AnisetteServerData +struct AnisetteServerData: Codable { + let servers: [Server] +} + +// MARK: - Server +struct Server: Codable { + var name: String + var address: String +} + +struct AniServer: Codable { + var name: String + var url: URL +} + +class AnisetteViewModel: ObservableObject { + @Published var selected: String = "" + + @Published var source: String = "https://servers.sidestore.io/servers.json" + @Published var servers: [Server] = [] + + func getListOfServers() { + URLSession.shared.dataTask(with: URL(string: source)!) { data, response, error in + if let error = error { + return + } + if let data = data { + do { + let servers = try Foundation.JSONDecoder().decode(AnisetteServerData.self, from: data) + DispatchQueue.main.async { + self.servers = servers.servers.map { Server(name: $0.name, address: $0.address) } + } + } catch { + + } + } + } + .resume() + for server in servers { + print(server) + print(server.name.count) + print(server.name) + } + } + +} + +struct AnisetteServers: View { + @Environment(\.presentationMode) var presentationMode + @StateObject var viewModel: AnisetteViewModel = AnisetteViewModel() + @State var selected: String? = nil + var errorCallback: () -> () + + var body: some View { + NavigationView { + ZStack { + Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all) + .onAppear { + viewModel.getListOfServers() + } + VStack { + if #available(iOS 16.0, *) { + SwiftUI.List($viewModel.servers, id: \.address, selection: $selected) { server in + HStack { + VStack(alignment: .leading) { + Text("\(server.name.wrappedValue)") + .font(.headline) + .underline(true, color: .white) + Text("\(server.address.wrappedValue)") + .fontWeight(.thin) + } + if selected != nil { + if server.address.wrappedValue == selected { + Spacer() + Image(systemName: "checkmark") + .onAppear { + UserDefaults.standard.menuAnisetteURL = server.address.wrappedValue + print(UserDefaults.synchronize(.standard)()) + print(UserDefaults.standard.menuAnisetteURL) + print(server.address.wrappedValue) + } + } + } + } + .backgroundStyle((selected == nil) ? Color(UIColor(named: "SettingsHighlighted")!) : Color(UIColor(named: "SettingsBackground")!)) + .listRowSeparatorTint(.white) + .listRowBackground((selected == nil) ? Color(UIColor(named: "SettingsHighlighted")!).ignoresSafeArea(.all) : Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all)) + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + .listRowBackground(Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all)) + + } else { + List(selection: $selected) { + ForEach($viewModel.servers, id: \.name) { server in + VStack { + HStack { + Text("\(server.name.wrappedValue)") + .foregroundColor(.white) + .frame(alignment: .center) + Text("\(server.address.wrappedValue)") + .foregroundColor(.white) + .frame(alignment: .center) + } + } + Spacer() + } + } + .listStyle(.plain) + // Fallback on earlier versions + } + if #available(iOS 15.0, *) { + TextField("Anisette Server List", text: $viewModel.source) + .padding(.leading, 5) + .padding(.vertical, 10) + .frame(alignment: .center) + .textFieldStyle(.plain) + .border(.white, width: 1) + .onSubmit { + UserDefaults.standard.menuAnisetteList = viewModel.source + viewModel.getListOfServers() + } + SUIButton(action: { + viewModel.getListOfServers() + }, label: { + Text("Refresh Servers") + }) + .padding(.bottom, 20) + SUIButton(role: .destructive, action: { +#if !DEBUG + if Keychain.shared.adiPb != nil { + Keychain.shared.adiPb = nil + } +#endif + print("Cleared adi.pb from keychain") + errorCallback() + self.presentationMode.wrappedValue.dismiss() + }, label: { + Text("Reset adi.pb") +// if (selected != nil) { +// Text("\(selected!.uuidString)") +// } + }) + .padding(.bottom, 20) + } else { + // Fallback on earlier versions + } + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .navigationTitle("Anisette Servers") + .onAppear { + if UserDefaults.standard.menuAnisetteList != "" { + viewModel.source = UserDefaults.standard.menuAnisetteList + } else { + viewModel.source = "https://servers.sidestore.io/servers.json" + } + print(UserDefaults.standard.menuAnisetteURL) + print(UserDefaults.standard.menuAnisetteList) + } + } +} + 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/ErrorLogTableViewCell.swift b/AltStore/Settings/Error Log/ErrorLogTableViewCell.swift index 71b67b88..0e4f817c 100644 --- a/AltStore/Settings/Error Log/ErrorLogTableViewCell.swift +++ b/AltStore/Settings/Error Log/ErrorLogTableViewCell.swift @@ -8,6 +8,16 @@ import UIKit +@objc(ErrorLogMenuButton) +private final class ErrorLogMenuButton: UIButton { + @available(iOS 14.0, *) + override func menuAttachmentPoint(for configuration: UIContextMenuConfiguration) -> CGPoint { + var point = super.menuAttachmentPoint(for: configuration) + point.y = self.bounds.midY + return point + } +} + @objc(ErrorLogTableViewCell) final class ErrorLogTableViewCell: UITableViewCell { diff --git a/AltStore/Settings/Error Log/ErrorLogViewController.swift b/AltStore/Settings/Error Log/ErrorLogViewController.swift index 7a67a301..d58fd64c 100644 --- a/AltStore/Settings/Error Log/ErrorLogViewController.swift +++ b/AltStore/Settings/Error Log/ErrorLogViewController.swift @@ -21,6 +21,13 @@ final class ErrorLogViewController: UITableViewController private lazy var dataSource = self.makeDataSource() private var expandedErrorIDs = Set() + private var isScrolling = false { + didSet { + guard self.isScrolling != oldValue else { return } + self.updateButtonInteractivity() + } + } + private lazy var timeFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .none @@ -39,6 +46,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 +76,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,12 +103,19 @@ 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 + } ]) cell.menuButton.menu = menu + cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true + cell.selectionStyle = .none + } else { + cell.menuButton.isUserInteractionEnabled = false } - + // Include errorDescriptionTextView's text in cell summary. cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ") @@ -232,22 +249,27 @@ private extension ErrorLogViewController func searchFAQ(for loggedError: LoggedError) { - let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")! + let baseURL = URL(string: "https://faq.altstore.io/getting-started/error-codes")! 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 { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard #unavailable(iOS 14) else { return } let loggedError = self.dataSource.item(at: indexPath) let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) @@ -321,3 +343,32 @@ extension ErrorLogViewController: QLPreviewControllerDataSource { return fileURL as QLPreviewItem } } + +extension ErrorLogViewController +{ + override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) + { + self.isScrolling = true + } + + override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) + { + self.isScrolling = false + } + + override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) + { + guard !decelerate else { return } + self.isScrolling = false + } + + private func updateButtonInteractivity() + { + guard #available(iOS 14, *) else { return } + + for case let cell as ErrorLogTableViewCell in self.tableView.visibleCells + { + cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true + } + } +} diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 02c8f716..db0db89b 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -3,7 +3,7 @@ - + @@ -20,8 +20,8 @@ -