Compare commits

...

33 Commits

Author SHA1 Message Date
ny
0b0c87d564 If it's gonna be that way... 2024-08-05 21:31:54 -04:00
ny
4a00955461 Add availability check to fix CI build(?) 2024-08-05 21:25:00 -04:00
ny
86f804391a Fix build issues given by develop 2024-08-05 21:15:42 -04:00
June Park
be75d8b4d9 Update AuthenticationOperation.swift
Signed-off-by: June Park <rjp2030@outlook.com>
2024-08-05 20:50:38 -04:00
ny
864e03cd4a Remove Settings bundle, add SwiftUI view instead
Fix refresh all shortcut intent
2024-08-05 20:50:36 -04:00
nythepegasus
be5e84537a Removes unnecessary StoreApp convenience properties as well as fix other issues 2024-08-05 20:49:34 -04:00
nythepegasus
1ae8d336be Removes unnecessary StoreApp convenience properties 2024-08-05 20:49:34 -04:00
nythepegasus
989b8e0010 Improves error message when file does not exist at AppVersion.downloadURL 2024-08-05 20:49:34 -04:00
nythepegasus
983355d356 Verifies min/max OS version before downloading app + asks user to download older app version if necessary 2024-08-05 20:49:34 -04:00
nythepegasus
a6ff416067 Conforms AppVersion to AppProtocol 2024-08-05 20:49:34 -04:00
nythepegasus
bbf712a822 Supports optional @Managed properties 2024-08-05 20:49:34 -04:00
nythepegasus
dd0436511a Supports non-NSManagedObjects for @Managed properties
This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.
2024-08-05 20:49:34 -04:00
nythepegasus
4667841c8d Parses AppVersion.minOSVersion/maxOSVersion from source JSON 2024-08-05 20:49:34 -04:00
nythepegasus
d2f0409452 Conforms OperatingSystemVersion to Comparable 2024-08-05 20:49:34 -04:00
Riley Testut
d718d2155f Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion
We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.

However, we kept the underlying Core Data property name the same to avoid extra migration.
2024-08-05 20:49:34 -04:00
nythepegasus
d0b424f408 Skips logging cancelled errors 2024-08-05 20:49:34 -04:00
nythepegasus
3e6ca7c673 Limits quitting other AltStore/SideStore processes to database migrations 2024-08-05 20:49:33 -04:00
nythepegasus
31a3323ef2 Fixes logging non-AltServerErrors as AltServerError.underlyingError 2024-08-05 20:49:33 -04:00
nythepegasus
7edb5716b7 Fix typo 2024-08-05 20:49:33 -04:00
nythepegasus
78bf461c5b Fix wrong color in AuthenticationViewController 2024-08-05 20:49:33 -04:00
nythepegasus
8cfe8a1ac0 Fix Error Log not showing UIAlertController on iOS <=13 2024-08-05 20:49:33 -04:00
nythepegasus
9b7c0387cf Fix Error Log showing UIAlertController on iOS 14+ 2024-08-05 20:49:33 -04:00
nythepegasus
1a36726361 Fixes incorrect Search FAQ URL 2024-08-05 20:49:33 -04:00
nythepegasus
e2a5e263f4 Fixes Error Log context menu appearing while scrolling 2024-08-05 20:49:33 -04:00
nythepegasus
16d13c430c Fixes Error Log context menu covering cell content 2024-08-05 20:49:33 -04:00
nythepegasus
b024e67fee Opens Error Log when tapping ToastView 2024-08-05 20:49:33 -04:00
nythepegasus
4365ba0f1a [skip ci] Update the no wifi message to include VPN 2024-08-05 20:49:31 -04:00
nythepegasus
36ceec3ae7 Fix minimuxer status checking 2024-08-05 20:48:16 -04:00
nythepegasus
99652aae65 Include "Enable JIT" errors in Error Log 2024-08-05 20:47:02 -04:00
nythepegasus
6c8a400aec Fix format strings I broke 2024-08-05 20:47:00 -04:00
nythepegasus
c24de874e6 Finish Riley's monster commit
3b38d725d7
May the Gods have mercy on my soul.
2024-08-05 20:46:08 -04:00
nythepegasus
775167415a Adds ResultOperation.localizedFailure 2024-08-05 20:32:13 -04:00
nythepegasus
459e378522 Change error from Swift.Error to NSError 2024-08-05 20:32:13 -04:00
60 changed files with 2170 additions and 1067 deletions

View File

@@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; }; 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 */; }; 0E1A1F912AE36A9700364CAD /* bytearray.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E1A1F902AE36A9600364CAD /* bytearray.c */; };
0E764E172ADFF5740043DD4E /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = 0E764E162ADFF5740043DD4E /* AltBackup.ipa */; }; 0E764E172ADFF5740043DD4E /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = 0E764E162ADFF5740043DD4E /* AltBackup.ipa */; };
0EA1665B2ADFE0D2003015C1 /* out-limd.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166472ADFE0D1003015C1 /* out-limd.c */; }; 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 */; }; 0EA1667B2ADFE140003015C1 /* Structure.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 0EA1665A2ADFE0D2003015C1 /* Structure.cpp */; };
0EA1667D2ADFE140003015C1 /* xplist.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166592ADFE0D2003015C1 /* xplist.c */; }; 0EA1667D2ADFE140003015C1 /* xplist.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166592ADFE0D2003015C1 /* xplist.c */; };
0EA1667E2ADFE140003015C1 /* time64.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA1664C2ADFE0D1003015C1 /* time64.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 */; }; 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 */; }; 19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; };
19104DB52909C06D00C49C7B /* EmotionalDamage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19104DB42909C06D00C49C7B /* EmotionalDamage.swift */; }; 19104DB52909C06D00C49C7B /* EmotionalDamage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19104DB42909C06D00C49C7B /* EmotionalDamage.swift */; };
19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; }; 19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; }; 191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
191E5FDC290AFA5C001A3B7C /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 191E5FDB290AFA5C001A3B7C /* OpenSSL */; }; 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 */; }; 19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */; };
4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; }; 4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; };
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; };
@@ -162,7 +171,6 @@
BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; }; BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; };
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; }; BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; };
BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF580497246A3D19008AE704 /* UIKit.framework */; }; 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 */; }; 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, ); }; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; };
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; }; BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; };
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAF2429599900125131 /* TextCollectionReusableView.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 */; }; BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; };
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; }; BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; };
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.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 */; }; BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; }; BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; };
BFAECC552501B0A400528F27 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF624858BDE00DD5981 /* Connection.swift */; }; 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 */; }; BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */; };
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; }; BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; }; 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 */; }; BFECAC8924FD950E0077C41F /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF22485828200DD5981 /* ConnectionManager.swift */; };
BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.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 */; }; BFECAC8B24FD950E0077C41F /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
@@ -499,6 +506,8 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
0E05025B2BEC947000879B5C /* String+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SideStore.swift"; sourceTree = "<group>"; };
0E1A1F902AE36A9600364CAD /* bytearray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = bytearray.c; path = src/bytearray.c; sourceTree = "<group>"; }; 0E1A1F902AE36A9600364CAD /* bytearray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = bytearray.c; path = src/bytearray.c; sourceTree = "<group>"; };
0E764E162ADFF5740043DD4E /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; name = AltBackup.ipa; path = AltStore/Resources/AltBackup.ipa; sourceTree = SOURCE_ROOT; }; 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; }; 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; }; 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; }; 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; }; 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 = "<group>"; };
0EA4B9BB2AE4A3F6009209CE /* plist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = plist.c; path = Dependencies/libplist/src/plist.c; sourceTree = SOURCE_ROOT; }; 0EA4B9BB2AE4A3F6009209CE /* plist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = plist.c; path = Dependencies/libplist/src/plist.c; sourceTree = SOURCE_ROOT; };
0EE7FDC02BE8BC2100D1E390 /* ALTWrappedError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWrappedError.m; sourceTree = "<group>"; };
0EE7FDC22BE8BC4200D1E390 /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = "<group>"; };
0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; };
19104DB22909C06C00C49C7B /* libEmotionalDamage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libEmotionalDamage.a; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 19104DB42909C06D00C49C7B /* EmotionalDamage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmotionalDamage.swift; sourceTree = "<group>"; };
191E5FAB290A5D92001A3B7C /* libminimuxer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libminimuxer.a; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; };
19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; }; 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "minimuxer-helpers.swift"; path = "Dependencies/minimuxer/minimuxer-helpers.swift"; sourceTree = SOURCE_ROOT; }; 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; }; 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 = "<group>"; }; BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = "<group>"; };
BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; }; BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; }; BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; };
BFD44605241188C300EAB90A /* CodableServerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableServerError.swift; sourceTree = "<group>"; }; BFD44605241188C300EAB90A /* CodableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableError.swift; sourceTree = "<group>"; };
BFD52BD222A06EFB000B7ED1 /* ALTConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTConstants.h; sourceTree = "<group>"; }; BFD52BD222A06EFB000B7ED1 /* ALTConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTConstants.h; sourceTree = "<group>"; };
BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; }; 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; }; 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 */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
0EE7FDBF2BE8BBBF00D1E390 /* Errors */ = {
isa = PBXGroup;
children = (
0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */,
0EE7FDC02BE8BC2100D1E390 /* ALTWrappedError.m */,
0EE7FDC22BE8BC4200D1E390 /* ALTWrappedError.h */,
);
path = Errors;
sourceTree = "<group>";
};
19104DB32909C06D00C49C7B /* EmotionalDamage */ = { 19104DB32909C06D00C49C7B /* EmotionalDamage */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1061,7 +1084,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF1E3128229F474900370A3C /* ServerProtocol.swift */, BF1E3128229F474900370A3C /* ServerProtocol.swift */,
BFD44605241188C300EAB90A /* CodableServerError.swift */, BFD44605241188C300EAB90A /* CodableError.swift */,
); );
path = "Server Protocol"; path = "Server Protocol";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1074,6 +1097,7 @@
BF18BFFF2485A75F00DD5981 /* Server Protocol */, BF18BFFF2485A75F00DD5981 /* Server Protocol */,
BFF767CF2489AC240097E58C /* Connections */, BFF767CF2489AC240097E58C /* Connections */,
BFF7C92D2578464D00E55F36 /* XPC */, BFF7C92D2578464D00E55F36 /* XPC */,
0EE7FDBF2BE8BBBF00D1E390 /* Errors */,
BFF767C32489A6800097E58C /* Extensions */, BFF767C32489A6800097E58C /* Extensions */,
BFF767C42489A6980097E58C /* Categories */, BFF767C42489A6980097E58C /* Categories */,
); );
@@ -1396,6 +1420,8 @@
BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */, BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */,
BF66EEE42501AED0007EE018 /* UserDefaults+AltStore.swift */, BF66EEE42501AED0007EE018 /* UserDefaults+AltStore.swift */,
BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */, BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */,
0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */,
0E05025B2BEC947000879B5C /* String+SideStore.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1575,7 +1601,6 @@
BFD247962284D7C100981D42 /* Resources */, BFD247962284D7C100981D42 /* Resources */,
BF6C8FA8242935CA00125131 /* Dependencies */, BF6C8FA8242935CA00125131 /* Dependencies */,
BFD247972284D7D800981D42 /* Supporting Files */, BFD247972284D7D800981D42 /* Supporting Files */,
1920B04E2924AC8300744F60 /* Settings.bundle */,
); );
path = AltStore; path = AltStore;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1659,6 +1684,7 @@
children = ( children = (
BFE60737231ADF49002B0E8E /* Settings.storyboard */, BFE60737231ADF49002B0E8E /* Settings.storyboard */,
BFE60739231ADF82002B0E8E /* SettingsViewController.swift */, BFE60739231ADF82002B0E8E /* SettingsViewController.swift */,
0EA426392C2230150026D7FB /* AnisetteServerList.swift */,
BFE6073F231AFD2A002B0E8E /* InsetGroupTableViewCell.swift */, BFE6073F231AFD2A002B0E8E /* InsetGroupTableViewCell.swift */,
BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */, BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */,
BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */, BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */,
@@ -1775,6 +1801,7 @@
children = ( children = (
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */, D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */, D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */,
); );
path = "Error Log"; path = "Error Log";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1827,6 +1854,7 @@
BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */, BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */,
BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */, BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */,
BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */, BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */,
0EE7FDC82BE8CF4800D1E390 /* ALTWrappedError.h in Headers */,
BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */, BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -2210,7 +2238,6 @@
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */, BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */, BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */, BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
1920B04F2924AC8300744F60 /* Settings.bundle in Resources */,
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */, BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */,
BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */, BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */,
BFD247752284B9A500981D42 /* Main.storyboard in Resources */, BFD247752284B9A500981D42 /* Main.storyboard in Resources */,
@@ -2271,7 +2298,7 @@
BF1FE358251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, BF1FE358251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
BFECAC8F24FD950E0077C41F /* Result+Conveniences.swift in Sources */, BFECAC8F24FD950E0077C41F /* Result+Conveniences.swift in Sources */,
BF8CAE472489E772004D6CCE /* DaemonRequestHandler.swift in Sources */, BF8CAE472489E772004D6CCE /* DaemonRequestHandler.swift in Sources */,
BFECAC8824FD950E0077C41F /* CodableServerError.swift in Sources */, BFECAC8824FD950E0077C41F /* CodableError.swift in Sources */,
BFC712C32512D5F100AB5EBE /* XPCConnection.swift in Sources */, BFC712C32512D5F100AB5EBE /* XPCConnection.swift in Sources */,
BFC712C52512D5F100AB5EBE /* XPCConnectionHandler.swift in Sources */, BFC712C52512D5F100AB5EBE /* XPCConnectionHandler.swift in Sources */,
BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */, BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */,
@@ -2368,8 +2395,8 @@
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */, BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */,
BF580482246A28F7008AE704 /* ViewController.swift in Sources */, BF580482246A28F7008AE704 /* ViewController.swift in Sources */,
BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */, BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */,
0EE7FDC62BE8CEA300D1E390 /* ALTLocalizedError.swift in Sources */,
03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */, 03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */,
BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */,
BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */, BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -2385,7 +2412,7 @@
BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */, BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */,
BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */, BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */,
BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */, BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */,
BFAECC522501B0A400528F27 /* CodableServerError.swift in Sources */, BFAECC522501B0A400528F27 /* CodableError.swift in Sources */,
BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */, BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */,
BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */, BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */,
BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */, BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */,
@@ -2393,11 +2420,14 @@
BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */, BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */,
BFC712C42512D5F100AB5EBE /* XPCConnection.swift in Sources */, BFC712C42512D5F100AB5EBE /* XPCConnection.swift in Sources */,
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */, D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */,
0E05025A2BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift in Sources */,
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */, BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */,
BF66EED42501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel in Sources */, BF66EED42501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel in Sources */,
BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */, BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */,
BFAECC552501B0A400528F27 /* Connection.swift in Sources */, BFAECC552501B0A400528F27 /* Connection.swift in Sources */,
BF66EEDA2501AECA007EE018 /* RefreshAttempt.swift in Sources */, BF66EEDA2501AECA007EE018 /* RefreshAttempt.swift in Sources */,
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */,
0EE7FDCB2BE8D12B00D1E390 /* ALTLocalizedError.swift in Sources */,
BF66EEA92501AEC5007EE018 /* Tier.swift in Sources */, BF66EEA92501AEC5007EE018 /* Tier.swift in Sources */,
BF66EEDB2501AECA007EE018 /* StoreApp.swift in Sources */, BF66EEDB2501AECA007EE018 /* StoreApp.swift in Sources */,
BF66EEDE2501AECA007EE018 /* AppID.swift in Sources */, BF66EEDE2501AECA007EE018 /* AppID.swift in Sources */,
@@ -2406,6 +2436,7 @@
BF66EEDD2501AECA007EE018 /* AppPermission.swift in Sources */, BF66EEDD2501AECA007EE018 /* AppPermission.swift in Sources */,
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */, D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */,
BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */, BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */,
0EE7FDC72BE8CF4100D1E390 /* ALTWrappedError.m in Sources */,
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */, D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */, BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */, BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */,
@@ -2430,6 +2461,7 @@
BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */, BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */,
BF66EECC2501AECA007EE018 /* Source.swift in Sources */, BF66EECC2501AECA007EE018 /* Source.swift in Sources */,
BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */, BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */,
0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */,
BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */, BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */,
BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */, BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */,
@@ -2485,7 +2517,6 @@
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */, BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */,
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */, D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
@@ -2508,6 +2539,7 @@
BF41B808233433C100C593A3 /* LoadingState.swift in Sources */, BF41B808233433C100C593A3 /* LoadingState.swift in Sources */,
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */, BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */, D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */,
0EA4263A2C2230150026D7FB /* AnisetteServerList.swift in Sources */,
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */, D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */,
B39F16132918D7C5002E9404 /* Consts.swift in Sources */, B39F16132918D7C5002E9404 /* Consts.swift in Sources */,
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
@@ -2533,9 +2565,11 @@
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */, BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */,
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */,
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */, BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */, BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,

View File

@@ -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
}

View File

@@ -81,7 +81,7 @@ final class AppContentViewController: UITableViewController
self.subtitleLabel.text = self.app.subtitle self.subtitleLabel.text = self.app.subtitle
self.descriptionTextView.text = self.app.localizedDescription self.descriptionTextView.text = self.app.localizedDescription
if let version = self.app.latestVersion if let version = self.app.latestAvailableVersion
{ {
self.versionDescriptionTextView.text = version.localizedDescription self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version) self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)

View File

@@ -384,7 +384,7 @@ private extension AppViewController
button.progress = progress 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.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = versionDate self.navigationBarDownloadButton.countdownDate = versionDate
@@ -510,7 +510,7 @@ extension AppViewController
catch catch
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) let toastView = ToastView(error: error, opensLog: true)
toastView.show(in: self) toastView.show(in: self)
} }
} }

View File

@@ -382,7 +382,7 @@ private extension AppDelegate
for update in updates for update in updates
{ {
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue } 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() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "") content.title = NSLocalizedString("New Update Available", comment: "")

View File

@@ -108,11 +108,9 @@ private extension AuthenticationViewController
case .failure(let error as NSError): case .failure(let error as NSError):
DispatchQueue.main.async { 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) let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self) toastView.show(in: self)
self.toastView = toastView self.toastView = toastView

View File

@@ -114,9 +114,9 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app) let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress 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 else
{ {
@@ -278,7 +278,7 @@ private extension BrowseViewController
{ {
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) let toastView = ToastView(error: error, opensLog: true)
toastView.show(in: self) toastView.show(in: self)
case .success: print("Installed app:", app.bundleIdentifier) case .success: print("Installed app:", app.bundleIdentifier)

View File

@@ -18,8 +18,17 @@ extension TimeInterval
final class ToastView: RSTToastView final class ToastView: RSTToastView
{ {
static let openErrorLogNotification = Notification.Name("ALTOpenErrorLogNotification")
var preferredDuration: TimeInterval 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?) override init(text: String, detailText detailedText: String?)
{ {
if detailedText == nil if detailedText == nil
@@ -43,53 +52,43 @@ final class ToastView: RSTToastView
// RSTToastView does not expose stack view containing labels, // RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview. // so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0 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) convenience init(error: Error)
{ {
var error = error as NSError var error = error as NSError
var underlyingError = error.underlyingError var underlyingError = error.underlyingError
var preferredDuration: TimeInterval?
if if
let unwrappedUnderlyingError = underlyingError, let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue 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 error = unwrappedUnderlyingError as NSError
if let localizedTitle = nsError.localizedTitle {
error = error.withLocalizedTitle(localizedTitle)
}
if let localizedFailure = nsError.localizedFailure {
error = error.withLocalizedFailure(localizedFailure)
}
underlyingError = nil underlyingError = nil
preferredDuration = .longToastViewDuration
} }
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let text: String let detailText = error.localizedDescription
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
}
self.init(text: text, detailText: detailText) self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration
}
} }
required init(coder aDecoder: NSCoder) { required init(coder aDecoder: NSCoder) {
@@ -112,6 +111,18 @@ final class ToastView: RSTToastView
override func show(in view: UIView, duration: TimeInterval) 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) super.show(in: view, duration: duration)
let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "") let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
@@ -127,4 +138,10 @@ final class ToastView: RSTToastView
{ {
self.show(in: view, duration: self.preferredDuration) self.show(in: view, duration: self.preferredDuration)
} }
@objc
func showErrorLog() {
guard self.opensErrorLog else { return }
NotificationCenter.default.post(name: ToastView.openErrorLogNotification, object: self)
}
} }

View File

@@ -8,6 +8,7 @@
import Foundation import Foundation
import minimuxer
import AltStoreCore import AltStoreCore
@available(iOS 14, *) @available(iOS 14, *)
@@ -39,8 +40,12 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
// Give ourselves 9 extra seconds before starting handle() timeout timer. // Give ourselves 9 extra seconds before starting handle() timeout timer.
// 10 seconds or longer results in timeout regardless. // 10 seconds or longer results in timeout regardless.
self.queue.asyncAfter(deadline: .now() + 9.0) { self.queue.asyncAfter(deadline: .now() + 8.0) {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil)) 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 if !DatabaseManager.shared.isStarted
@@ -52,12 +57,14 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
} }
else else
{ {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
self.refreshApps(intent: intent) self.refreshApps(intent: intent)
} }
} }
} }
else else
{ {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
self.refreshApps(intent: intent) self.refreshApps(intent: intent)
} }
} }
@@ -83,6 +90,11 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
// We took too long to finish and return the final result, // We took too long to finish and return the final result,
// so we'll now present a normal notification when finished. // so we'll now present a normal notification when finished.
operation.presentsFinishedNotification = true 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)) 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(). // Queue response in case refreshing finishes after confirm() but before handle().
self.queuedResponses[intent] = response 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)) 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)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
} }
catch let error as NSError catch let error as NSError
{ {

View File

@@ -14,6 +14,7 @@ import Intents
import Combine import Combine
import WidgetKit import WidgetKit
import minimuxer
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
@@ -37,11 +38,6 @@ final class AppManagerPublisher: ObservableObject
fileprivate(set) var refreshProgress = [String: Progress]() 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 final class AppManager
{ {
static let shared = AppManager() static let shared = AppManager()
@@ -317,6 +313,35 @@ extension AppManager
self.run([clearAppCacheOperation], context: nil) 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 extension AppManager
@@ -369,7 +394,7 @@ extension AppManager
case .success(let source): fetchedSources.insert(source) case .success(let source): fetchedSources.insert(source)
case .failure(let error): case .failure(let error):
let source = managedObjectContext.object(with: source.objectID) as! Source let source = managedObjectContext.object(with: source.objectID) as! Source
source.error = (error as NSError).sanitizedForCoreData() source.error = (error as NSError).sanitizedForSerialization()
errors[source] = error errors[source] = error
} }
@@ -457,7 +482,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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) completionHandler(result)
} }
catch catch
@@ -476,7 +501,7 @@ extension AppManager
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
guard let storeApp = app.storeApp else { guard let storeApp = app.storeApp else {
completionHandler(.failure(OperationError.appNotFound)) completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return Progress.discreteProgress(totalUnitCount: 1) return Progress.discreteProgress(totalUnitCount: 1)
} }
@@ -484,7 +509,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown } guard let result = results.values.first else { throw OperationError.unknown() }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -520,8 +545,8 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -559,7 +584,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -585,8 +610,8 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -610,7 +635,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -680,13 +705,20 @@ extension AppManager
var installedApp: InstalledApp? var installedApp: InstalledApp?
} }
let appName = installedApp.name
let context = Context() let context = Context()
context.installedApp = installedApp context.installedApp = installedApp
let enableJITOperation = EnableJITOperation(context: context) let enableJITOperation = EnableJITOperation(context: context)
enableJITOperation.resultHandler = { (result) in 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) self.run([enableJITOperation], context: context, requiresSerialQueue: true)
@@ -822,6 +854,18 @@ private extension AppManager
return bundleIdentifier 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 @discardableResult
@@ -1062,7 +1106,9 @@ private extension AppManager
switch result switch result
{ {
case .failure(let error): context.error = error 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) fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
@@ -1269,14 +1315,21 @@ private extension AppManager
case .success(let installedApp): case .success(let installedApp):
completionHandler(.success(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, // 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. // 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. app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
let installProgress = self._install(app, operation: operation, group: group) { (result) in if minimuxer.ready() {
completionHandler(result) 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): case .failure(let error):
@@ -1543,7 +1596,7 @@ private extension AppManager
} }
guard let application = ALTApplication(fileURL: app.fileURL) else { guard let application = ALTApplication(fileURL: app.fileURL) else {
completionHandler(.failure(OperationError.appNotFound)) completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return progress return progress
} }
@@ -1555,8 +1608,8 @@ private extension AppManager
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString) let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) 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) let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp } guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1694,11 +1747,35 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() } do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) } 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) 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) 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) func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
{ {
// Find "Install AltStore" operation if it already exists in `context` // Find "Install AltStore" operation if it already exists in `context`

View File

@@ -22,13 +22,27 @@ extension AppManager
var managedObjectContext: NSManagedObjectContext? var managedObjectContext: NSManagedObjectContext?
var errorDescription: String? { var localizedTitle: String? {
if let error = self.primaryError var localizedTitle: String?
{ self.managedObjectContext?.performAndWait {
return error.localizedDescription 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? var localizedDescription: String?
self.managedObjectContext?.performAndWait { self.managedObjectContext?.performAndWait {
@@ -67,8 +81,14 @@ extension AppManager
} }
var errorUserInfo: [String : Any] { var errorUserInfo: [String : Any] {
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] } let errors = Array(self.errors.values)
return [NSUnderlyingErrorKey: error] 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) init(_ error: Error)

View File

@@ -155,6 +155,13 @@ final class MyAppsViewController: UICollectionViewController
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue) @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 private extension MyAppsViewController
@@ -188,7 +195,7 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage> func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{ {
let fetchRequest = InstalledApp.updatesFetchRequest() 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)] NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
@@ -197,21 +204,21 @@ private extension MyAppsViewController
dataSource.cellIdentifierHandler = { _ in "UpdateCell" } dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let self = self else { return } 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 let cell = cell as! UpdateCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = app.versionDescription cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.bannerView.iconImageView.image = nil cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.configure(for: app) 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 cell.bannerView.subtitleLabel.text = versionDate
let appName: String let appName: String
@@ -225,7 +232,7 @@ private extension MyAppsViewController
appName = app.name 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.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -528,11 +535,9 @@ private extension MyAppsViewController
guard !failures.isEmpty else { return } guard !failures.isEmpty else { return }
let toastView: ToastView
if let failure = failures.first, results.count == 1 if let failure = failures.first, results.count == 1
{ {
toastView = ToastView(error: failure.value) ToastView(error: failure.value).show(in: self)
} }
else else
{ {
@@ -550,11 +555,10 @@ private extension MyAppsViewController
let error = failures.first?.value as NSError? let error = failures.first?.value as NSError?
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription 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.preferredDuration = 4.0
toastView.show(in: self)
} }
toastView.show(in: self)
} }
self.refreshGroup = nil self.refreshGroup = nil
@@ -645,11 +649,7 @@ private extension MyAppsViewController
@IBAction func refreshAllApps(_ sender: UIBarButtonItem) @IBAction func refreshAllApps(_ sender: UIBarButtonItem)
{ {
if !minimuxer.ready() { guard minimuxerStatus else { return }
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
self.isRefreshingAllApps = true self.isRefreshingAllApps = true
self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.collectionViewLayout.invalidateLayout()
@@ -694,8 +694,7 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
@@ -713,11 +712,7 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem) @IBAction func sideloadApp(_ sender: UIBarButtonItem)
{ {
if !minimuxer.ready() { guard minimuxerStatus else { return }
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil) let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil)
@@ -900,9 +895,8 @@ private extension MyAppsViewController
completion(.failure((OperationError.cancelled))) completion(.failure((OperationError.cancelled)))
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
completion(.failure(error)) completion(.failure(error))
} }
} }
@@ -1016,18 +1010,13 @@ private extension MyAppsViewController
UIApplication.shared.open(installedApp.openAppURL) { success in UIApplication.shared.open(installedApp.openAppURL) { success in
guard !success else { return } guard !success else { return }
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name)) ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
func refresh(_ installedApp: InstalledApp) func refresh(_ installedApp: InstalledApp)
{ {
if !minimuxer.ready() { guard minimuxerStatus else { return }
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let previousProgress = AppManager.shared.refreshProgress(for: installedApp) let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else { guard previousProgress == nil else {
@@ -1050,11 +1039,7 @@ private extension MyAppsViewController
func activate(_ installedApp: InstalledApp) func activate(_ installedApp: InstalledApp)
{ {
if !minimuxer.ready() { guard minimuxerStatus else { return }
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
func finish(_ result: Result<InstalledApp, Error>) func finish(_ result: Result<InstalledApp, Error>)
{ {
@@ -1076,8 +1061,7 @@ private extension MyAppsViewController
DispatchQueue.main.async { DispatchQueue.main.async {
installedApp.isActive = false installedApp.isActive = false
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1131,12 +1115,8 @@ private extension MyAppsViewController
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil) func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
{ {
guard installedApp.isActive else { return } guard installedApp.isActive, minimuxerStatus else { return }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
installedApp.isActive = false installedApp.isActive = false
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
@@ -1149,13 +1129,12 @@ private extension MyAppsViewController
} }
catch catch
{ {
print("Failed to activate app:", error) print("Failed to deactivate app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
installedApp.isActive = true installedApp.isActive = true
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
@@ -1186,8 +1165,7 @@ private extension MyAppsViewController
case .success: break case .success: break
case .failure(let error): case .failure(let error):
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1198,11 +1176,8 @@ private extension MyAppsViewController
func backup(_ installedApp: InstalledApp) func backup(_ installedApp: InstalledApp)
{ {
if !minimuxer.ready() { guard minimuxerStatus else { return }
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let title = NSLocalizedString("Start Backup?", comment: "") 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: "") 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) print("Failed to back up app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue]) self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
} }
} }
@@ -1242,11 +1216,8 @@ private extension MyAppsViewController
func restore(_ installedApp: InstalledApp) func restore(_ installedApp: InstalledApp)
{ {
if !minimuxer.ready() { guard minimuxerStatus else { return }
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name) 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) let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel) alertController.addAction(.cancel)
@@ -1264,8 +1235,7 @@ private extension MyAppsViewController
print("Failed to restore app:", error) print("Failed to restore app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1340,8 +1310,7 @@ private extension MyAppsViewController
print("Failed to change app icon.", error) print("Failed to change app icon.", error)
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1350,14 +1319,11 @@ private extension MyAppsViewController
@available(iOS 14, *) @available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp) func enableJIT(for installedApp: InstalledApp)
{ {
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable { guard minimuxerStatus else { return }
let toastView = ToastView(error: OperationError.tooNewError)
toastView.show(in: self) if #available(iOS 17, *) {
return 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)
if #unavailable(iOS 17), !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return return
} }
@@ -1367,8 +1333,8 @@ private extension MyAppsViewController
{ {
case .success: break case .success: break
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 5) AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
} }
} }
} }

View File

@@ -313,9 +313,8 @@ private extension NewsViewController
{ {
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
case .success: print("Installed app:", storeApp.bundleIdentifier) case .success: print("Installed app:", storeApp.bundleIdentifier)
} }
@@ -391,9 +390,9 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp) let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress 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 else
{ {

View File

@@ -14,7 +14,8 @@ import AltStoreCore
import AltSign import AltSign
import minimuxer import minimuxer
enum AuthenticationError: LocalizedError typealias AuthenticationError = AuthenticationErrorCode.Error
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
{ {
case noTeam case noTeam
case noCertificate case noCertificate
@@ -23,11 +24,11 @@ enum AuthenticationError: LocalizedError
case missingPrivateKey case missingPrivateKey
case missingCertificate case missingCertificate
var errorDescription: String? { var errorFailureReason: String {
switch self { 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 .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 .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate 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 guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context), 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) 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
account.isActiveAccount = true account.isActiveAccount = true
@@ -432,7 +433,7 @@ private extension AuthenticationOperation
} }
else else
{ {
completionHandler(.failure(error ?? OperationError.unknown)) completionHandler(.failure(error ?? OperationError.unknown()))
} }
} }
} }
@@ -449,7 +450,7 @@ private extension AuthenticationOperation
if let team = teams.first { if let team = teams.first {
return completionHandler(.success(team)) return completionHandler(.success(team))
} else { } else {
return completionHandler(.failure(AuthenticationError.noTeam)) return completionHandler(.failure(AuthenticationError(.noTeam)))
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -460,7 +461,7 @@ private extension AuthenticationOperation
if !self.present(selectTeamViewController) if !self.present(selectTeamViewController)
{ {
return completionHandler(.failure(AuthenticationError.noTeam)) return completionHandler(.failure(AuthenticationError(.noTeam)))
} }
} }
} }
@@ -489,20 +490,20 @@ private extension AuthenticationOperation
{ {
func requestCertificate() 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 ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
do do
{ {
let certificate = try Result(certificate, error).get() 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 ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do do
{ {
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else { guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw AuthenticationError.missingCertificate throw AuthenticationError(.missingCertificate)
} }
certificate.privateKey = privateKey certificate.privateKey = privateKey
@@ -523,7 +524,7 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate]) 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 ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success if let error = error, !success

View File

@@ -13,11 +13,12 @@ import AltStoreCore
import EmotionalDamage import EmotionalDamage
import minimuxer import minimuxer
enum RefreshError: LocalizedError typealias RefreshError = RefreshErrorCode.Error
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
{ {
case noInstalledApps case noInstalledApps
var errorDescription: String? { var errorFailureReason: String {
switch self switch self
{ {
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "") case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
@@ -94,7 +95,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
super.main() super.main()
guard !self.installedApps.isEmpty else { guard !self.installedApps.isEmpty else {
self.finish(.failure(RefreshError.noInstalledApps)) self.finish(.failure(RefreshError(.noInstalledApps)))
return return
} }
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
@@ -202,7 +203,7 @@ private extension BackgroundRefreshAppsOperation
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
var shouldPresentAlert = false var shouldPresentAlert = true
do do
{ {
@@ -218,20 +219,18 @@ private extension BackgroundRefreshAppsOperation
content.title = NSLocalizedString("Refreshed Apps", comment: "") content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "") content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
} }
catch RefreshError.noInstalledApps catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.noInstalledApps
{ {
shouldPresentAlert = false shouldPresentAlert = false
} }
catch catch
{ {
print("Failed to refresh apps in background.", error) print("Failed to refresh apps in background.", error)
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "") content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription content.body = error.localizedDescription
shouldPresentAlert = false
} }
if shouldPresentAlert if shouldPresentAlert
{ {
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)

View File

@@ -55,12 +55,15 @@ class BackupAppOperation: ResultOperation<Void>
let appName = installedApp.name let appName = installedApp.name
self.appName = appName 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) var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
returnURLComponents?.host = "appBackupResponse" returnURLComponents?.host = "appBackupResponse"
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) } guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
var openURLComponents = URLComponents() var openURLComponents = URLComponents()
openURLComponents.scheme = installedApp.openAppURL.scheme openURLComponents.scheme = installedApp.openAppURL.scheme
openURLComponents.host = self.action.rawValue openURLComponents.host = self.action.rawValue

View File

@@ -12,66 +12,108 @@ import Roxas
import AltStoreCore import AltStoreCore
import AltSign 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) @objc(DownloadAppOperation)
final class DownloadAppOperation: ResultOperation<ALTApplication> final class DownloadAppOperation: ResultOperation<ALTApplication>
{ {
let app: AppProtocol let app: AppProtocol
let context: AppOperationContext let context: AppOperationContext
private let appName: String
private let bundleIdentifier: String private let bundleIdentifier: String
private var sourceURL: URL?
private let destinationURL: URL private let destinationURL: URL
private let session = URLSession(configuration: .default) private let session = URLSession(configuration: .default)
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL() private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext) init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
{ {
self.app = app self.app = app
self.context = context self.context = context
self.appName = app.name
self.bundleIdentifier = app.bundleIdentifier self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = destinationURL self.destinationURL = destinationURL
super.init() super.init()
// App = 3, Dependencies = 1 // App = 3, Dependencies = 1
self.progress.totalUnitCount = 4 self.progress.totalUnitCount = 4
} }
override func main() override func main()
{ {
super.main() super.main()
if let error = self.context.error if let error = self.context.error
{ {
self.finish(.failure(error)) self.finish(.failure(error))
return return
} }
print("Downloading App:", self.bundleIdentifier) print("Downloading App:", self.bundleIdentifier)
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) } self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
self.downloadApp(from: sourceURL) { result in 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<ALTApplication, any Error>) {
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 do
{ {
let application = try result.get() let application = try result.get()
@@ -112,24 +154,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
} }
} }
override func finish(_ result: Result<ALTApplication, Error>) func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
{
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<ALTApplication, Error>) -> Void)
{ {
func finishOperation(_ result: Result<URL, Error>) func finishOperation(_ result: Result<URL, Error>)
{ {
@@ -138,8 +163,8 @@ private extension DownloadAppOperation
let fileURL = try result.get() let fileURL = try result.get()
var isDirectory: ObjCBool = false 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) try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL: URL let appBundleURL: URL
@@ -178,6 +203,9 @@ private extension DownloadAppOperation
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
do 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() let (fileURL, _) = try Result((fileURL, response), error).get()
finishOperation(.success(fileURL)) finishOperation(.success(fileURL))
@@ -252,7 +280,7 @@ private extension DownloadAppOperation
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data) let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set<URL>() var dependencyURLs = Set<URL>()
var dependencyError: DependencyError? var dependencyError: Error?
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1) 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 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)) completionHandler(.failure(nsError))
} }
catch catch
@@ -294,7 +322,7 @@ private extension DownloadAppOperation
} }
} }
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void) func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
{ {
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do do
@@ -315,9 +343,10 @@ private extension DownloadAppOperation
completionHandler(.success(destinationURL)) 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) progress.addChild(downloadTask.progress, withPendingUnitCount: 1)

View File

@@ -61,12 +61,10 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
switch result { switch result {
case .failure(let error): case .failure(let error):
switch error { switch error {
case .invalidURL: case .invalidURL, .errorConnecting:
self.finish(.failure(OperationError.unabletoconnectSideJIT)) self.finish(.failure(OperationError.unableToConnectSideJIT))
case .errorConnecting:
self.finish(.failure(OperationError.unabletoconnectSideJIT))
case .deviceNotFound: case .deviceNotFound:
self.finish(.failure(OperationError.unabletoconSideJITDevice)) self.finish(.failure(OperationError.unableToRespondSideJITDevice))
case .other(let message): case .other(let message):
if let startRange = message.range(of: "<p>"), if let startRange = message.range(of: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) { let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {

View File

@@ -45,7 +45,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
return return
} }
self.url = AnisetteManager.currentURL self.url = URL(string: UserDefaults.standard.menuAnisetteURL)
print("Anisette URL: \(self.url!.absoluteString)") print("Anisette URL: \(self.url!.absoluteString)")
if let identifier = Keychain.shared.identifier, if let identifier = Keychain.shared.identifier,
@@ -408,6 +408,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) { func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
fetchClientInfo { fetchClientInfo {
print("Fetching anisette V3") print("Fetching anisette V3")
let url = UserDefaults.standard.menuAnisetteURL
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers")) var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
request.httpMethod = "POST" request.httpMethod = "POST"
request.httpBody = try! JSONSerialization.data(withJSONObject: [ request.httpBody = try! JSONSerialization.data(withJSONObject: [

View File

@@ -45,8 +45,8 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
let session = self.context.session let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) } 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.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
@@ -260,7 +260,7 @@ extension FetchProvisioningProfilesOperation
{ {
if let expirationDate = sortedExpirationDates.first 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 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 else
{ {

View File

@@ -12,7 +12,10 @@ import Roxas
class ResultOperation<ResultType>: Operation class ResultOperation<ResultType>: Operation
{ {
var resultHandler: ((Result<ResultType, Error>) -> Void)? var resultHandler: ((Result<ResultType, Error>) -> Void)?
// Should only be set by subclasses
var localizedFailure: String?
@available(*, unavailable) @available(*, unavailable)
override func finish() override func finish()
{ {
@@ -22,16 +25,20 @@ class ResultOperation<ResultType>: Operation
func finish(_ result: Result<ResultType, Error>) func finish(_ result: Result<ResultType, Error>)
{ {
guard !self.isFinished else { return } guard !self.isFinished else { return }
var result = result
if self.isCancelled if self.isCancelled
{ {
self.resultHandler?(.failure(OperationError.cancelled)) result = .failure(OperationError.cancelled)
} }
else 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)
self.resultHandler?(result) let error = nsError.withLocalizedFailure(localizedFailure)
result = .failure(error)
} }
self.resultHandler?(result)
super.finish() super.finish()
} }
} }

View File

@@ -8,81 +8,186 @@
import Foundation import Foundation
import AltSign import AltSign
import AltStoreCore
import minimuxer 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 static func SideJITIssue(error: String?) -> OperationError {
case unabletoconnectSideJIT var o = OperationError(code: .SideJITIssue)
case unabletoconSideJITDevice o.errorFailure = error
case wrongIP return o
case SideJITIssue(error: String) }
case refreshsidejit
case unknownResult
case cancelled
case timedOut
case notAuthenticated static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
case appNotFound OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
case unknownUDID
static func provisioningError(result: String, message: String?) -> OperationError {
case invalidApp var o = OperationError(code: .provisioningError, failureReason: result)
case invalidParameters o.errorTitle = message
return o
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date) }
case noSources static func cacheClearError(errors: [String]) -> OperationError {
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
case openAppFailed(name: String) }
case missingAppGroup
static func anisetteV1Error(message: String) -> OperationError {
case noWiFi OperationError(code: .anisetteV1Error, failureReason: message)
case tooNewError }
case anisetteV1Error(message: String)
case provisioningError(result: String, message: String?) static func anisetteV3Error(message: String) -> OperationError {
case anisetteV3Error(message: String) OperationError(code: .anisetteV3Error, failureReason: message)
}
case cacheClearError(errors: [String])
}
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "") 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 .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "") case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "") case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "") case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "") case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", 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 .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 accessed.", comment: "")
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "") case .appNotFound:
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "") let appName = self.appName ?? NSLocalizedString("The app", 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: "") return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
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 .openAppFailed:
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: "") let appName = self.appName ?? NSLocalizedString("The app", comment: "")
case .wrongIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", 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 .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: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", 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: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
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: 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 .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: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
case .cacheClearError(let errors): return String(format: NSLocalizedString("An error occurred while clearing cache: %@", comment: ""), errors.joined(separator: "\n")) case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
case .SideJITIssue(let errors): return NSLocalizedString("SideJITServer Error: \(errors)", comment: "")
} }
} }
var recoverySuggestion: String? { 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 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 if requiredAppIDs > 1
{ {
let availableText: String let availableText: String
@@ -94,23 +199,23 @@ enum OperationError: LocalizedError
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs)) 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) let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage message = prefixMessage + " " + baseMessage + "\n\n"
} }
else else
{ {
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date) message = baseMessage + " "
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
} }
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 return message
default: return nil default: return nil

View File

@@ -25,22 +25,38 @@ protocol PatchAppContext
var error: Error? { get } var error: Error? { get }
} }
enum PatchAppError: LocalizedError extension PatchAppError
{ {
case unsupportedOperatingSystemVersion(OperatingSystemVersion) enum Code: Int, ALTErrorCode, CaseIterable {
typealias Error = PatchAppError
var errorDescription: String? {
switch self case unsupportedOperatingSystemVersion
{ }
case .unsupportedOperatingSystemVersion(let osVersion):
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)" static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
if osVersion.patchVersion != 0 PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
{ }
osVersionString += ".\(osVersion.patchVersion)" }
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: "")
} }
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
} }
} }
} }

View File

@@ -439,7 +439,7 @@ private extension PatchViewController
do 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() _ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier) if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)

View File

@@ -38,8 +38,8 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
if let error = self.context.error { return self.finish(.failure(error)) } 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 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 { for p in profiles {
do { do {
let bytes = p.value.data.toRustByteSlice() let bytes = p.value.data.toRustByteSlice()
@@ -49,14 +49,13 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
} }
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
print("Sending refresh app request...")
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier) let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
self.managedObjectContext.perform { self.managedObjectContext.perform {
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else { 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 return
} }
installedApp.update(provisioningProfile: p.value) installedApp.update(provisioningProfile: p.value)

View File

@@ -57,7 +57,7 @@ final class SendAppOperation: ResultOperation<()>
} }
} else { } else {
print("IPA doesn't exist????") print("IPA doesn't exist????")
self.finish(.failure(OperationError.appNotFound)) self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name))))
} }
} }
} }

View File

@@ -8,48 +8,87 @@
import Foundation import Foundation
import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
enum VerificationError: ALTLocalizedError extension VerificationError
{ {
case privateEntitlements(ALTApplication, entitlements: [String: Any]) enum Code: Int, ALTErrorCode, CaseIterable {
case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String) typealias Error = VerificationError
case iOSVersionNotSupported(ALTApplication)
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 { var errorDescription: String? {
switch self switch self.code {
{ case .iOSVersionNotSupported:
case .privateEntitlements(let app, _): return app guard let deviceOSVersion else { return nil }
case .mismatchedBundleIdentifiers(let app, _): return app
case .iOSVersionNotSupported(let app): return app 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? { var errorFailureReason: String {
return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name) switch self.code
}
var failureReason: String? {
switch self
{ {
case .privateEntitlements(let app, _): case .privateEntitlements:
return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name) let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
return String(formatted: "“%@” requires private permissions.", appName)
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 .mismatchedBundleIdentifiers:
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
case .iOSVersionNotSupported(let app): return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID)
let name = app.name } else {
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)" }
if app.minimumiOSVersion.patchVersion > 0
{ case .iOSVersionNotSupported:
version += ".\(app.minimumiOSVersion.patchVersion)" 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<Void>
guard let app = self.context.app else { throw OperationError.invalidParameters } guard let app = self.context.app else { throw OperationError.invalidParameters }
guard app.bundleIdentifier == self.context.bundleIdentifier else { if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier) guard app.bundleIdentifier == self.context.bundleIdentifier else {
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
}
} }
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else { guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
throw VerificationError.iOSVersionNotSupported(app) throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
} }
if #available(iOS 13.5, *) if #available(iOS 13.5, *)
@@ -116,7 +157,7 @@ final class VerifyAppOperation: ResultOperation<Void>
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any] let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
app.hasPrivateEntitlements = true app.hasPrivateEntitlements = true
let error = VerificationError.privateEntitlements(app, entitlements: entitlements) let error = VerificationError.privateEntitlements(entitlements, app: app)
self.process(error) { (result) in self.process(error) { (result) in
self.finish(result.mapError { $0 as Error }) 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)) } guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
DispatchQueue.main.async { 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 permissions = entitlements.keys.sorted().joined(separator: "\n")
let message = String(format: NSLocalizedString(""" let message = String(format: NSLocalizedString("""
You must allow access to these private permissions before continuing: You must allow access to these private permissions before continuing:
@@ -166,8 +208,7 @@ private extension VerifyAppOperation
})) }))
presentingViewController.present(alertController, animated: true, completion: nil) presentingViewController.present(alertController, animated: true, completion: nil)
case .mismatchedBundleIdentifiers: return completion(.failure(error)) case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error))
case .iOSVersionNotSupported: return completion(.failure(error))
} }
} }
} }

View File

@@ -1,111 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Root</string>
<key>ApplicationGroupContainerIdentifier</key>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Anisette Server</string>
<key>Key</key>
<string>customAnisetteURL</string>
<key>DefaultValue</key>
<string>https://ani.sidestore.io</string>
<key>Titles</key>
<array>
<string>SideStore</string>
<string>SideStore (.zip)</string>
<string>SideStore (.xyz)</string>
<string>Macley (US)</string>
<string>Jawshoeadan</string>
<string>WesleyBryie</string>
<string>Stossy11</string>
</array>
<key>Values</key>
<array>
<string>https://ani.sidestore.io</string>
<string>https://ani.sidestore.zip</string>
<string>https://ani.846969.xyz</string>
<string>http://5.249.163.88:6969/</string>
<string>https://anisette.jawshoeadan.me</string>
<string>https://ani.wesbryie.com</string>
<string>https://anisette.stossy11.com</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>Danger Zone</string>
<key>FooterText</key>
<string>If you disable the toggle then app will use the server you input into the &quot;Anisette URL&quot; box rather than one selected from the above selector.</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Use preferred servers</string>
<key>Key</key>
<string>textServer</string>
<key>DefaultValue</key>
<true/>
<key>FooterText</key>
<string>chicken</string>
</dict>
<dict>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>Title</key>
<string>Anisette URL</string>
<key>Key</key>
<string>textInputAnisetteURL</string>
<key>AutocapitalizationType</key>
<string>None</string>
<key>AutocorrectionType</key>
<string>No</string>
<key>KeyboardType</key>
<string>URL</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>SideJITServer</string>
<key>FooterText</key>
<string>If you disable the toggle then the app will not be able to access and use SideJITServer on iOS 17+. &quot;SideJITServer IP&quot; is not needed but is recommended for stability.</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Enable SideJIT Support</string>
<key>Key</key>
<string>sidejitenable</string>
<key>DefaultValue</key>
<false/>
<key>FooterText</key>
<string>chicken</string>
</dict>
<dict>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>Title</key>
<string>SideJITServer IP</string>
<key>Key</key>
<string>textInputSideJITServerurl</string>
<key>AutocapitalizationType</key>
<string>None</string>
<key>AutocorrectionType</key>
<string>No</string>
<key>KeyboardType</key>
<string>URL</string>
</dict>
</array>
</dict>
</plist>

View File

@@ -10,6 +10,11 @@ import Foundation
public struct AnisetteManager { public struct AnisetteManager {
var menuURL: String {
var url: String
url = UserDefaults.standard.menuAnisetteURL
return url
}
/// User defined URL from Settings/UserDefaults /// User defined URL from Settings/UserDefaults
static var userURL: String? { static var userURL: String? {
var urlString: String? var urlString: String?

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -8,6 +8,16 @@
import UIKit 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) @objc(ErrorLogTableViewCell)
final class ErrorLogTableViewCell: UITableViewCell final class ErrorLogTableViewCell: UITableViewCell
{ {

View File

@@ -21,6 +21,13 @@ final class ErrorLogViewController: UITableViewController
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
private var expandedErrorIDs = Set<NSManagedObjectID>() private var expandedErrorIDs = Set<NSManagedObjectID>()
private var isScrolling = false {
didSet {
guard self.isScrolling != oldValue else { return }
self.updateButtonInteractivity()
}
}
private lazy var timeFormatter: DateFormatter = { private lazy var timeFormatter: DateFormatter = {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none dateFormatter.dateStyle = .none
@@ -39,6 +46,15 @@ final class ErrorLogViewController: UITableViewController
self.tableView.dataSource = self.dataSource self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = 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 private extension ErrorLogViewController
@@ -60,14 +76,8 @@ private extension ErrorLogViewController
let cell = cell as! ErrorLogTableViewCell let cell = cell as! ErrorLogTableViewCell
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date) cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "") cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
cell.errorCodeLabel.text = loggedError.error.localizedErrorCode
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
}
let nsError = loggedError.error as NSError let nsError = loggedError.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
cell.errorDescriptionTextView.text = errorDescription cell.errorDescriptionTextView.text = errorDescription
@@ -93,12 +103,19 @@ private extension ErrorLogViewController
}, },
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
self?.searchFAQ(for: loggedError) 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.menu = menu
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
cell.selectionStyle = .none
} else {
cell.menuButton.isUserInteractionEnabled = false
} }
// Include errorDescriptionTextView's text in cell summary. // 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: ". ") 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) 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)! 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)] components.queryItems = [URLQueryItem(name: "q", value: query)]
let safariViewController = SFSafariViewController(url: components.url ?? baseURL) let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
safariViewController.preferredControlTintColor = .altPrimary safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true) self.present(safariViewController, animated: true)
} }
func viewMoreDetails(for loggedError: LoggedError) {
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
}
} }
extension ErrorLogViewController extension ErrorLogViewController
{ {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{ {
guard #unavailable(iOS 14) else { return }
let loggedError = self.dataSource.item(at: indexPath) let loggedError = self.dataSource.item(at: indexPath)
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
@@ -321,3 +343,32 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
return fileURL as QLPreviewItem 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
}
}
}

View File

@@ -3,7 +3,7 @@
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -20,8 +20,8 @@
<color key="backgroundColor" name="SettingsBackground"/> <color key="backgroundColor" name="SettingsBackground"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2"> <label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
<rect key="frame" x="0.0" y="1340" width="375" height="25"/> <rect key="frame" x="0.0" y="1245" width="375" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -351,22 +351,22 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw">
<rect key="frame" x="30" y="15.5" width="86" height="20.5"/> <rect key="frame" x="30" y="15.5" width="86" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk"> <stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
<rect key="frame" x="187.5" y="15.5" width="157.5" height="20.5"/> <rect key="frame" x="187.5" y="15.5" width="157.5" height="20.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
<rect key="frame" x="0.0" y="0.0" width="125.5" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="125.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
<rect key="frame" x="139.5" y="0.0" width="18" height="20.5"/> <rect key="frame" x="139.5" y="0.0" width="18" height="20.5"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -395,22 +395,22 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/> <rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X"> <stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/> <rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
<rect key="frame" x="129" y="0.0" width="18" height="20.5"/> <rect key="frame" x="129" y="0.0" width="18" height="20.5"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -618,7 +618,7 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/> <userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
<connections> <connections>
<segue destination="g8a-Rf-zWa" kind="show" id="vFC-Id-Ww6"/> <segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="vFC-Id-Ww6"/>
</connections> </connections>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
@@ -655,7 +655,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eZ3-BT-q4D" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eZ3-BT-q4D" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1118" width="375" height="51"/> <rect key="frame" x="0.0" y="1074" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eZ3-BT-q4D" id="17m-VV-hzf"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eZ3-BT-q4D" id="17m-VV-hzf">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -688,7 +688,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1169" width="375" height="51"/> <rect key="frame" x="0.0" y="1125" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -721,14 +721,14 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1220" width="375" height="51"/> <rect key="frame" x="0.0" y="1176" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reset adi.pb" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
<rect key="frame" x="30" y="15.5" width="102" height="20.5"/> <rect key="frame" x="30" y="15.5" width="135.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -746,39 +746,6 @@
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/> <edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="fj2-EJ-Z98" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1271" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fj2-EJ-Z98" id="BcT-Fs-KNg">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Advanced Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OcM-OM-uDE">
<rect key="frame" x="30" y="15.5" width="154" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Pcu-Sy-yfZ">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Pcu-Sy-yfZ" secondAttribute="trailing" id="CFy-IO-4eb"/>
<constraint firstItem="OcM-OM-uDE" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="OGl-h4-FPx"/>
<constraint firstItem="Pcu-Sy-yfZ" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="R7L-4O-lTn"/>
<constraint firstItem="OcM-OM-uDE" firstAttribute="leading" secondItem="BcT-Fs-KNg" secondAttribute="leadingMargin" id="yoh-C6-UC5"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes> <userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style"> <userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/> <integer key="value" value="3"/>
@@ -1150,7 +1117,7 @@ Settings by i cons from the Noun Project</string>
</textView> </textView>
</subviews> </subviews>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5"> <button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5" customClass="ErrorLogMenuButton">
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/> <rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="NO"/> <bool key="isElement" value="NO"/>
@@ -1199,11 +1166,73 @@ Settings by i cons from the Noun Project</string>
</barButtonItem> </barButtonItem>
</rightBarButtonItems> </rightBarButtonItems>
</navigationItem> </navigationItem>
<connections>
<segue destination="7gm-d1-zWK" kind="presentation" identifier="showErrorDetails" id="9vz-y6-evp"/>
</connections>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1697" y="1774"/> <point key="canvasLocation" x="1697" y="1774"/>
</scene> </scene>
<!--Error Details View Controller-->
<scene sceneID="XNO-Yg-I7t">
<objects>
<viewController id="xB2-Se-VVg" customClass="ErrorDetailsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eBQ-se-VIy">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ctd-NB-4ov">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="Nm8-69-Ngi"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="ctd-NB-4ov" firstAttribute="leading" secondItem="eBQ-se-VIy" secondAttribute="leading" id="Cv1-Te-gBH"/>
<constraint firstItem="ctd-NB-4ov" firstAttribute="top" secondItem="eBQ-se-VIy" secondAttribute="top" id="HRY-Rg-iMI"/>
<constraint firstAttribute="trailing" secondItem="ctd-NB-4ov" secondAttribute="trailing" id="Lc1-K7-iuq"/>
<constraint firstAttribute="bottom" secondItem="ctd-NB-4ov" secondAttribute="bottom" id="zCz-Cy-Y5z"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="XpE-V9-EaY">
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="rnr-dX-4Ev">
<connections>
<segue destination="ZSp-1n-UJ9" kind="unwind" unwindAction="unwindFromErrorDetails:" id="TFu-zD-QyF"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="textView" destination="ctd-NB-4ov" id="x2C-9R-Xz1"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8AM-Vx-XTN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="ZSp-1n-UJ9" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="3389.5999999999999" y="1772.5637181409297"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="4LJ-Od-dCK">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7gm-d1-zWK" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="xB2-Se-VVg" kind="relationship" relationship="rootViewController" id="RpP-UM-JfJ"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="OXW-bf-HIj" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2554" y="1773"/>
</scene>
</scenes> </scenes>
<resources> <resources>
<image name="Next" width="18" height="18"/> <image name="Next" width="18" height="18"/>
@@ -1218,5 +1247,8 @@ Settings by i cons from the Noun Project</string>
<systemColor name="labelColor"> <systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources> </resources>
</document> </document>

View File

@@ -7,6 +7,7 @@
// //
import UIKit import UIKit
import SwiftUI
import SafariServices import SafariServices
import MessageUI import MessageUI
import Intents import Intents
@@ -57,7 +58,7 @@ extension SettingsViewController
case refreshSideJITServer case refreshSideJITServer
case clearCache case clearCache
case resetPairingFile case resetPairingFile
case resetAdiPb case anisetteServers
case advancedSettings case advancedSettings
} }
@@ -92,6 +93,7 @@ final class SettingsViewController: UITableViewController
super.init(coder: aDecoder) super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
} }
override func viewDidLoad() override func viewDidLoad()
@@ -109,16 +111,13 @@ final class SettingsViewController: UITableViewController
debugModeGestureRecognizer.numberOfTouchesRequired = 3 debugModeGestureRecognizer.numberOfTouchesRequired = 3
self.tableView.addGestureRecognizer(debugModeGestureRecognizer) self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
print(Bundle.main.infoDictionary)
var versionString: String = "" var versionString: String = ""
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
{ {
versionString += "SideStore \(version)" versionString += "SideStore \(version)"
if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String { if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
print(xcode)
versionString += " - Xcode \(xcode) - " versionString += " - Xcode \(xcode) - "
if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String { if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
print(build)
versionString += "\(build)" versionString += "\(build)"
} }
} }
@@ -133,8 +132,14 @@ final class SettingsViewController: UITableViewController
{ {
versionString += "SideStore\t" versionString += "SideStore\t"
} }
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version") versionString += "\n\(Bundle.Info.appbundleIdentifier)"
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
self.versionLabel.numberOfLines = 0
self.versionLabel.lineBreakMode = .byWordWrapping
self.versionLabel.setNeedsUpdateConstraints()
self.tableView.contentInset.bottom = 40 self.tableView.contentInset.bottom = 40
self.update() self.update()
@@ -152,6 +157,18 @@ final class SettingsViewController: UITableViewController
self.update() self.update()
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "anisetteServers" {
let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: {
ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self)
}))
self.show(controller, sender: nil)
} else {
super.prepare(for: segue, sender: sender)
}
}
} }
private extension SettingsViewController private extension SettingsViewController
@@ -408,6 +425,15 @@ private extension SettingsViewController
self.performSegue(withIdentifier: "showPatreon", sender: nil) self.performSegue(withIdentifier: "showPatreon", sender: nil)
} }
} }
@objc func openErrorLog(_: Notification) {
guard self.presentedViewController == nil else { return }
self.navigationController?.popViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.performSegue(withIdentifier: "showErrorLog", sender: nil)
}
}
} }
extension SettingsViewController extension SettingsViewController
@@ -440,14 +466,14 @@ extension SettingsViewController
{ {
let cell = super.tableView(tableView, cellForRowAt: indexPath) let cell = super.tableView(tableView, cellForRowAt: indexPath)
// if #available(iOS 14, *) {} if #available(iOS 14, *) {}
// else if let cell = cell as? InsetGroupTableViewCell, else if let cell = cell as? InsetGroupTableViewCell,
// indexPath.section == Section.appRefresh.rawValue, indexPath.section == Section.appRefresh.rawValue,
// indexPath.row == AppRefreshRow.backgroundRefresh.rawValue indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
// { {
// // Only one row is visible pre-iOS 14. // Only one row is visible pre-iOS 14.
// cell.style = .single cell.style = .single
// } }
if AppRefreshRow.AllCases().count == 1 if AppRefreshRow.AllCases().count == 1
{ {
@@ -667,25 +693,11 @@ extension SettingsViewController
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath) alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true) self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true) self.tableView.deselectRow(at: indexPath, animated: true)
case .resetAdiPb: case .anisetteServers:
let alertController = UIAlertController( self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: UIHostingController(rootView: AnisetteServers(selected: "", errorCallback: {
title: NSLocalizedString("Are you sure you want to reset the adi.pb file?", comment: ""), ToastView(text: "Reset adi.pb", detailText: "Buh").show(in: self)
message: NSLocalizedString("The adi.pb file is used to generate anisette data, which is required to log into an Apple ID. If you are having issues with account related things, you can try this. However, you will be required to do 2FA again. This will do nothing if you are using an older anisette server.", comment: ""), }))), sender: nil)
preferredStyle: UIAlertController.Style.actionSheet) // self.performSegue(withIdentifier: "anisetteServers", sender: nil)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset adi.pb", comment: ""), style: .destructive){ _ in
if Keychain.shared.adiPb != nil {
Keychain.shared.adiPb = nil
print("Cleared adi.pb from keychain")
}
self.tableView.deselectRow(at: indexPath, animated: true)
})
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
case .advancedSettings: case .advancedSettings:
// Create the URL that deep links to your app's custom settings. // Create the URL that deep links to your app's custom settings.
if let url = URL(string: UIApplication.openSettingsURLString) { if let url = URL(string: UIApplication.openSettingsURLString) {

View File

@@ -12,17 +12,22 @@ import CoreData
import AltStoreCore import AltStoreCore
import Roxas import Roxas
struct SourceError: LocalizedError struct SourceError: ALTLocalizedError
{ {
enum Code enum Code: Int, ALTErrorCode
{ {
typealias Error = SourceError
case unsupported case unsupported
} }
var code: Code var code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var source: Source @Managed var source: Source
var errorDescription: String? { var errorFailureReason: String {
switch self.code switch self.code
{ {
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name) case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name)

View File

@@ -33,6 +33,7 @@ final class TabBarController: UITabBarController
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
} }
override func viewDidAppear(_ animated: Bool) override func viewDidAppear(_ animated: Bool)
@@ -141,4 +142,7 @@ private extension TabBarController
{ {
self.selectedIndex = Tab.myApps.rawValue self.selectedIndex = Tab.myApps.rawValue
} }
@objc func openErrorLog(_: Notification){
self.selectedIndex = Tab.settings.rawValue
}
} }

View File

@@ -10,23 +10,27 @@ import Foundation
import CoreData import CoreData
@propertyWrapper @dynamicMemberLookup @propertyWrapper @dynamicMemberLookup
struct Managed<ManagedObject: NSManagedObject> struct Managed<ManagedObject>
{ {
var wrappedValue: ManagedObject { var wrappedValue: ManagedObject {
didSet { didSet {
self.managedObjectContext = self.wrappedValue.managedObjectContext self.managedObjectContext = self.managedObject?.managedObjectContext
} }
} }
private var managedObjectContext: NSManagedObjectContext?
var projectedValue: Managed<ManagedObject> { var projectedValue: Managed<ManagedObject> {
return self return self
} }
private var managedObjectContext: NSManagedObjectContext?
private var managedObject: NSManagedObject? {
return self.wrappedValue as? NSManagedObject
}
init(wrappedValue: ManagedObject) init(wrappedValue: ManagedObject)
{ {
self.wrappedValue = wrappedValue self.wrappedValue = wrappedValue
self.managedObjectContext = wrappedValue.managedObjectContext self.managedObjectContext = self.managedObject?.managedObjectContext
} }
subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T
@@ -46,4 +50,18 @@ struct Managed<ManagedObject: NSManagedObject>
return result return result
} }
// Optionals
subscript<Wrapped, T>(dynamicMember keyPath: KeyPath<Wrapped, T>) -> T? where ManagedObject == Optional<Wrapped> {
var result: T?
if let context = self.managedObjectContext {
context.performAndWait {
result = self.wrappedValue?[keyPath: keyPath] as? T
}
} else {
result = self.wrappedValue?[keyPath: keyPath] as? T
}
return result
}
} }

View File

@@ -23,5 +23,6 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
// Shared // Shared
#import <AltStoreCore/ALTConstants.h> #import <AltStoreCore/ALTConstants.h>
#import <AltStoreCore/ALTConnection.h> #import <AltStoreCore/ALTConnection.h>
#import <AltStoreCore/ALTWrappedError.h>
#import <AltStoreCore/NSError+ALTServerError.h> #import <AltStoreCore/NSError+ALTServerError.h>
#import <AltStoreCore/CFNotificationName+AltStore.h> #import <AltStoreCore/CFNotificationName+AltStore.h>

View File

@@ -0,0 +1,18 @@
//
// OperatingSystemVersion+Comparable.swift
// AltStoreCore
//
// Created by nythepegasus on 5/9/24.
//
import Foundation
extension OperatingSystemVersion: Comparable {
public static func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
return lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion
}
public static func <(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
return lhs.stringValue.compare(rhs.stringValue, options: .numeric) == .orderedAscending
}
}

View File

@@ -0,0 +1,14 @@
//
// String+SideStore.swift
// AltStoreCore
//
// Created by nythepegasus on 5/9/24.
//
import Foundation
public extension String {
init(formatted: String, comment: String? = nil, _ args: String...) {
self.init(format: NSLocalizedString(formatted, comment: comment ?? ""), args)
}
}

View File

@@ -26,6 +26,8 @@ public extension UserDefaults
@NSManaged var textInputSideJITServerurl: String? @NSManaged var textInputSideJITServerurl: String?
@NSManaged var textInputAnisetteURL: String? @NSManaged var textInputAnisetteURL: String?
@NSManaged var customAnisetteURL: String? @NSManaged var customAnisetteURL: String?
@NSManaged var menuAnisetteURL: String
@NSManaged var menuAnisetteList: String
@NSManaged var preferredServerID: String? @NSManaged var preferredServerID: String?
@NSManaged var isBackgroundRefreshEnabled: Bool @NSManaged var isBackgroundRefreshEnabled: Bool
@@ -81,8 +83,9 @@ public extension UserDefaults
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported, #keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions, #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing, #keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
#keyPath(UserDefaults.requiresAppGroupMigration): true #keyPath(UserDefaults.requiresAppGroupMigration): true,
] #keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io"
] as [String : Any]
UserDefaults.standard.register(defaults: defaults) UserDefaults.standard.register(defaults: defaults)
UserDefaults.shared.register(defaults: defaults) UserDefaults.shared.register(defaults: defaults)

View File

@@ -40,7 +40,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
/* Relationships */ /* Relationships */
@NSManaged public private(set) var app: StoreApp? @NSManaged public private(set) var app: StoreApp?
@NSManaged public private(set) var latestVersionApp: StoreApp? @NSManaged @objc(latestVersionApp) public internal(set) var latestSupportedVersionApp: StoreApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
@@ -54,6 +54,8 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
case localizedDescription case localizedDescription
case downloadURL case downloadURL
case size case size
case minOSVersion
case maxOSVersion
} }
public required init(from decoder: Decoder) throws public required init(from decoder: Decoder) throws
@@ -72,6 +74,9 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL) self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
self.size = try container.decode(Int64.self, forKey: .size) self.size = try container.decode(Int64.self, forKey: .size)
self._minOSVersion = try container.decodeIfPresent(String.self, forKey: .minOSVersion)
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
} }
catch catch
{ {
@@ -113,4 +118,13 @@ public extension AppVersion
return appVersion return appVersion
} }
var isSupported: Bool {
if let minOSVersion = self.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
return false
} else if let maxOSVersion = self.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
return false
}
return true
}
} }

View File

@@ -13,11 +13,11 @@ import Roxas
extension CFNotificationName extension CFNotificationName
{ {
fileprivate static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString) fileprivate static let willMigrateDatabase = CFNotificationName("com.rileytestut.AltStore.WillMigrateDatabase" as CFString)
} }
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in private let ReceivedWillMigrateDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
DatabaseManager.shared.receivedWillAccessDatabaseNotification() DatabaseManager.shared.receivedWillMigrateDatabaseNotification()
} }
fileprivate class PersistentContainer: RSTPersistentContainer fileprivate class PersistentContainer: RSTPersistentContainer
@@ -52,15 +52,15 @@ public class DatabaseManager
private let coordinator = NSFileCoordinator() private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue() private let coordinatorQueue = OperationQueue()
private var ignoreWillAccessDatabaseNotification = false private var ignoreWillMigrateDatabaseNotification = false
private init() private init()
{ {
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self)) self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
self.persistentContainer.preferredMergePolicy = MergePolicy() self.persistentContainer.preferredMergePolicy = MergePolicy()
let observer = Unmanaged.passUnretained(self).toOpaque() let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately) CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillMigrateDatabaseNotification, CFNotificationName.willMigrateDatabase.rawValue, nil, .deliverImmediately)
} }
} }
@@ -87,10 +87,13 @@ public extension DatabaseManager
guard !self.isStarted else { return finish(nil) } guard !self.isStarted else { return finish(nil) }
// Quit any other running AltStore processes to prevent concurrent database access during and after migration. if self.persistentContainer.isMigrationRequired {
self.ignoreWillAccessDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true) // Quit any other running AltStore processes to prevent concurrent database access during and after migration.
self.ignoreWillMigrateDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willMigrateDatabase, nil, nil, true)
}
self.migrateDatabaseToAppGroupIfNeeded { (result) in self.migrateDatabaseToAppGroupIfNeeded { (result) in
switch result switch result
{ {
@@ -229,7 +232,7 @@ private extension DatabaseManager
else else
{ {
storeApp = StoreApp.makeAltStoreApp(in: context) storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.latestVersion?.version = localApp.version storeApp.latestSupportedVersion?.version = localApp.version
storeApp.source = altStoreSource storeApp.source = altStoreSource
} }
@@ -417,13 +420,13 @@ private extension DatabaseManager
} }
} }
func receivedWillAccessDatabaseNotification() func receivedWillMigrateDatabaseNotification()
{ {
defer { self.ignoreWillAccessDatabaseNotification = false } defer { self.ignoreWillMigrateDatabaseNotification = false }
// Ignore notifications sent by the current process. // Ignore notifications sent by the current process.
guard !self.ignoreWillAccessDatabaseNotification else { return } guard !self.ignoreWillMigrateDatabaseNotification else { return }
exit(104) exit(104)
} }
} }

View File

@@ -62,14 +62,14 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
@objc public var hasUpdate: Bool { @objc public var hasUpdate: Bool {
if self.storeApp == nil { return false } if self.storeApp == nil { return false }
if self.storeApp!.latestVersion == nil { return false } if self.storeApp!.latestSupportedVersion == nil { return false }
let currentVersion = SemanticVersion(self.version) let currentVersion = SemanticVersion(self.version)
let latestVersion = SemanticVersion(self.storeApp!.latestVersion!.version) let latestVersion = SemanticVersion(self.storeApp!.latestSupportedVersion!.version)
if currentVersion == nil || latestVersion == nil { if currentVersion == nil || latestVersion == nil {
// One of the versions is not valid SemVer, fall back to comparing the version strings by character // One of the versions is not valid SemVer, fall back to comparing the version strings by character
return self.version < self.storeApp!.latestVersion!.version return self.version < self.storeApp!.latestSupportedVersion!.version
} }
return currentVersion! < latestVersion! return currentVersion! < latestVersion!
@@ -163,8 +163,8 @@ public extension InstalledApp
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp> class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
{ {
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp> let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES", fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate)) #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version))
return fetchRequest return fetchRequest
} }
@@ -275,14 +275,12 @@ public extension InstalledApp
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) } do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
catch { print("Creating App Directory Error: \(error)") } catch { print("Creating App Directory Error: \(error)") }
print("`appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL return appsDirectoryURL
} }
class var legacyAppsDirectoryURL: URL { class var legacyAppsDirectoryURL: URL {
let baseDirectory = FileManager.default.applicationSupportDirectory let baseDirectory = FileManager.default.applicationSupportDirectory
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps") let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
print("legacy `appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL return appsDirectoryURL
} }

View File

@@ -19,6 +19,8 @@ extension LoggedError
case deactivate case deactivate
case backup case backup
case restore case restore
case connection
case enableJIT
} }
} }
@@ -66,7 +68,12 @@ public class LoggedError: NSManagedObject, Fetchable
self.date = date self.date = date
self._operation = operation?.rawValue self._operation = operation?.rawValue
let nsError = error as NSError let nsError: NSError
if let error = error as? ALTServerError, error.code == .underlyingError, let underlyingError = error.underlyingError {
nsError = underlyingError as NSError
} else {
nsError = error as NSError
}
self.domain = nsError.domain self.domain = nsError.domain
self.code = Int32(nsError.code) self.code = Int32(nsError.code)
self.userInfo = nsError.userInfo self.userInfo = nsError.userInfo
@@ -91,7 +98,7 @@ public extension LoggedError
return app return app
} }
var error: Error { var error: NSError {
let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo) let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo)
return nsError return nsError
} }
@@ -113,6 +120,8 @@ public extension LoggedError
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName) case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName)
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName) case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName)
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName) case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName)
case .connection: return String(format: NSLocalizedString("Connection during %@ Failed", comment: ""), self.appName)
case .enableJIT: return String(format: NSLocalizedString("Enabling JIT for %@ Failed", comment: ""), self.appName)
} }
} }
} }

View File

@@ -44,7 +44,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion } let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself. // Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }), if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestSupportedVersionApp?.latestSupportedVersion == $0 }),
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion }) let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
{ {
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion) secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)

View File

@@ -48,7 +48,7 @@ fileprivate extension NSManagedObject
func setStoreAppLatestVersion(_ appVersion: NSManagedObject) func setStoreAppLatestVersion(_ appVersion: NSManagedObject)
{ {
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestVersion)) self.setValue(appVersion, forKey: #keyPath(StoreApp.latestSupportedVersion))
let versions = NSOrderedSet(array: [appVersion]) let versions = NSOrderedSet(array: [appVersion])
self.setValue(versions, forKey: #keyPath(StoreApp._versions)) self.setValue(versions, forKey: #keyPath(StoreApp._versions))

View File

@@ -146,7 +146,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged @objc(source) public var _source: Source? @NSManaged @objc(source) public var _source: Source?
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet @NSManaged @objc(permissions) public var _permissions: NSOrderedSet
@NSManaged public private(set) var latestVersion: AppVersion? @NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet @NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values. @NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
@@ -169,31 +169,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
return self._versions.array as! [AppVersion] return self._versions.array as! [AppVersion]
} }
@nonobjc public var size: Int64? {
guard let version = self.latestVersion else { return nil }
return version.size
}
@nonobjc public var version: String? {
guard let version = self.latestVersion else { return nil }
return version.version
}
@nonobjc public var versionDescription: String? {
guard let version = self.latestVersion else { return nil }
return version.localizedDescription
}
@nonobjc public var versionDate: Date? {
guard let version = self.latestVersion else { return nil }
return version.date
}
@nonobjc public var downloadURL: URL? {
guard let version = self.latestVersion else { return nil }
return version.downloadURL
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
@@ -314,16 +289,30 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
} }
} }
private extension StoreApp internal extension StoreApp
{ {
func setVersions(_ versions: [AppVersion]) func setVersions(_ versions: [AppVersion])
{ {
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self.latestVersion = latestVersion
self._versions = NSOrderedSet(array: versions) self._versions = NSOrderedSet(array: versions)
let latestSupportedVersion = versions.first(where: { $0.isSupported })
self.latestSupportedVersion = latestSupportedVersion
for case let version as AppVersion in self._versions
{
if version == latestSupportedVersion
{
version.latestSupportedVersionApp = self
}
else
{
// Ensure we replace any previous relationship when merging.
version.latestSupportedVersionApp = nil
}
}
// Preserve backwards compatibility by assigning legacy property values. // Preserve backwards compatibility by assigning legacy property values.
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self._version = latestVersion.version self._version = latestVersion.version
self._versionDate = latestVersion.date self._versionDate = latestVersion.date
self._versionDescription = latestVersion.localizedDescription self._versionDescription = latestVersion.localizedDescription
@@ -334,6 +323,10 @@ private extension StoreApp
public extension StoreApp public extension StoreApp
{ {
var latestAvailableVersion: AppVersion? {
return self._versions.firstObject as? AppVersion
}
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp> @nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
{ {
return NSFetchRequest<StoreApp>(entityName: "StoreApp") return NSFetchRequest<StoreApp>(entityName: "StoreApp")

View File

@@ -40,7 +40,7 @@ extension ALTApplication: AppProtocol
extension StoreApp: AppProtocol extension StoreApp: AppProtocol
{ {
public var url: URL? { public var url: URL? {
return self.downloadURL return self.latestAvailableVersion?.downloadURL
} }
} }
@@ -50,3 +50,17 @@ extension InstalledApp: AppProtocol
return self.fileURL return self.fileURL
} }
} }
extension AppVersion: AppProtocol {
public var name: String {
return self.app?.name ?? self.bundleIdentifier
}
public var bundleIdentifier: String {
return self.appBundleID
}
public var url: URL? {
return self.downloadURL
}
}

View File

@@ -8,9 +8,16 @@
#import "NSError+ALTServerError.h" #import "NSError+ALTServerError.h"
NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer"; #if TARGET_OS_OSX
NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation"; #import "AltServer-Swift.h"
NSErrorDomain const AltServerConnectionErrorDomain = @"com.rileytestut.AltServer.Connection"; #else
#import "AltStoreCore/AltStoreCore-Swift.h"
#endif
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
NSErrorDomain const AltServerInstallationErrorDomain = @"AltServer.InstallationError";
NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError";
NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain"; NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain";
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode"; NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
@@ -24,8 +31,16 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
+ (void)load + (void)load
{ {
[NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) { [NSError alt_setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey]) if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
{
return [error altserver_localizedDescription];
}
else if ([userInfoKey isEqualToString:NSLocalizedFailureErrorKey])
{
return [error altserver_localizedFailure];
}
else if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{ {
return [error altserver_localizedFailureReason]; return [error altserver_localizedFailureReason];
} }
@@ -41,10 +56,10 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return nil; return nil;
}]; }];
[NSError setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) { [NSError alt_setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey]) if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{ {
return [error altserver_connection_localizedDescription]; return [error altserver_connection_localizedFailureReason];
} }
else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey]) else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
{ {
@@ -55,6 +70,53 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
}]; }];
} }
- (nullable NSString *)altserver_localizedDescription
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
// We're wrapping another error, so return the wrapped error's localized description.
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.localizedDescription;
}
default:
return nil;
}
}
- (nullable NSString *)altserver_localizedFailure
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.alt_localizedFailure;
}
case ALTServerErrorConnectionFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError.localizedFailureReason != nil)
{
// Only return localized failure if there is an underlying error with failure reason.
#if TARGET_OS_OSX
return NSLocalizedString(@"There was an error connecting to the device.", @"");
#else
return NSLocalizedString(@"AltServer could not establish a connection to SideStore.", @"");
#endif
}
return nil;
}
default:
return nil;
}
}
- (nullable NSString *)altserver_localizedFailureReason - (nullable NSString *)altserver_localizedFailureReason
{ {
switch ((ALTServerError)self.code) switch ((ALTServerError)self.code)
@@ -80,12 +142,21 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"An unknown error occured.", @""); return NSLocalizedString(@"An unknown error occured.", @"");
case ALTServerErrorConnectionFailed: case ALTServerErrorConnectionFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError.localizedFailureReason != nil)
{
return underlyingError.localizedFailureReason;
}
// Return fallback failure reason if there isn't an underlying error with failure reason.
#if TARGET_OS_OSX #if TARGET_OS_OSX
return NSLocalizedString(@"There was an error connecting to the device.", @""); return NSLocalizedString(@"There was an error connecting to the device.", @"");
#else #else
return NSLocalizedString(@"Could not connect to SideStore.", @""); return NSLocalizedString(@"Could not connect to SideStore.", @"");
#endif #endif
}
case ALTServerErrorLostConnection: case ALTServerErrorLostConnection:
return NSLocalizedString(@"Lost connection to SideStore.", @""); return NSLocalizedString(@"Lost connection to SideStore.", @"");
@@ -93,8 +164,8 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"SideStore could not find this device.", @""); return NSLocalizedString(@"SideStore could not find this device.", @"");
case ALTServerErrorDeviceWriteFailed: case ALTServerErrorDeviceWriteFailed:
return NSLocalizedString(@"Failed to write app data to device.", @""); return NSLocalizedString(@"SideStore could not write data to this device.", @"");
case ALTServerErrorInvalidRequest: case ALTServerErrorInvalidRequest:
return NSLocalizedString(@"SideStore received an invalid request.", @""); return NSLocalizedString(@"SideStore received an invalid request.", @"");
@@ -102,14 +173,20 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"SideStore sent an invalid response.", @""); return NSLocalizedString(@"SideStore sent an invalid response.", @"");
case ALTServerErrorInvalidApp: case ALTServerErrorInvalidApp:
return NSLocalizedString(@"The app is invalid.", @""); return NSLocalizedString(@"The app is in an invalid format.", @"");
case ALTServerErrorInstallationFailed: case ALTServerErrorInstallationFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError != nil) {
return underlyingError.localizedFailureReason ?: underlyingError.localizedDescription;
}
return NSLocalizedString(@"An error occured while installing the app.", @""); return NSLocalizedString(@"An error occured while installing the app.", @"");
}
case ALTServerErrorMaximumFreeAppLimitReached: case ALTServerErrorMaximumFreeAppLimitReached:
return NSLocalizedString(@"Cannot activate more than 3 apps with a non-developer Apple ID.", @""); return NSLocalizedString(@"You cannot activate more than 3 apps with a non-developer Apple ID.", @"");
case ALTServerErrorUnsupportediOSVersion: case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install SideStore.", @""); return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install SideStore.", @"");
@@ -117,8 +194,8 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"SideStore does not support this request.", @""); return NSLocalizedString(@"SideStore does not support this request.", @"");
case ALTServerErrorUnknownResponse: case ALTServerErrorUnknownResponse:
return NSLocalizedString(@"Received an unknown response from SideStore.", @""); return NSLocalizedString(@"SideStore received an unknown response from SideStore.", @"");
case ALTServerErrorInvalidAnisetteData: case ALTServerErrorInvalidAnisetteData:
return NSLocalizedString(@"The provided anisette data is invalid.", @""); return NSLocalizedString(@"The provided anisette data is invalid.", @"");
@@ -153,7 +230,19 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
{ {
switch ((ALTServerError)self.code) switch ((ALTServerError)self.code)
{ {
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.localizedRecoverySuggestion;
}
case ALTServerErrorConnectionFailed: case ALTServerErrorConnectionFailed:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError.localizedRecoverySuggestion != nil){
return underlyingError.localizedRecoverySuggestion;
}
// If there is no underlying error found, fall through to AltServerErrorDeviceNotFound
}
case ALTServerErrorDeviceNotFound: case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"Make sure you have trusted this device with your computer and Wi-Fi sync is enabled.", @""); return NSLocalizedString(@"Make sure you have trusted this device with your computer and Wi-Fi sync is enabled.", @"");
@@ -182,6 +271,13 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
{ {
switch ((ALTServerError)self.code) switch ((ALTServerError)self.code)
{ {
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.alt_localizedDebugDescription;
}
case ALTServerErrorIncompatibleDeveloperDisk: case ALTServerErrorIncompatibleDeveloperDisk:
{ {
NSString *path = self.userInfo[NSFilePathErrorKey]; NSString *path = self.userInfo[NSFilePathErrorKey];
@@ -191,7 +287,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
} }
NSString *osVersion = [self altserver_osVersion] ?: NSLocalizedString(@"this device's OS version", @""); NSString *osVersion = [self altserver_osVersion] ?: NSLocalizedString(@"this device's OS version", @"");
NSString *debugDescription = [NSString stringWithFormat:NSLocalizedString(@"The Developer disk located at\n\n%@\n\nis incompatible with %@.", @""), path, osVersion]; NSString *debugDescription = [NSString stringWithFormat:NSLocalizedString(@"The Developer disk located at %@ is incompatible with %@.", @""), path, osVersion];
return debugDescription; return debugDescription;
} }
@@ -232,7 +328,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
#pragma mark - AltServerConnectionErrorDomain - #pragma mark - AltServerConnectionErrorDomain -
- (nullable NSString *)altserver_connection_localizedDescription - (nullable NSString *)altserver_connection_localizedFailureReason
{ {
switch ((ALTServerConnectionError)self.code) switch ((ALTServerConnectionError)self.code)
{ {

View File

@@ -0,0 +1,182 @@
//
// ALTLocalizedError.swift
// AltStore
//
// Created by Riley Testut on 10/14/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
public let ALTLocalizedTitleErrorKey = "ALTLocalizedTitle"
public let ALTLocalizedDescriptionKey = "ALTLocalizedDescription"
public protocol ALTLocalizedError<Code>: LocalizedError, CustomNSError, CustomStringConvertible
{
associatedtype Code: ALTErrorCode
var code: Code { get }
var errorFailureReason: String { get }
var errorTitle: String? { get set }
var errorFailure: String? { get set }
var sourceFile: String? { get set }
var sourceLine: UInt? { get set }
}
public extension ALTLocalizedError
{
var sourceFile: String? {
get { nil }
set {}
}
var sourceLine: UInt? {
get { nil }
set {}
}
}
public protocol ALTErrorCode: RawRepresentable where RawValue == Int
{
associatedtype Error: ALTLocalizedError where Error.Code == Self
static var errorDomain: String { get } // Optional
}
public protocol ALTErrorEnum: ALTErrorCode
{
associatedtype Error = DefaultLocalizedError<Self>
var errorFailureReason: String { get }
}
/// LocalizedError & CustomNSError & CustomStringConvertible
public extension ALTLocalizedError
{
var errorCode: Int { self.code.rawValue }
var errorDescription: String? {
guard (self as NSError).localizedFailure == nil else {
// Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason.
return nil
}
// Otherwise, return failureReason for localizedDescription to avoid system prepending "Operation Failed" message.
return self.failureReason
}
var failureReason: String? {
return self.errorFailureReason
}
var errorUserInfo: [String : Any] {
let userInfo: [String: Any?] = [
NSLocalizedFailureErrorKey: self.errorFailure,
ALTLocalizedTitleErrorKey: self.errorTitle,
// ALTSourceFileErrorKey: self.sourceFile, // TODO: Figure out where these come from
// ALTSourceLineErrorKey: self.sourceLine,
]
return userInfo.compactMapValues { $0 }
}
var description: String {
let description = "\(self.localizedErrorCode)\(self.localizedDescription)"
return description
}
}
/// Default Implementations
public extension ALTLocalizedError where Code: ALTErrorEnum
{
static var errorDomain: String {
return Code.errorDomain
}
// ALTErrorEnum Codes provide their failure reason directly.
var errorFailureReason: String {
return self.code.errorFailureReason
}
}
/// Default Implementations
public extension ALTErrorCode
{
static var errorDomain: String {
let typeName = String(reflecting: Self.self) // "\(Self.self)" doesn't include module name, but String(reflecting:) does.
let errorDomain = typeName.replacingOccurrences(of: "ErrorCode", with: "Error")
return errorDomain
}
}
public extension ALTLocalizedError
{
// Allows us to initialize errors with localizedTitle + localizedFailure
// while still using the error's custom initializer at callsite.
init(_ error: Self, localizedTitle: String? = nil, localizedFailure: String? = nil)
{
self = error
if let localizedTitle
{
self.errorTitle = localizedTitle
}
if let localizedFailure
{
self.errorFailure = localizedFailure
}
}
}
public struct DefaultLocalizedError<Code: ALTErrorEnum>: ALTLocalizedError
{
public let code: Code
public var errorTitle: String?
public var errorFailure: String?
public var sourceFile: String?
public var sourceLine: UInt?
public init(_ code: Code, localizedTitle: String? = nil, localizedFailure: String? = nil, sourceFile: String? = #fileID, sourceLine: UInt? = #line)
{
self.code = code
self.errorTitle = localizedTitle
self.errorFailure = localizedFailure
self.sourceFile = sourceFile
self.sourceLine = sourceLine
}
}
/// Custom Operators
/// These allow us to pattern match ALTErrorCodes against arbitrary errors via ~ prefix.
prefix operator ~
public prefix func ~<Code: ALTErrorCode>(expression: Code) -> NSError
{
let nsError = NSError(domain: Code.errorDomain, code: expression.rawValue)
return nsError
}
public func ~=(pattern: any Swift.Error, value: any Swift.Error) -> Bool
{
let isMatch = pattern._domain == value._domain && pattern._code == value._code
return isMatch
}
// These operators *should* allow us to match ALTErrorCodes against arbitrary errors,
// but they don't work as of iOS 16.1 and Swift 5.7.
//
//public func ~=<Error: ALTLocalizedError>(pattern: Error, value: Swift.Error) -> Bool
//{
// let isMatch = pattern._domain == value._domain && pattern._code == value._code
// return isMatch
//}
//
//public func ~=<Code: ALTErrorCode>(pattern: Code, value: Swift.Error) -> Bool
//{
// let isMatch = Code.errorDomain == value._domain && pattern.rawValue == value._code
// return isMatch
//}

View File

@@ -0,0 +1,25 @@
//
// ALTWrappedError.h
// AltStoreCore
//
// Created by Riley Testut on 11/28/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
// Overrides localizedDescription to check userInfoValueProvider for failure reason
// instead of default behavior which just returns NSLocalizedFailureErrorKey if present.
//
// Must be written in Objective-C for Swift.Error <-> NSError bridging to work correctly.
@interface ALTWrappedError : NSError
@property (copy, nonatomic) NSError *wrappedError;
- (instancetype)initWithError:(NSError *)error userInfo:(NSDictionary<NSString *, id> *)userInfo;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,73 @@
//
// ALTWrappedError.m
// AltStoreCore
//
// Created by Riley Testut on 11/28/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
#import "ALTWrappedError.h"
@implementation ALTWrappedError
+ (BOOL)supportsSecureCoding
{
// Required in order to serialize errors for legacy AltServer communication.
return YES;
}
- (instancetype)initWithError:(NSError *)error userInfo:(NSDictionary<NSString *,id> *)userInfo
{
self = [super initWithDomain:error.domain code:error.code userInfo:userInfo];
if (self)
{
if ([error isKindOfClass:[ALTWrappedError class]])
{
_wrappedError = [(ALTWrappedError *)error wrappedError];
}
else
{
_wrappedError = [error copy];
}
}
return self;
}
- (NSString *)localizedDescription
{
NSString *localizedFailure = self.userInfo[NSLocalizedFailureErrorKey];
if (localizedFailure != nil)
{
NSString *wrappedLocalizedDescription = self.wrappedError.userInfo[NSLocalizedDescriptionKey];
NSString *localizedFailureReason = wrappedLocalizedDescription ?: self.wrappedError.localizedFailureReason ?: self.wrappedError.localizedDescription;
NSString *localizedDescription = [NSString stringWithFormat:@"%@ %@", localizedFailure, localizedFailureReason];
return localizedDescription;
}
// localizedFailure is nil, so return wrappedError's localizedDescription.
return self.wrappedError.localizedDescription;
}
- (NSString *)localizedFailureReason
{
return self.wrappedError.localizedFailureReason;
}
- (NSString *)localizedRecoverySuggestion
{
return self.wrappedError.localizedRecoverySuggestion;
}
- (NSString *)debugDescription
{
return self.wrappedError.debugDescription;
}
- (NSString *)helpAnchor
{
return self.wrappedError.helpAnchor;
}
@end

View File

@@ -22,12 +22,8 @@ public extension ALTServerError
case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error) case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error)
case let error as NSError: case let error as NSError:
var userInfo = error.userInfo var userInfo = error.userInfo
if !userInfo.keys.contains(NSUnderlyingErrorKey) userInfo[NSUnderlyingErrorKey] = error
{
// Assign underlying error (if there isn't already one).
userInfo[NSUnderlyingErrorKey] = error
}
self = ALTServerError(.underlyingError, userInfo: userInfo) self = ALTServerError(.underlyingError, userInfo: userInfo)
} }
} }

View File

@@ -8,7 +8,17 @@
import Foundation import Foundation
extension NSError #if canImport(UIKit)
import UIKit
public typealias ALTFont = UIFont
#elseif canImport(AppKit)
import AppKit
public typealias ALTFont = NSFont
#endif
import AltSign
public extension NSError
{ {
@objc(alt_localizedFailure) @objc(alt_localizedFailure)
var localizedFailure: String? { var localizedFailure: String? {
@@ -21,52 +31,52 @@ extension NSError
let debugDescription = (self.userInfo[NSDebugDescriptionErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSDebugDescriptionErrorKey) as? String) let debugDescription = (self.userInfo[NSDebugDescriptionErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSDebugDescriptionErrorKey) as? String)
return debugDescription return debugDescription
} }
@objc(alt_localizedTitle)
var localizedTitle: String? {
return self.userInfo[ALTLocalizedTitleErrorKey] as? String
}
@objc(alt_errorWithLocalizedFailure:) @objc(alt_errorWithLocalizedFailure:)
func withLocalizedFailure(_ failure: String) -> NSError func withLocalizedFailure(_ failure: String) -> NSError
{ {
var userInfo = self.userInfo switch self
userInfo[NSLocalizedFailureErrorKey] = failure
if let failureReason = self.localizedFailureReason
{ {
userInfo[NSLocalizedFailureReasonErrorKey] = failureReason case var error as any ALTLocalizedError:
error.errorFailure = failure
return error as NSError
default:
var userInfo = self.userInfo
userInfo[NSLocalizedFailureReasonErrorKey] = failure
return ALTWrappedError(error: self, userInfo: userInfo)
} }
else if self.localizedFailure == nil && self.localizedFailureReason == nil && self.localizedDescription.contains(self.localizedErrorCode) }
@objc(alt_errorWithLocalizedTitle:)
func withLocalizedTitle(_ title: String) -> NSError {
switch self
{ {
// Default localizedDescription, so replace with just the localized error code portion. case var error as any ALTLocalizedError:
userInfo[NSLocalizedFailureReasonErrorKey] = "(\(self.localizedErrorCode).)" error.errorTitle = title
return error as NSError
default:
var userInfo = self.userInfo
userInfo[ALTLocalizedTitleErrorKey] = title
return ALTWrappedError(error: self, userInfo: userInfo)
} }
else
{
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedDescription
}
if let localizedDescription = NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedDescriptionKey) as? String
{
userInfo[NSLocalizedDescriptionKey] = localizedDescription
}
// Don't accidentally remove localizedDescription from dictionary
// userInfo[NSLocalizedDescriptionKey] = NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedDescriptionKey) as? String
if let recoverySuggestion = self.localizedRecoverySuggestion
{
userInfo[NSLocalizedRecoverySuggestionErrorKey] = recoverySuggestion
}
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
} }
func sanitizedForCoreData() -> NSError func sanitizedForSerialization() -> NSError
{ {
var userInfo = self.userInfo var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
// Remove userInfo values that don't conform to NSSecureEncoding. // Remove userInfo values that don't conform to NSSecureEncoding.
userInfo = userInfo.filter { (key, value) in userInfo = userInfo.filter { (key, value) in
return (value as AnyObject) is NSSecureCoding return (value as AnyObject) is NSSecureCoding
@@ -75,69 +85,165 @@ extension NSError
// Sanitize underlying errors. // Sanitize underlying errors.
if let underlyingError = userInfo[NSUnderlyingErrorKey] as? Error if let underlyingError = userInfo[NSUnderlyingErrorKey] as? Error
{ {
let sanitizedError = (underlyingError as NSError).sanitizedForCoreData() let sanitizedError = (underlyingError as NSError).sanitizedForSerialization()
userInfo[NSUnderlyingErrorKey] = sanitizedError userInfo[NSUnderlyingErrorKey] = sanitizedError
} }
if #available(iOS 14.5, macOS 11.3, *), let underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? [Error] if #available(iOS 14.5, macOS 11.3, *), let underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? [Error]
{ {
let sanitizedErrors = underlyingErrors.map { ($0 as NSError).sanitizedForCoreData() } let sanitizedErrors = underlyingErrors.map { ($0 as NSError).sanitizedForSerialization() }
userInfo[NSMultipleUnderlyingErrorsKey] = sanitizedErrors userInfo[NSMultipleUnderlyingErrorsKey] = sanitizedErrors
} }
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo) let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error return error
} }
func formattedDetailedDescription(with font: ALTFont) -> NSAttributedString {
#if canImport(UIKit)
let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font.fontDescriptor
#else
let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
#endif
let boldFont = ALTFont(descriptor: boldFontDescriptor, size: font.pointSize) ?? font
var preferredKeyOrder = [
NSDebugDescriptionErrorKey,
NSLocalizedDescriptionKey,
NSLocalizedFailureErrorKey,
NSLocalizedFailureReasonErrorKey,
NSLocalizedRecoverySuggestionErrorKey,
ALTLocalizedTitleErrorKey,
// ALTSourceFileErrorKey,
// ALTSourceLineErrorKey,
NSUnderlyingErrorKey
]
if #available(iOS 14.5, macOS 11.3, *) {
preferredKeyOrder.append(NSMultipleUnderlyingErrorsKey)
}
var userInfo = self.userInfo
userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
let sortedUserInfo = userInfo.sorted { a, b in
let indexA = preferredKeyOrder.firstIndex(of: a.key)
let indexB = preferredKeyOrder.firstIndex(of: b.key)
switch (indexA, indexB) {
case (let indexA?, let indexB?): return indexA < indexB
case (_?, nil): return true // indexA exists, indexB is nil, indexA should come first
case (nil, _?): return false // indexB exists, indexB is nil, indexB should come first
case (nil, nil): return a.key < b.key // both nil, so sort alphabetically
}
}
let detailedDescription = NSMutableAttributedString()
for (key, value) in sortedUserInfo
{
let keyName: String
switch key
{
case NSDebugDescriptionErrorKey: keyName = NSLocalizedString("Debug Description", comment: "")
case NSLocalizedDescriptionKey: keyName = NSLocalizedString("Error Description", comment: "")
case NSLocalizedFailureErrorKey: keyName = NSLocalizedString("Failure", comment: "")
case NSLocalizedFailureReasonErrorKey: keyName = NSLocalizedString("Failure Reason", comment: "")
case NSLocalizedRecoverySuggestionErrorKey: keyName = NSLocalizedString("Recovery Suggestion", comment: "")
case ALTLocalizedTitleErrorKey: keyName = NSLocalizedString("Title", comment: "")
// case ALTSourceFileErrorKey: keyName = NSLocalizedString("Source File", comment: "")
// case ALTSourceLineErrorKey: keyName = NSLocalizedString("Source Line", comment: "")
case NSUnderlyingErrorKey: keyName = NSLocalizedString("Underlying Error", comment: "")
default:
if #available(iOS 14.5, macOS 11.3, *), key == NSMultipleUnderlyingErrorsKey
{
keyName = NSLocalizedString("Underlying Errors", comment: "")
}
else
{
keyName = key
}
}
let attributedKey = NSAttributedString(string: keyName, attributes: [.font: boldFont])
let attributedValue = NSAttributedString(string: String(describing: value), attributes: [.font: font])
let attributedString = NSMutableAttributedString(attributedString: attributedKey)
attributedString.mutableString.append("\n")
attributedString.append(attributedValue)
if !detailedDescription.string.isEmpty
{
detailedDescription.mutableString.append("\n\n")
}
detailedDescription.append(attributedString)
}
// Support dark mode
#if canImport(UIKit)
if #available(iOS 13, *)
{
detailedDescription.addAttribute(.foregroundColor, value: UIColor.label, range: NSMakeRange(0, detailedDescription.length))
}
#else
detailedDescription.addAttribute(.foregroundColor, value: NSColor.labelColor, range: NSMakeRange(0, detailedDescription.length))
#endif
return detailedDescription
}
} }
extension Error public extension NSError
{
typealias UserInfoProvider = (Error, String) -> Any?
@objc
class func alt_setUserInfoValueProvider(forDomain domain: String, provider: UserInfoProvider?) {
NSError.setUserInfoValueProvider(forDomain: domain) { error, key in
let nsError = error as NSError
switch key{
case NSLocalizedDescriptionKey:
if nsError.localizedFailure != nil {
// Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason
return nil
} else if let localizedDescription = provider?(error, NSLocalizedDescriptionKey) as? String {
// Only call provider() if there is no localizedFailure
return localizedDescription
}
/* Otherwise return failureReason for localizedDescription to avoid system prepending "Operation Failed" message
Do NOT return provider(NSLocalizedFailureReason) which might be unexpectedly nil if unrecognized error code. */
return nsError.localizedFailureReason
default:
return provider?(error, key)
}
}
}
}
public extension Error
{ {
var underlyingError: Error? { var underlyingError: Error? {
let underlyingError = (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error return (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
return underlyingError
} }
var localizedErrorCode: String { var localizedErrorCode: String {
let localizedErrorCode = String(format: NSLocalizedString("%@ error %@", comment: ""), (self as NSError).domain, (self as NSError).code as NSNumber) return String(format: NSLocalizedString("%@ %@", comment: ""), (self as NSError).domain, self.displayCode as NSNumber)
return localizedErrorCode
} }
}
protocol ALTLocalizedError: LocalizedError, CustomNSError var displayCode: Int {
{ guard let serverError = self as? ALTServerError else {
var failure: String? { get } return (self as NSError).code
}
var underlyingError: Error? { get }
}
extension ALTLocalizedError /* We want AltServerError codes to start at 2000, but we can't change them without breaking AltServer compatibility.
{ Instead we just add 2000 when displaying code to the user to make it appear as if the codes start at 2000 anyway.
var errorUserInfo: [String : Any] { */
let userInfo = ([ return 2000 + serverError.code.rawValue
NSLocalizedDescriptionKey: self.errorDescription,
NSLocalizedFailureReasonErrorKey: self.failureReason,
NSLocalizedFailureErrorKey: self.failure,
NSUnderlyingErrorKey: self.underlyingError
] as [String: Any?]).compactMapValues { $0 }
return userInfo
} }
var underlyingError: Error? {
// Error's default implementation calls errorUserInfo,
// but ALTLocalizedError.errorUserInfo calls underlyingError.
// Return nil to prevent infinite recursion.
return nil
}
var errorDescription: String? {
guard let errorFailure = self.failure else { return (self.underlyingError as NSError?)?.localizedDescription }
guard let failureReason = self.failureReason else { return errorFailure }
let errorDescription = errorFailure + " " + failureReason
return errorDescription
}
var failureReason: String? { (self.underlyingError as NSError?)?.localizedDescription }
var recoverySuggestion: String? { (self.underlyingError as NSError?)?.localizedRecoverySuggestion }
var helpAnchor: String? { (self.underlyingError as NSError?)?.helpAnchor }
} }

View File

@@ -0,0 +1,249 @@
//
// CodableError.swift
// AltKit
//
// Created by Riley Testut on 3/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
private extension ErrorUserInfoKey
{
static let altLocalizedDescription: String = "ALTLocalizedDescription"
static let altLocalizedFailureReason: String = "ALTLocalizedFailureReason"
static let altLocalizedRecoverySuggestion: String = "ALTLocalizedRecoverySuggestion"
static let altDebugDescription: String = "ALTDebugDescription"
}
extension CodableError
{
enum UserInfoValue: Codable
{
case unknown
case string(String)
case number(Int)
case error(NSError)
case codableError(CodableError)
indirect case array([UserInfoValue])
indirect case dictionary([String: UserInfoValue])
var value: Any? {
switch self
{
case .unknown: return nil
case .string(let string): return string
case .number(let number): return number
case .error(let error): return error
case .codableError(let error): return error.error
case .array(let array): return array.compactMap { $0.value } // .compactMap instead of .map to ensure nil values are removed.
case .dictionary(let dictionary): return dictionary.compactMapValues { $0.value } // .compactMapValues instead of .mapValues to ensure nil values are removed.
}
}
var codableValue: Codable? {
switch self
{
case .unknown, .string, .number: return self.value as? Codable
case .codableError(let error): return error
case .error(let nsError):
// Ignore error because we don't want to fail completely if error contains invalid user info value.
let sanitizedError = nsError.sanitizedForSerialization()
let data = try? NSKeyedArchiver.archivedData(withRootObject: sanitizedError, requiringSecureCoding: true)
return data
case .array(let array): return array
case .dictionary(let dictionary): return dictionary
}
}
init(_ rawValue: Any?)
{
switch rawValue
{
case let string as String: self = .string(string)
case let number as Int: self = .number(number)
case let error as NSError: self = .codableError(CodableError(error: error))
case let array as [Any]: self = .array(array.compactMap(UserInfoValue.init))
case let dictionary as [String: Any]: self = .dictionary(dictionary.compactMapValues(UserInfoValue.init))
default: self = .unknown
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.singleValueContainer()
if
let data = try? container.decode(Data.self),
let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
{
self = .error(error)
}
else if let codableError = try? container.decode(CodableError.self)
{
self = .codableError(codableError)
}
else if let string = try? container.decode(String.self)
{
self = .string(string)
}
else if let number = try? container.decode(Int.self)
{
self = .number(number)
}
else if let array = try? container.decode([UserInfoValue].self)
{
self = .array(array)
}
else if let dictionary = try? container.decode([String: UserInfoValue].self)
{
self = .dictionary(dictionary)
}
else
{
self = .unknown
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.singleValueContainer()
if let value = self.codableValue
{
try container.encode(value)
}
else
{
try container.encodeNil()
}
}
}
}
struct CodableError: Codable
{
var error: Error {
return self.rawError ?? NSError(domain: self.errorDomain, code: self.errorCode, userInfo: self.userInfo ?? [:])
}
private var rawError: Error?
private var errorDomain: String
private var errorCode: Int
private var userInfo: [String: Any]?
private enum CodingKeys: String, CodingKey
{
case errorDomain
case errorCode
case legacyUserInfo = "userInfo"
case errorUserInfo
}
init(error: Error)
{
self.rawError = error
let nsError = error as NSError
self.errorDomain = nsError.domain
self.errorCode = nsError.code
if !nsError.userInfo.isEmpty
{
self.userInfo = nsError.userInfo
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
// Assume ALTServerError.errorDomain if no explicit domain provided.
self.errorDomain = try container.decodeIfPresent(String.self, forKey: .errorDomain) ?? ALTServerError.errorDomain
self.errorCode = try container.decode(Int.self, forKey: .errorCode)
if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .errorUserInfo)
{
// Attempt decoding from .errorUserInfo first, because it will gracefully handle unknown user info values.
// Copy ALTLocalized... values to NSLocalized... if provider is nil or if error is unrecognized.
// This ensures we preserve error messages if receiving an unknown error.
var userInfo = rawUserInfo.compactMapValues { $0.value }
// Recognized == the provider returns value for NSLocalizedFailureReasonErrorKey, or error is ALTServerError.underlyingError.
let provider = NSError.userInfoValueProvider(forDomain: self.errorDomain)
let isRecognizedError = (
provider?(self.error, NSLocalizedFailureReasonErrorKey) != nil ||
(self.error._domain == ALTServerError.errorDomain && self.error._code == ALTServerError.underlyingError.rawValue)
)
if !isRecognizedError
{
// Error not recognized, so copy over NSLocalizedDescriptionKey and NSLocalizedFailureReasonErrorKey.
userInfo[NSLocalizedDescriptionKey] = userInfo[ErrorUserInfoKey.altLocalizedDescription]
userInfo[NSLocalizedFailureReasonErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedFailureReason]
}
// Copy over NSLocalizedRecoverySuggestionErrorKey and NSDebugDescriptionErrorKey if provider returns nil.
if provider?(self.error, NSLocalizedRecoverySuggestionErrorKey) == nil
{
userInfo[NSLocalizedRecoverySuggestionErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion]
}
if provider?(self.error, NSDebugDescriptionErrorKey) == nil
{
userInfo[NSDebugDescriptionErrorKey] = userInfo[ErrorUserInfoKey.altDebugDescription]
}
userInfo[ErrorUserInfoKey.altLocalizedDescription] = nil
userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nil
userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nil
userInfo[ErrorUserInfoKey.altDebugDescription] = nil
self.userInfo = userInfo
}
else if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .legacyUserInfo)
{
// Fall back to decoding .legacyUserInfo, which only supports String and NSError values.
let userInfo = rawUserInfo.compactMapValues { $0.value }
self.userInfo = userInfo
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.errorDomain, forKey: .errorDomain)
try container.encode(self.errorCode, forKey: .errorCode)
let rawLegacyUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
// .legacyUserInfo only supports String and NSError values.
switch value
{
case let string as String: return .string(string)
case let error as NSError: return .error(error) // Must use .error, not .codableError for backwards compatibility.
default: return nil
}
}
try container.encodeIfPresent(rawLegacyUserInfo, forKey: .legacyUserInfo)
let nsError = self.error as NSError
var userInfo = self.userInfo ?? [:]
userInfo[ErrorUserInfoKey.altLocalizedDescription] = nsError.localizedDescription
userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nsError.localizedFailureReason
userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nsError.localizedRecoverySuggestion
userInfo[ErrorUserInfoKey.altDebugDescription] = nsError.localizedDebugDescription
// No need to use alternate key. This is a no-op if userInfo already contains localizedFailure,
// but it caches the UserInfoProvider value if one exists.
userInfo[NSLocalizedFailureErrorKey] = nsError.localizedFailure
let rawUserInfo = userInfo.compactMapValues { UserInfoValue($0) }
try container.encodeIfPresent(rawUserInfo, forKey: .errorUserInfo)
}
}

View File

@@ -1,126 +0,0 @@
//
// CodableServerError.swift
// AltKit
//
// Created by Riley Testut on 3/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
extension CodableServerError
{
enum UserInfoValue: Codable
{
case string(String)
case error(NSError)
public init(from decoder: Decoder) throws
{
let container = try decoder.singleValueContainer()
if
let data = try? container.decode(Data.self),
let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
{
self = .error(error)
}
else if let string = try? container.decode(String.self)
{
self = .string(string)
}
else
{
throw DecodingError.dataCorruptedError(in: container, debugDescription: "UserInfoValue value cannot be decoded")
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.singleValueContainer()
switch self
{
case .string(let string): try container.encode(string)
case .error(let error):
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: error, requiringSecureCoding: true) else {
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "UserInfoValue value \(self) cannot be encoded")
throw EncodingError.invalidValue(self, context)
}
try container.encode(data)
}
}
}
}
struct CodableServerError: Codable
{
var error: ALTServerError {
return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
}
private var errorCode: ALTServerError.Code
private var userInfo: [String: Any]?
private enum CodingKeys: String, CodingKey
{
case errorCode
case userInfo
}
init(error: ALTServerError)
{
self.errorCode = error.code
var userInfo = error.userInfo
if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion
{
userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
}
if !userInfo.isEmpty
{
self.userInfo = userInfo
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let errorCode = try container.decode(Int.self, forKey: .errorCode)
self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .userInfo)
let userInfo = rawUserInfo?.mapValues { (value) -> Any in
switch value
{
case .string(let string): return string
case .error(let error): return error
}
}
self.userInfo = userInfo
}
func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.error.code.rawValue, forKey: .errorCode)
let rawUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
switch value
{
case let string as String: return .string(string)
case let error as NSError: return .error(error)
default: return nil
}
}
try container.encodeIfPresent(rawUserInfo, forKey: .userInfo)
}
}

View File

@@ -197,20 +197,20 @@ public enum ServerResponse: Decodable
// from easily changing response format for a request in the future. // from easily changing response format for a request in the future.
public struct ErrorResponse: ServerMessageProtocol public struct ErrorResponse: ServerMessageProtocol
{ {
public var version = 2 public var version = 3
public var identifier = "ErrorResponse" public var identifier = "ErrorResponse"
public var error: ALTServerError { public var error: ALTServerError {
return self.serverError?.error ?? ALTServerError(self.errorCode) return self.serverError.map { ALTServerError($0.error) } ?? ALTServerError(self.errorCode)
} }
private var serverError: CodableServerError? private var serverError: CodableError?
// Legacy (v1) // Legacy (v1)
private var errorCode: ALTServerError.Code private var errorCode: ALTServerError.Code
public init(error: ALTServerError) public init(error: ALTServerError)
{ {
self.serverError = CodableServerError(error: error) self.serverError = CodableError(error: error)
self.errorCode = error.code self.errorCode = error.code
} }
} }