Merge pull request #202 from SideStore/pullrequests/jawshoeadan/develop

Merge all of Riley's new error handling stuff REBASED
This commit is contained in:
Joe Mattiello
2022-12-30 17:10:45 -05:00
committed by GitHub
46 changed files with 2421 additions and 252 deletions

View File

@@ -23,6 +23,9 @@
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; };
B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; }; B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; };
B3146ED3284F581E00BBC3FD /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3146ED3284F581E00BBC3FD /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3146ECD284F580500BBC3FD /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F894295F7F9B002B1159 /* libfragmentzip.a */; };
B33FFBAA295F8F78002259E6 /* preboard.c in Sources */ = {isa = PBXBuildFile; fileRef = B33FFBA9295F8F78002259E6 /* preboard.c */; };
B33FFBAC295F8F98002259E6 /* companion_proxy.c in Sources */ = {isa = PBXBuildFile; fileRef = B33FFBAB295F8F98002259E6 /* companion_proxy.c */; };
B343F858295F6331002B1159 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F84C295F6321002B1159 /* libminimuxer_static.a */; }; B343F858295F6331002B1159 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F84C295F6321002B1159 /* libminimuxer_static.a */; };
B343F859295F6335002B1159 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F853295F6323002B1159 /* libem_proxy_static.a */; }; B343F859295F6335002B1159 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F853295F6323002B1159 /* libem_proxy_static.a */; };
B343F86D295F759E002B1159 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F86C295F759E002B1159 /* libresolv.tbd */; }; B343F86D295F759E002B1159 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F86C295F759E002B1159 /* libresolv.tbd */; };
@@ -35,9 +38,7 @@
B343F883295F7C5D002B1159 /* thread.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F879295F7C5D002B1159 /* thread.c */; }; B343F883295F7C5D002B1159 /* thread.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F879295F7C5D002B1159 /* thread.c */; };
B343F884295F7C5D002B1159 /* utils.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F87A295F7C5D002B1159 /* utils.c */; }; B343F884295F7C5D002B1159 /* utils.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F87A295F7C5D002B1159 /* utils.c */; };
B343F885295F7C5D002B1159 /* tlv.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F87B295F7C5D002B1159 /* tlv.c */; }; B343F885295F7C5D002B1159 /* tlv.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F87B295F7C5D002B1159 /* tlv.c */; };
B343F895295F7FAC002B1159 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F894295F7F9B002B1159 /* libfragmentzip.a */; };
B376FE3E29258C8900E18883 /* OSLog+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */; }; B376FE3E29258C8900E18883 /* OSLog+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */; };
B3919A52292DBE5400519575 /* ProgressRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D504F42528AD72C50014BB5D /* ProgressRing.swift */; };
B39575F5284F29E20080B4FF /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B39575F4284F29E20080B4FF /* Roxas.framework */; }; B39575F5284F29E20080B4FF /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B39575F4284F29E20080B4FF /* Roxas.framework */; };
B39F16132918D7C5002E9404 /* Consts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39F16122918D7C5002E9404 /* Consts.swift */; }; B39F16132918D7C5002E9404 /* Consts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39F16122918D7C5002E9404 /* Consts.swift */; };
B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39F16142918D7DA002E9404 /* Consts+Proxy.swift */; }; B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39F16142918D7DA002E9404 /* Consts+Proxy.swift */; };
@@ -320,13 +321,17 @@
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */; }; BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */; };
BFF435D8255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */; }; BFF435D8255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */; };
BFF615A82510042B00484D3B /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; }; BFF615A82510042B00484D3B /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; };
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; };
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; settings = {ATTRIBUTES = (Weak, ); }; }; D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; }; D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; };
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; };
D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; }; D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; };
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; }; D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; }; D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; };
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; }; D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; };
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */; }; D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */; };
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */; };
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; };
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; }; D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; };
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; }; D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; };
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; }; D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
@@ -334,6 +339,8 @@
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; }; D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */; }; D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */; };
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; }; D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; };
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; };
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -506,6 +513,8 @@
1920B04E2924AC8300744F60 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; }; 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>"; };
B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = "<group>"; }; B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = "<group>"; };
B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = "<group>"; };
B33FFBAB295F8F98002259E6 /* companion_proxy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = companion_proxy.c; path = src/companion_proxy.c; sourceTree = "<group>"; };
B343F847295F6321002B1159 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = minimuxer.xcodeproj; path = Dependencies/minimuxer.xcodeproj; sourceTree = SOURCE_ROOT; }; B343F847295F6321002B1159 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = minimuxer.xcodeproj; path = Dependencies/minimuxer.xcodeproj; sourceTree = SOURCE_ROOT; };
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = em_proxy.xcodeproj; path = Dependencies/em_proxy.xcodeproj; sourceTree = SOURCE_ROOT; }; B343F84D295F6323002B1159 /* em_proxy.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = em_proxy.xcodeproj; path = Dependencies/em_proxy.xcodeproj; sourceTree = SOURCE_ROOT; };
B343F86C295F759E002B1159 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk/usr/lib/libresolv.tbd; sourceTree = DEVELOPER_DIR; }; B343F86C295F759E002B1159 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk/usr/lib/libresolv.tbd; sourceTree = DEVELOPER_DIR; };
@@ -814,17 +823,21 @@
BFF7C906257844C900E55F36 /* AltXPCProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltXPCProtocol.h; sourceTree = "<group>"; }; BFF7C906257844C900E55F36 /* AltXPCProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltXPCProtocol.h; sourceTree = "<group>"; };
BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = "<group>"; }; BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = "<group>"; };
BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = "<group>"; }; BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = "<group>"; };
D504F42528AD72C50014BB5D /* ProgressRing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressRing.swift; sourceTree = "<group>"; }; D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = "<group>"; };
D533E8B62727841800A9B5DD /* libAppleArchive.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAppleArchive.tbd; path = usr/lib/libAppleArchive.tbd; sourceTree = SDKROOT; }; D533E8B62727841800A9B5DD /* libAppleArchive.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAppleArchive.tbd; path = usr/lib/libAppleArchive.tbd; sourceTree = SDKROOT; };
D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = "<group>"; }; D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = "<group>"; };
D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; }; D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; };
D533E8BD2727BBF800A9B5DD /* libcurl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcurl.a; path = Dependencies/libcurl/libcurl.a; sourceTree = SOURCE_ROOT; }; D533E8BD2727BBF800A9B5DD /* libcurl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcurl.a; path = Dependencies/libcurl/libcurl.a; sourceTree = SOURCE_ROOT; };
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; }; D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; }; D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = "<group>"; }; D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = "<group>"; };
D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = "<group>"; }; D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = "<group>"; };
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = "<group>"; }; D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = "<group>"; };
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = "<group>"; }; D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = "<group>"; };
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogViewController.swift; sourceTree = "<group>"; };
D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = "<group>"; };
D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = "<group>"; }; D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = "<group>"; };
D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = "<group>"; }; D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = "<group>"; };
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 10.xcdatamodel"; sourceTree = "<group>"; }; D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 10.xcdatamodel"; sourceTree = "<group>"; };
@@ -833,6 +846,8 @@
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; }; D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTrustedSourcesOperation.swift; sourceTree = "<group>"; }; D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTrustedSourcesOperation.swift; sourceTree = "<group>"; };
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; }; D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; };
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; };
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -898,7 +913,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
B343F895295F7FAC002B1159 /* libfragmentzip.a in Frameworks */, B33FFBA8295F8E98002259E6 /* libfragmentzip.a in Frameworks */,
191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */, 191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */,
191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */, 191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */,
19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */, 19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */,
@@ -965,6 +980,13 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B33FFB8F295F8CF2002259E6 /* Recovered References */ = {
isa = PBXGroup;
children = (
);
name = "Recovered References";
sourceTree = "<group>";
};
B343F848295F6321002B1159 /* Products */ = { B343F848295F6321002B1159 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1084,6 +1106,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF4587D72298D3A800BD7491 /* afc.c */, BF4587D72298D3A800BD7491 /* afc.c */,
B33FFBAB295F8F98002259E6 /* companion_proxy.c */,
BF4587DF2298D3A900BD7491 /* debugserver.c */, BF4587DF2298D3A900BD7491 /* debugserver.c */,
BF4587E42298D3A900BD7491 /* device_link_service.c */, BF4587E42298D3A900BD7491 /* device_link_service.c */,
BF4587C92298D3A800BD7491 /* diagnostics_relay.c */, BF4587C92298D3A800BD7491 /* diagnostics_relay.c */,
@@ -1100,6 +1123,7 @@
BF4587DC2298D3A900BD7491 /* mobilebackup2.c */, BF4587DC2298D3A900BD7491 /* mobilebackup2.c */,
BF4587F32298D3AA00BD7491 /* mobilesync.c */, BF4587F32298D3AA00BD7491 /* mobilesync.c */,
BF4587CB2298D3A800BD7491 /* notification_proxy.c */, BF4587CB2298D3A800BD7491 /* notification_proxy.c */,
B33FFBA9295F8F78002259E6 /* preboard.c */,
BF4587F42298D3AA00BD7491 /* property_list_service.c */, BF4587F42298D3AA00BD7491 /* property_list_service.c */,
BF4587E62298D3A900BD7491 /* restore.c */, BF4587E62298D3A900BD7491 /* restore.c */,
BF4587CC2298D3A800BD7491 /* sbservices.c */, BF4587CC2298D3A800BD7491 /* sbservices.c */,
@@ -1293,9 +1317,11 @@
BF66EEC92501AECA007EE018 /* Account.swift */, BF66EEC92501AECA007EE018 /* Account.swift */,
BF66EEC72501AECA007EE018 /* AppID.swift */, BF66EEC72501AECA007EE018 /* AppID.swift */,
BF66EEC62501AECA007EE018 /* AppPermission.swift */, BF66EEC62501AECA007EE018 /* AppPermission.swift */,
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */,
BF66EECA2501AECA007EE018 /* DatabaseManager.swift */, BF66EECA2501AECA007EE018 /* DatabaseManager.swift */,
BF66EEC02501AECA007EE018 /* InstalledApp.swift */, BF66EEC02501AECA007EE018 /* InstalledApp.swift */,
BF66EECB2501AECA007EE018 /* InstalledExtension.swift */, BF66EECB2501AECA007EE018 /* InstalledExtension.swift */,
D58916FD28C7C55C00E39C8B /* LoggedError.swift */,
BF66EEC52501AECA007EE018 /* MergePolicy.swift */, BF66EEC52501AECA007EE018 /* MergePolicy.swift */,
BF66EEBF2501AECA007EE018 /* NewsItem.swift */, BF66EEBF2501AECA007EE018 /* NewsItem.swift */,
BF66EEC82501AECA007EE018 /* PatreonAccount.swift */, BF66EEC82501AECA007EE018 /* PatreonAccount.swift */,
@@ -1323,6 +1349,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF66EEAE2501AECA007EE018 /* StoreAppPolicy.swift */, BF66EEAE2501AECA007EE018 /* StoreAppPolicy.swift */,
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */,
BF66EEAF2501AECA007EE018 /* InstalledAppPolicy.swift */, BF66EEAF2501AECA007EE018 /* InstalledAppPolicy.swift */,
); );
path = Policies; path = Policies;
@@ -1339,6 +1366,7 @@
BF66EEB62501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel */, BF66EEB62501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel */,
BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */, BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */,
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */, D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */,
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */,
); );
path = "Mapping Models"; path = "Mapping Models";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1397,7 +1425,6 @@
BF42345825101C1D006D1EB2 /* WidgetView.swift */, BF42345825101C1D006D1EB2 /* WidgetView.swift */,
BF98917C250AAC4F002ACF50 /* Countdown.swift */, BF98917C250AAC4F002ACF50 /* Countdown.swift */,
D55E163528776CB000A627A1 /* ComplicationView.swift */, D55E163528776CB000A627A1 /* ComplicationView.swift */,
D504F42528AD72C50014BB5D /* ProgressRing.swift */,
BF989170250AABF4002ACF50 /* Assets.xcassets */, BF989170250AABF4002ACF50 /* Assets.xcassets */,
BF989172250AABF4002ACF50 /* Info.plist */, BF989172250AABF4002ACF50 /* Info.plist */,
); );
@@ -1486,6 +1513,7 @@
BFD247852284BB3300981D42 /* Frameworks */, BFD247852284BB3300981D42 /* Frameworks */,
B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */, B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */,
BFD2476B2284B9A500981D42 /* Products */, BFD2476B2284B9A500981D42 /* Products */,
B33FFB8F295F8CF2002259E6 /* Recovered References */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -1626,6 +1654,7 @@
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */, BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */,
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */, BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */,
BFF0B695232242D3007A79E1 /* LicensesViewController.swift */, BFF0B695232242D3007A79E1 /* LicensesViewController.swift */,
D589170128C7D93500E39C8B /* Error Log */,
B3EE16B52925E27D00B3B1F5 /* AnisetteManager.swift */, B3EE16B52925E27D00B3B1F5 /* AnisetteManager.swift */,
); );
path = Settings; path = Settings;
@@ -1727,6 +1756,15 @@
path = XPC; path = XPC;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D589170128C7D93500E39C8B /* Error Log */ = {
isa = PBXGroup;
children = (
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
);
path = "Error Log";
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */ /* Begin PBXHeadersBuildPhase section */
@@ -2260,6 +2298,8 @@
B343F881295F7C5D002B1159 /* termcolors.c in Sources */, B343F881295F7C5D002B1159 /* termcolors.c in Sources */,
B343F87E295F7C5D002B1159 /* collection.c in Sources */, B343F87E295F7C5D002B1159 /* collection.c in Sources */,
BFD52C0F22A1A9CB000B7ED1 /* Real.cpp in Sources */, BFD52C0F22A1A9CB000B7ED1 /* Real.cpp in Sources */,
B33FFBAA295F8F78002259E6 /* preboard.c in Sources */,
B33FFBAC295F8F98002259E6 /* companion_proxy.c in Sources */,
BF4587FB2298D3AB00BD7491 /* notification_proxy.c in Sources */, BF4587FB2298D3AB00BD7491 /* notification_proxy.c in Sources */,
BF4588352298D3C100BD7491 /* userpref.c in Sources */, BF4588352298D3C100BD7491 /* userpref.c in Sources */,
BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */, BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */,
@@ -2333,11 +2373,13 @@
BF66EECF2501AECA007EE018 /* AltStoreToAltStore2.xcmappingmodel in Sources */, BF66EECF2501AECA007EE018 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
BF66EEA82501AEC5007EE018 /* Patron.swift in Sources */, BF66EEA82501AEC5007EE018 /* Patron.swift in Sources */,
BF66EEDD2501AECA007EE018 /* AppPermission.swift in Sources */, BF66EEDD2501AECA007EE018 /* AppPermission.swift in Sources */,
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */,
BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */, BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel 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 */,
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */, BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */, BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */,
BF66EEEB2501AED0007EE018 /* UIApplication+AppExtension.swift in Sources */, BF66EEEB2501AED0007EE018 /* UIApplication+AppExtension.swift in Sources */,
BF66EED92501AECA007EE018 /* Team.swift in Sources */, BF66EED92501AECA007EE018 /* Team.swift in Sources */,
@@ -2348,10 +2390,12 @@
BF66EEE02501AECA007EE018 /* Account.swift in Sources */, BF66EEE02501AECA007EE018 /* Account.swift in Sources */,
BF66EED52501AECA007EE018 /* AltStore.xcdatamodeld in Sources */, BF66EED52501AECA007EE018 /* AltStore.xcdatamodeld in Sources */,
BFAECC582501B0A400528F27 /* ALTConstants.m in Sources */, BFAECC582501B0A400528F27 /* ALTConstants.m in Sources */,
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */, BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */, BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */, BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */, BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */,
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */,
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 */,
@@ -2376,7 +2420,6 @@
BF42345A25101C35006D1EB2 /* WidgetView.swift in Sources */, BF42345A25101C35006D1EB2 /* WidgetView.swift in Sources */,
D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */, D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */,
BF98917F250AAC4F002ACF50 /* AltWidget.swift in Sources */, BF98917F250AAC4F002ACF50 /* AltWidget.swift in Sources */,
B3919A52292DBE5400519575 /* ProgressRing.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -2397,6 +2440,7 @@
BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */, BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */,
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */, D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */,
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */, BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */, BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
@@ -2446,6 +2490,7 @@
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */, BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */, BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */, BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */, BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */, D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */,
@@ -3398,6 +3443,7 @@
BF66EEB72501AECA007EE018 /* AltStore.xcdatamodeld */ = { BF66EEB72501AECA007EE018 /* AltStore.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;
children = ( children = (
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */,
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */, D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */,
BFBF33142526754700B7B8C9 /* AltStore 9.xcdatamodel */, BFBF33142526754700B7B8C9 /* AltStore 9.xcdatamodel */,
BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */, BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */,
@@ -3409,7 +3455,7 @@
BF66EEBD2501AECA007EE018 /* AltStore 2.xcdatamodel */, BF66EEBD2501AECA007EE018 /* AltStore 2.xcdatamodel */,
BF66EEBE2501AECA007EE018 /* AltStore 4.xcdatamodel */, BF66EEBE2501AECA007EE018 /* AltStore 4.xcdatamodel */,
); );
currentVersion = D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */; currentVersion = D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */;
path = AltStore.xcdatamodeld; path = AltStore.xcdatamodeld;
sourceTree = "<group>"; sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel; versionGroupType = wrapper.xcdatamodel;

View File

@@ -14,13 +14,7 @@ import AppCenter
import AppCenterAnalytics import AppCenterAnalytics
import AppCenterCrashes import AppCenterCrashes
#if DEBUG private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
#elseif RELEASE
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
#else
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
#endif
extension AnalyticsManager extension AnalyticsManager
{ {

View File

@@ -80,10 +80,21 @@ 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
self.versionDescriptionTextView.text = self.app.versionDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version) if let version = self.app.latestVersion
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter) {
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size)) self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
}
else
{
self.versionDescriptionTextView.text = nil
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
}
self.descriptionTextView.maximumNumberOfLines = 5 self.descriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)

View File

@@ -384,10 +384,10 @@ private extension AppViewController
button.progress = progress button.progress = progress
} }
if Date() < self.app.versionDate if let versionDate = self.app.latestVersion?.date, versionDate > Date()
{ {
self.bannerView.button.countdownDate = self.app.versionDate self.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = self.app.versionDate self.navigationBarDownloadButton.countdownDate = versionDate
} }
else else
{ {

View File

@@ -33,34 +33,34 @@ extension AppDelegate
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
@available(iOS 14, *) @available(iOS 14, *)
private var intentHandler: IntentHandler { private var intentHandler: IntentHandler {
get { _intentHandler as! IntentHandler } get { _intentHandler as! IntentHandler }
set { _intentHandler = newValue } set { _intentHandler = newValue }
} }
@available(iOS 14, *) @available(iOS 14, *)
private var viewAppIntentHandler: ViewAppIntentHandler { private var viewAppIntentHandler: ViewAppIntentHandler {
get { _viewAppIntentHandler as! ViewAppIntentHandler } get { _viewAppIntentHandler as! ViewAppIntentHandler }
set { _viewAppIntentHandler = newValue } set { _viewAppIntentHandler = newValue }
} }
private lazy var _intentHandler: Any = { private lazy var _intentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() } guard #available(iOS 14, *) else { fatalError() }
return IntentHandler() return IntentHandler()
}() }()
private lazy var _viewAppIntentHandler: Any = { private lazy var _viewAppIntentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() } guard #available(iOS 14, *) else { fatalError() }
return ViewAppIntentHandler() return ViewAppIntentHandler()
}() }()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{ {
// Register default settings before doing anything else. // Register default settings before doing anything else.
UserDefaults.registerDefaults() UserDefaults.registerDefaults()
DatabaseManager.shared.start { (error) in DatabaseManager.shared.start { (error) in
if let error = error if let error = error
{ {
@@ -71,50 +71,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
print("Started DatabaseManager.") print("Started DatabaseManager.")
} }
} }
AnalyticsManager.shared.start() AnalyticsManager.shared.start()
self.setTintColor() self.setTintColor()
SecureValueTransformer.register() SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil if UserDefaults.standard.firstLaunch == nil
{ {
Keychain.shared.reset() Keychain.shared.reset()
UserDefaults.standard.firstLaunch = Date() UserDefaults.standard.firstLaunch = Date()
} }
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
#if DEBUG || BETA #if DEBUG || BETA
UserDefaults.standard.isDebugModeEnabled = true UserDefaults.standard.isDebugModeEnabled = true
#endif #endif
self.prepareForBackgroundFetch() self.prepareForBackgroundFetch()
return true return true
} }
func applicationDidEnterBackground(_ application: UIApplication) func applicationDidEnterBackground(_ application: UIApplication)
{ {
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
switch result
{
case .success: break
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
}
}
} }
func applicationWillEnterForeground(_ application: UIApplication) func applicationWillEnterForeground(_ application: UIApplication)
{ {
AppManager.shared.update() AppManager.shared.update()
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
{ {
return self.open(url) return self.open(url)
} }
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
{ {
guard #available(iOS 14, *) else { return nil } guard #available(iOS 14, *) else { return nil }
switch intent switch intent
{ {
case is RefreshAllIntent: return self.intentHandler case is RefreshAllIntent: return self.intentHandler
@@ -133,7 +145,7 @@ extension AppDelegate
// Use this method to select a configuration to create the new scene with. // Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
} }
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{ {
// Called when the user discards a scene session. // Called when the user discards a scene session.
@@ -148,36 +160,36 @@ private extension AppDelegate
{ {
self.window?.tintColor = .altPrimary self.window?.tintColor = .altPrimary
} }
func open(_ url: URL) -> Bool func open(_ url: URL) -> Bool
{ {
if url.isFileURL if url.isFileURL
{ {
guard url.pathExtension.lowercased() == "ipa" else { return false } guard url.pathExtension.lowercased() == "ipa" else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url]) NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
} }
return true return true
} }
else else
{ {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host?.lowercased() else { return false } guard let host = components.host?.lowercased() else { return false }
switch host switch host
{ {
case "patreon": case "patreon":
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
} }
return true return true
case "appbackupresponse": case "appbackupresponse":
let result: Result<Void, Error> let result: Result<Void, Error>
switch url.path.lowercased() switch url.path.lowercased()
{ {
case "/success": result = .success(()) case "/success": result = .success(())
@@ -188,37 +200,37 @@ private extension AppDelegate
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString), let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
let errorDescription = queryItems["errorDescription"] let errorDescription = queryItems["errorDescription"]
else { return false } else { return false }
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription]) let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
result = .failure(error) result = .failure(error)
default: return false default: return false
} }
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result]) NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
return true return true
case "install": case "install":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:] let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false } guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL]) NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
} }
return true return true
case "source": case "source":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:] let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false } guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL]) NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
} }
return true return true
default: return false default: return false
} }
} }
@@ -231,47 +243,47 @@ extension AppDelegate
{ {
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery). // "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60) UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
} }
#if DEBUG #if DEBUG
UIApplication.shared.registerForRemoteNotifications() UIApplication.shared.registerForRemoteNotifications()
#endif #endif
} }
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
{ {
let tokenParts = deviceToken.map { data -> String in let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data) return String(format: "%02.2hhx", data)
} }
let token = tokenParts.joined() let token = tokenParts.joined()
print("Push Token:", token) print("Push Token:", token)
} }
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{ {
self.application(application, performFetchWithCompletionHandler: completionHandler) self.application(application, performFetchWithCompletionHandler: completionHandler)
} }
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{ {
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
{ {
let threeHours: TimeInterval = 3 * 60 * 60 let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("App Refresh Tip", comment: "") content.title = NSLocalizedString("App Refresh Tip", comment: "")
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "") content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger) let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
UserDefaults.standard.presentedLaunchReminderNotification = true UserDefaults.standard.presentedLaunchReminderNotification = true
} }
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
if let error = taskResult.error if let error = taskResult.error
{ {
@@ -280,7 +292,7 @@ extension AppDelegate
taskCompletionHandler() taskCompletionHandler()
return return
} }
if !DatabaseManager.shared.isStarted if !DatabaseManager.shared.isStarted
{ {
DatabaseManager.shared.start() { (error) in DatabaseManager.shared.start() { (error) in
@@ -309,7 +321,7 @@ extension AppDelegate
} }
} }
} }
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void, func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
{ {
@@ -319,15 +331,15 @@ extension AppDelegate
case .failure: backgroundFetchCompletionHandler(.failed) case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData) case .success: backgroundFetchCompletionHandler(.newData)
} }
if !UserDefaults.standard.isBackgroundRefreshEnabled if !UserDefaults.standard.isBackgroundRefreshEnabled
{ {
refreshAppsCompletionHandler(.success([:])) refreshAppsCompletionHandler(.success([:]))
} }
} }
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return } guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context) let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler) AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
@@ -343,49 +355,49 @@ private extension AppDelegate
do do
{ {
let (sources, context) = try result.get() let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult> let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false previousUpdatesFetchRequest.includesPendingChanges = false
previousUpdatesFetchRequest.resultType = .dictionaryResultType previousUpdatesFetchRequest.resultType = .dictionaryResultType
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)] previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult> let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
previousNewsItemsFetchRequest.includesPendingChanges = false previousNewsItemsFetchRequest.includesPendingChanges = false
previousNewsItemsFetchRequest.resultType = .dictionaryResultType previousNewsItemsFetchRequest.resultType = .dictionaryResultType
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)] previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]] let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]] let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
try context.save() try context.save()
let updatesFetchRequest = InstalledApp.updatesFetchRequest() let updatesFetchRequest = InstalledApp.updatesFetchRequest()
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem> let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
let updates = try context.fetch(updatesFetchRequest) let updates = try context.fetch(updatesFetchRequest)
let newsItems = try context.fetch(newsItemsFetchRequest) let newsItems = try context.fetch(newsItemsFetchRequest)
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 else { continue } guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "") content.title = NSLocalizedString("New Update Available", comment: "")
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version) content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
content.sound = .default content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
for newsItem in newsItems for newsItem in newsItems
{ {
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue } guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
guard !newsItem.isSilent else { continue } guard !newsItem.isSilent else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
if let app = newsItem.storeApp if let app = newsItem.storeApp
{ {
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name) content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
@@ -394,10 +406,10 @@ private extension AppDelegate
{ {
content.title = NSLocalizedString("SideStore News", comment: "") content.title = NSLocalizedString("SideStore News", comment: "")
} }
content.body = newsItem.title content.body = newsItem.title
content.sound = .default content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
@@ -405,7 +417,7 @@ private extension AppDelegate
DispatchQueue.main.async { DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count UIApplication.shared.applicationIconBadgeNumber = updates.count
} }
completionHandler(.success(sources)) completionHandler(.success(sources))
} }
catch catch

View File

@@ -113,7 +113,7 @@ 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 Date() < app.versionDate if let versionDate = app.latestVersion?.date, versionDate > Date()
{ {
cell.bannerView.button.countdownDate = app.versionDate cell.bannerView.button.countdownDate = app.versionDate
} }

View File

@@ -71,8 +71,10 @@ class CollapsingTextView: UITextView
{ {
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines) let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if self.intrinsicContentSize.height > maximumCollapsedHeight let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{ {
var exclusionFrame = moreButtonFrame var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY exclusionFrame.origin.y += self.moreButton.bounds.midY

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ALTAnisetteURL</key>
<string>http://ani.sidestore.io/</string>
<key>ALTAppGroups</key> <key>ALTAppGroups</key>
<array> <array>
<string>group.$(APP_GROUP_IDENTIFIER)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
@@ -9,12 +11,10 @@
</array> </array>
<key>ALTDeviceID</key> <key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string> <string>00008101-000129D63698001E</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key> <key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string> <string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key> <key>ALTServerID</key>
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string> <string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
@@ -93,6 +93,11 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_altserver._tcp</string> <string>_altserver._tcp</string>
@@ -133,11 +138,6 @@
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>

View File

@@ -86,6 +86,7 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data)) types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
types.append(.xml) types.append(.xml)
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types) let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil) self.present(documentPickerController, animated: true, completion: nil)
}) })
@@ -172,7 +173,19 @@ extension LaunchViewController
{ {
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "") let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) let errorDescription: String
if #available(iOS 14.5, *)
{
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
errorDescription = errorMessages.joined(separator: "\n\n")
}
else
{
errorDescription = error.debugDescription
}
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
self.handleLaunchConditions() self.handleLaunchConditions()
})) }))

View File

@@ -1121,7 +1121,7 @@ private extension AppManager
presentingViewController?.dismiss(animated: true, completion: nil) presentingViewController?.dismiss(animated: true, completion: nil)
} }
} }
presentingViewController.present(navigationController, animated: true, completion: nil) presentingViewController.present(navigationController, animated: true, completion: nil)
} }
} }
catch catch
@@ -1223,7 +1223,7 @@ private extension AppManager
let progress = Progress.discreteProgress(totalUnitCount: 100) let progress = Progress.discreteProgress(totalUnitCount: 100)
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context) let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.url) context.app = ALTApplication(fileURL: app.fileURL)
/* Fetch Provisioning Profiles */ /* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context) let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
@@ -1662,13 +1662,8 @@ private extension AppManager
} }
if #available(iOS 14, *) if #available(iOS 14, *)
{ {
WidgetCenter.shared.getCurrentConfigurations { (result) in WidgetCenter.shared.reloadAllTimelines()
guard case .success(let widgets) = result else { return }
guard let widget = widgets.first(where: { $0.configuration is ViewAppIntent }) else { return }
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
}
} }
do { try installedApp.managedObjectContext?.save() } do { try installedApp.managedObjectContext?.save() }
@@ -1677,6 +1672,8 @@ private extension AppManager
catch catch
{ {
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier) group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, for: operation)
} }
} }
@@ -1701,6 +1698,43 @@ 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

@@ -186,7 +186,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?.versionDate, ascending: true), fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)] NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
@@ -195,7 +195,7 @@ 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 else { return } guard let app = installedApp.storeApp, let latestVersion = app.latestVersion 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
@@ -209,7 +209,7 @@ private extension MyAppsViewController
cell.bannerView.configure(for: app) cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter) let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate cell.bannerView.subtitleLabel.text = versionDate
let appName: String let appName: String
@@ -223,7 +223,7 @@ private extension MyAppsViewController
appName = app.name appName = app.name
} }
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, app.version, versionDate) cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.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)

View File

@@ -391,7 +391,7 @@ 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 Date() < storeApp.versionDate if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
{ {
footerView.bannerView.button.countdownDate = storeApp.versionDate footerView.bannerView.button.countdownDate = storeApp.versionDate
} }

View File

@@ -36,7 +36,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
let context: AppOperationContext let context: AppOperationContext
private let bundleIdentifier: String private let bundleIdentifier: String
private let sourceURL: URL private var sourceURL: URL?
private let destinationURL: URL private let destinationURL: URL
private let session = URLSession(configuration: .default) private let session = URLSession(configuration: .default)
@@ -69,7 +69,9 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
print("Downloading App:", self.bundleIdentifier) print("Downloading App:", self.bundleIdentifier)
self.downloadApp(from: self.sourceURL) { result in guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
self.downloadApp(from: sourceURL) { result in
do do
{ {
let application = try result.get() let application = try result.get()
@@ -165,7 +167,7 @@ private extension DownloadAppOperation
} }
} }
if self.sourceURL.isFileURL if sourceURL.isFileURL
{ {
finishOperation(.success(sourceURL)) finishOperation(.success(sourceURL))

View File

@@ -47,6 +47,7 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"] let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"]
if let anisette = ALTAnisetteData(json: formattedJSON) { if let anisette = ALTAnisetteData(json: formattedJSON) {
DLOG("Anisette used: %@", formattedJSON)
self.finish(.success(anisette)) self.finish(.success(anisette))
} }
} }

View File

@@ -86,6 +86,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
let resignedBundleID = appExtension.bundleIdentifier let resignedBundleID = appExtension.bundleIdentifier
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID) let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
print("`parentBundleID`: \(parentBundleID)")
print("`resignedParentBundleID`: \(resignedParentBundleID)")
print("`resignedBundleID`: \(resignedBundleID)")
print("`originalBundleID`: \(originalBundleID)")
let installedExtension: InstalledExtension let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID }) if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })

View File

@@ -11,6 +11,8 @@ import AltSign
enum OperationError: LocalizedError enum OperationError: LocalizedError
{ {
static let domain = OperationError.unknown._domain
case unknown case unknown
case unknownResult case unknownResult
case cancelled case cancelled

View File

@@ -39,9 +39,10 @@ class SendAppOperation: ResultOperation<()>
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) } guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa. // self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url) let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
let fileURL = InstalledApp.refreshedIPAURL(for: app) let fileURL = InstalledApp.refreshedIPAURL(for: app)
print("AFC App `fileURL`: \(fileURL.absoluteString)")
let ns_bundle = NSString(string: app.bundleIdentifier) let ns_bundle = NSString(string: app.bundleIdentifier)
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String) let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
@@ -59,6 +60,7 @@ class SendAppOperation: ResultOperation<()>
attempts -= 1 attempts -= 1
} }
if res == 0 { if res == 0 {
print("minimuxer_yeet_app_afc `res` == \(res)")
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
self.finish(.success(())) self.finish(.success(()))
} else { } else {

View File

@@ -36,14 +36,14 @@
"bundleIdentifier": "com.rileytestut.AltStore.Beta", "bundleIdentifier": "com.rileytestut.AltStore.Beta",
"developerName": "Riley Testut", "developerName": "Riley Testut",
"subtitle": "An alternative App Store for iOS.", "subtitle": "An alternative App Store for iOS.",
"version": "1.5.1b", "version": "1.6b2",
"versionDate": "2022-05-27T12:00:00-07:00", "versionDate": "2022-09-21T13:00:00-05:00",
"versionDescription": "This beta fixes the following issues:\n\n• Using Apple IDs that contain capital letters\n• Using Apple IDs with 2FA enabled without any trusted devices\n• Repeatedly asking some users to sign in every refresh\n• \"Incorrect Apple ID or password\" error after changing Apple ID email address\n• “Application is missing application-identifier” error when sideloading or (de-)activating certain apps", "versionDescription": "• Fixed “error migrating persistent store” issue on launch\n\nPREVIOUS VERSION\n\nLock Screen Widget (iOS 16+)\n• Counts down days until AltStore expires\n• Comes in 2 different styles: “icon” and “text”\n\nError Log\n• View past errors in more detail\n• Tap an error to copy the error message or error code\n• Search for error code directly in AltStore FAQ",
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_5_1_b.ipa", "downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_6_b2.ipa",
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.", "localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.",
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png", "iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084", "tintColor": "018084",
"size": 5464776, "size": 5465933,
"beta": true, "beta": true,
"screenshotURLs": [ "screenshotURLs": [
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG", "https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",

View File

@@ -53,6 +53,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate
guard UIApplication.shared.applicationState == .background else { return } guard UIApplication.shared.applicationState == .background else { return }
// Make sure to update AppDelegate.applicationDidEnterBackground() as well.
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
switch result
{
case .success: break
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
}
}
} }

View File

@@ -0,0 +1,52 @@
//
// ErrorLogTableViewCell.swift
// AltStore
//
// Created by Riley Testut on 9/9/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import UIKit
@objc(ErrorLogTableViewCell)
class ErrorLogTableViewCell: UITableViewCell
{
@IBOutlet var appIconImageView: AppIconImageView!
@IBOutlet var dateLabel: UILabel!
@IBOutlet var errorFailureLabel: UILabel!
@IBOutlet var errorCodeLabel: UILabel!
@IBOutlet var errorDescriptionTextView: CollapsingTextView!
@IBOutlet var menuButton: UIButton!
private var didLayoutSubviews = false
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
let moreButtonFrame = self.convert(self.errorDescriptionTextView.moreButton.frame, from: self.errorDescriptionTextView)
guard moreButtonFrame.contains(point) else { return super.hitTest(point, with: event) }
// Pass touches through menuButton so user can press moreButton.
return self.errorDescriptionTextView.moreButton
}
override func layoutSubviews()
{
super.layoutSubviews()
self.didLayoutSubviews = true
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{
if !self.didLayoutSubviews
{
// Ensure cell is laid out so it will report correct size.
self.layoutIfNeeded()
}
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
return size
}
}

View File

@@ -0,0 +1,301 @@
//
// ErrorLogViewController.swift
// AltStore
//
// Created by Riley Testut on 9/6/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import UIKit
import SafariServices
import AltStoreCore
import Roxas
import Nuke
class ErrorLogViewController: UITableViewController
{
private lazy var dataSource = self.makeDataSource()
private var expandedErrorIDs = Set<NSManagedObjectID>()
private lazy var timeFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .short
return dateFormatter
}()
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
}
}
private extension ErrorLogViewController
{
func makeDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<LoggedError, UIImage>
{
let fetchRequest = LoggedError.fetchRequest() as NSFetchRequest<LoggedError>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LoggedError.date, ascending: false)]
fetchRequest.returnsObjectsAsFaults = false
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(LoggedError.localizedDateString), cacheName: nil)
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<LoggedError, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.proxy = self
dataSource.rowAnimation = .fade
dataSource.cellConfigurationHandler = { [weak self] (cell, loggedError, indexPath) in
guard let self else { return }
let cell = cell as! ErrorLogTableViewCell
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
switch loggedError.domain
{
case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code))
case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code))
default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode
}
let nsError = loggedError.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
cell.errorDescriptionTextView.text = errorDescription
cell.errorDescriptionTextView.maximumNumberOfLines = 5
cell.errorDescriptionTextView.isCollapsed = !self.expandedErrorIDs.contains(loggedError.objectID)
cell.errorDescriptionTextView.moreButton.addTarget(self, action: #selector(ErrorLogViewController.toggleCollapsingCell(_:)), for: .primaryActionTriggered)
cell.appIconImageView.image = nil
cell.appIconImageView.isIndicatingActivity = true
cell.appIconImageView.layer.borderColor = UIColor.gray.cgColor
let displayScale = (self.traitCollection.displayScale == 0.0) ? 1.0 : self.traitCollection.displayScale // 0.0 == "unspecified"
cell.appIconImageView.layer.borderWidth = 1.0 / displayScale
if #available(iOS 14, *)
{
let menu = UIMenu(title: "", children: [
UIAction(title: NSLocalizedString("Copy Error Message", comment: ""), image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in
self?.copyErrorMessage(for: loggedError)
},
UIAction(title: NSLocalizedString("Copy Error Code", comment: ""), image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in
self?.copyErrorCode(for: loggedError)
},
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
self?.searchFAQ(for: loggedError)
}
])
cell.menuButton.menu = menu
}
// 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: ". ")
// Group all paragraphs together into single accessibility element (otherwise, each paragraph is independently selectable).
cell.errorDescriptionTextView.accessibilityLabel = cell.errorDescriptionTextView.text
}
dataSource.prefetchHandler = { (loggedError, indexPath, completion) in
RSTAsyncBlockOperation { (operation) in
loggedError.managedObjectContext?.perform {
if let installedApp = loggedError.installedApp
{
installedApp.loadIcon { (result) in
switch result
{
case .failure(let error): completion(nil, error)
case .success(let image): completion(image, nil)
}
}
}
else if let storeApp = loggedError.storeApp
{
ImagePipeline.shared.loadImage(with: storeApp.iconURL, progress: nil) { (response, error) in
guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image
{
completion(image, nil)
}
else
{
completion(nil, error)
}
}
}
else
{
completion(nil, nil)
}
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ErrorLogTableViewCell
cell.appIconImageView.image = image
cell.appIconImageView.isIndicatingActivity = false
}
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.text = NSLocalizedString("No Errors", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("Errors that occur when sideloading or refreshing apps will appear here.", comment: "")
dataSource.placeholderView = placeholderView
return dataSource
}
}
private extension ErrorLogViewController
{
@IBAction func toggleCollapsingCell(_ sender: UIButton)
{
let point = self.tableView.convert(sender.center, from: sender.superview)
guard let indexPath = self.tableView.indexPathForRow(at: point), let cell = self.tableView.cellForRow(at: indexPath) as? ErrorLogTableViewCell else { return }
let loggedError = self.dataSource.item(at: indexPath)
if cell.errorDescriptionTextView.isCollapsed
{
self.expandedErrorIDs.remove(loggedError.objectID)
}
else
{
self.expandedErrorIDs.insert(loggedError.objectID)
}
self.tableView.performBatchUpdates {
cell.layoutIfNeeded()
}
}
@IBAction func clearLoggedErrors(_ sender: UIBarButtonItem)
{
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet)
alertController.popoverPresentationController?.barButtonItem = sender
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Clear Error Log", comment: ""), style: .destructive) { _ in
self.clearLoggedErrors()
})
self.present(alertController, animated: true)
}
func clearLoggedErrors()
{
DatabaseManager.shared.purgeLoggedErrors { result in
do
{
try result.get()
}
catch
{
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Failed to Clear Error Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true)
}
}
}
}
func copyErrorMessage(for loggedError: LoggedError)
{
let nsError = loggedError.error as NSError
let errorMessage = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
UIPasteboard.general.string = errorMessage
}
func copyErrorCode(for loggedError: LoggedError)
{
let errorCode = loggedError.error.localizedErrorCode
UIPasteboard.general.string = errorCode
}
func searchFAQ(for loggedError: LoggedError)
{
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+")
components.queryItems = [URLQueryItem(name: "q", value: query)]
let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true)
}
}
extension ErrorLogViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let loggedError = self.dataSource.item(at: indexPath)
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
tableView.deselectRow(at: indexPath, animated: true)
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Copy Error Message", comment: ""), style: .default) { [weak self] _ in
self?.copyErrorMessage(for: loggedError)
tableView.deselectRow(at: indexPath, animated: true)
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Copy Error Code", comment: ""), style: .default) { [weak self] _ in
self?.copyErrorCode(for: loggedError)
tableView.deselectRow(at: indexPath, animated: true)
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Search FAQ", comment: ""), style: .default) { [weak self] _ in
self?.searchFAQ(for: loggedError)
tableView.deselectRow(at: indexPath, animated: true)
})
self.present(alertController, animated: true)
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let deleteAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { _, _, completion in
let loggedError = self.dataSource.item(at: indexPath)
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
do
{
let loggedError = context.object(with: loggedError.objectID) as! LoggedError
context.delete(loggedError)
try context.save()
completion(true)
}
catch
{
print("[ALTLog] Failed to delete LoggedError \(loggedError.objectID):", error)
completion(false)
}
}
}
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
let indexPath = IndexPath(row: 0, section: section)
let loggedError = self.dataSource.item(at: indexPath)
if Calendar.current.isDateInToday(loggedError.date)
{
return NSLocalizedString("Today", comment: "")
}
else
{
return loggedError.localizedDateString
}
}
}

View File

@@ -238,7 +238,8 @@ private extension PatreonViewController
@objc func didUpdatePatrons(_ notification: Notification) @objc func didUpdatePatrons(_ notification: Notification)
{ {
DispatchQueue.main.async { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// Wait short delay before reloading or else footer won't properly update if it's already visible 🤷
self.collectionView.reloadData() self.collectionView.reloadData()
} }
} }
@@ -270,17 +271,27 @@ extension PatreonViewController
footerView.button.isHidden = false footerView.button.isHidden = false
//footerView.button.addTarget(self, action: #selector(PatreonViewController.fetchPatrons), for: .primaryActionTriggered) //footerView.button.addTarget(self, action: #selector(PatreonViewController.fetchPatrons), for: .primaryActionTriggered)
if self.patronsDataSource.itemCount > 0 switch AppManager.shared.updatePatronsResult
{ {
footerView.button.isHidden = true case .none: footerView.button.isIndicatingActivity = true
} case .success?: footerView.button.isHidden = true
else case .failure?:
{ #if DEBUG
switch AppManager.shared.updatePatronsResult let debug = true
#else
let debug = false
#endif
if self.patronsDataSource.itemCount == 0 || debug
{ {
case .none: footerView.button.isIndicatingActivity = true // Only show error message if there aren't any cached Patrons (or if this is a debug build).
case .success?: footerView.button.isHidden = true
case .failure?: footerView.button.setTitle(NSLocalizedString("Error Loading Patrons", comment: ""), for: .normal) footerView.button.isHidden = false
footerView.button.setTitle(NSLocalizedString("Error Loading Patrons", comment: ""), for: .normal)
}
else
{
footerView.button.isHidden = true
} }
} }

View File

@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17503.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<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="17502"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@@ -540,7 +541,7 @@
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/> <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="2"/>
</userDefinedRuntimeAttribute> </userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/> <userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
@@ -548,6 +549,42 @@
<segue destination="GBh-rB-juy" kind="show" identifier="showRefreshAttempts" id="K2i-nF-6qa"/> <segue destination="GBh-rB-juy" kind="show" identifier="showRefreshAttempts" id="K2i-nF-6qa"/>
</connections> </connections>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="972" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb">
<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" ambiguous="YES" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PWC-OG-5jx">
<rect key="frame" x="30" y="15.5" width="119" 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" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="VfB-c5-5wG">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="PWC-OG-5jx" firstAttribute="leading" secondItem="qIT-rz-ZUb" secondAttribute="leadingMargin" id="BQr-Nx-fIq"/>
<constraint firstItem="PWC-OG-5jx" firstAttribute="centerY" secondItem="qIT-rz-ZUb" secondAttribute="centerY" id="IDa-ov-tmK"/>
<constraint firstAttribute="trailingMargin" secondItem="VfB-c5-5wG" secondAttribute="trailing" id="Q6c-iP-6bi"/>
<constraint firstItem="VfB-c5-5wG" firstAttribute="centerY" secondItem="qIT-rz-ZUb" secondAttribute="centerY" id="WuL-ax-fFw"/>
</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>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
<connections>
<segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="SSW-vL-86I"/>
</connections>
</tableViewCell>
</cells> </cells>
</tableViewSection> </tableViewSection>
</sections> </sections>
@@ -842,6 +879,123 @@ Settings by i cons from the Noun Project</string>
</objects> </objects>
<point key="canvasLocation" x="1697" y="-199"/> <point key="canvasLocation" x="1697" y="-199"/>
</scene> </scene>
<!--Error Log-->
<scene sceneID="Htu-2V-dbE">
<objects>
<tableViewController id="g8a-Rf-zWa" customClass="ErrorLogViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="BBn-tI-e0e">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="HAm-mA-O78" customClass="ErrorLogTableViewCell">
<rect key="frame" x="16" y="55.5" width="343" height="107.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="HAm-mA-O78" id="swa-et-rfA">
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="mtw-JM-T70">
<rect key="frame" x="16" y="11" width="311" height="85.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="bjU-TX-4lm" userLabel="Compact">
<rect key="frame" x="0.0" y="0.0" width="311" height="43.5"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sDZ-ZN-NT1" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="43" height="43"/>
<constraints>
<constraint firstAttribute="width" secondItem="sDZ-ZN-NT1" secondAttribute="height" multiplier="1:1" id="M8a-Wh-6wd"/>
<constraint firstAttribute="width" constant="43" id="dgV-jc-GET"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="82d-v0-RCp">
<rect key="frame" x="51" y="0.0" width="260" height="39"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Q2j-Tc-bp2">
<rect key="frame" x="0.0" y="0.0" width="260" height="18"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Success" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Na7-uj-XYZ">
<rect key="frame" x="0.0" y="0.0" width="60" height="18"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SGf-pP-RL0">
<rect key="frame" x="229.5" y="0.0" width="30.5" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Error Code" textAlignment="natural" lineBreakMode="headTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="R5a-wv-xHd">
<rect key="frame" x="0.0" y="22" width="260" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
</stackView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Error Description" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1df-ri-hKN" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="51.5" width="311" height="34"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES"/>
<bool key="isElement" value="NO"/>
</accessibility>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5">
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="NO"/>
</accessibility>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title=" "/>
</button>
</subviews>
<constraints>
<constraint firstItem="ba2-EY-tf5" firstAttribute="leading" secondItem="swa-et-rfA" secondAttribute="leading" id="70b-ce-vg1"/>
<constraint firstItem="ba2-EY-tf5" firstAttribute="top" secondItem="swa-et-rfA" secondAttribute="top" id="98S-pF-R8J"/>
<constraint firstAttribute="bottomMargin" secondItem="mtw-JM-T70" secondAttribute="bottom" id="fmH-Tj-9iY"/>
<constraint firstAttribute="trailingMargin" secondItem="mtw-JM-T70" secondAttribute="trailing" id="fu1-uu-mXb"/>
<constraint firstAttribute="bottom" secondItem="ba2-EY-tf5" secondAttribute="bottom" id="kvX-B0-v1x"/>
<constraint firstAttribute="leadingMargin" secondItem="mtw-JM-T70" secondAttribute="leading" id="mIY-lM-64i"/>
<constraint firstAttribute="trailing" secondItem="ba2-EY-tf5" secondAttribute="trailing" id="qnx-qR-VAH"/>
<constraint firstAttribute="topMargin" secondItem="mtw-JM-T70" secondAttribute="top" id="wOS-QI-w47"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="appIconImageView" destination="sDZ-ZN-NT1" id="5Fb-6X-vNV"/>
<outlet property="dateLabel" destination="SGf-pP-RL0" id="jCW-Ib-QGv"/>
<outlet property="errorCodeLabel" destination="R5a-wv-xHd" id="AeF-Yh-OVe"/>
<outlet property="errorDescriptionTextView" destination="1df-ri-hKN" id="s4Z-Id-iS8"/>
<outlet property="errorFailureLabel" destination="Na7-uj-XYZ" id="c6r-XP-oIL"/>
<outlet property="menuButton" destination="ba2-EY-tf5" id="GjL-NZ-KuX"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="g8a-Rf-zWa" id="3Tb-tm-jjW"/>
<outlet property="delegate" destination="g8a-Rf-zWa" id="mbI-k7-Dqq"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Error Log" largeTitleDisplayMode="never" id="a1p-3W-bSi">
<barButtonItem key="rightBarButtonItem" systemItem="trash" id="BnQ-Eh-1gC">
<connections>
<action selector="clearLoggedErrors:" destination="g8a-Rf-zWa" id="faq-89-H5j"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1697" y="1774"/>
</scene>
</scenes> </scenes>
<resources> <resources>
<image name="Next" width="18" height="18"/> <image name="Next" width="18" height="18"/>
@@ -852,5 +1006,8 @@ Settings by i cons from the Noun Project</string>
<systemColor name="darkTextColor"> <systemColor name="darkTextColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>
<systemColor name="labelColor">
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources> </resources>
</document> </document>

View File

@@ -52,6 +52,7 @@ extension SettingsViewController
{ {
case sendFeedback case sendFeedback
case refreshAttempts case refreshAttempts
case errorLog
} }
} }
@@ -502,7 +503,7 @@ extension SettingsViewController
toastView.show(in: self) toastView.show(in: self)
} }
case .refreshAttempts: break case .refreshAttempts, .errorLog: break
} }
default: break default: break

View File

@@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>AltStore 10.xcdatamodel</string> <string>AltStore 11.xcdatamodel</string>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
<attribute name="appBundleID" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceID" optional="YES" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="appBundleID"/>
<constraint value="version"/>
<constraint value="sourceID"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
<attribute name="appBundleID" attributeType="String"/>
<attribute name="appName" attributeType="String"/>
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="operation" optional="YES" attributeType="String"/>
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View File

@@ -0,0 +1,116 @@
//
// AppVersion.swift
// AltStoreCore
//
// Created by Riley Testut on 8/18/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import CoreData
@objc(AppVersion)
public class AppVersion: NSManagedObject, Decodable, Fetchable
{
/* Properties */
@NSManaged public var version: String
@NSManaged public var date: Date
@NSManaged public var localizedDescription: String?
@NSManaged public var downloadURL: URL
@NSManaged public var size: Int64
@nonobjc public var minOSVersion: OperatingSystemVersion? {
guard let osVersionString = self._minOSVersion else { return nil }
let osVersion = OperatingSystemVersion(string: osVersionString)
return osVersion
}
@NSManaged @objc(minOSVersion) private var _minOSVersion: String?
@nonobjc public var maxOSVersion: OperatingSystemVersion? {
guard let osVersionString = self._maxOSVersion else { return nil }
let osVersion = OperatingSystemVersion(string: osVersionString)
return osVersion
}
@NSManaged @objc(maxOSVersion) private var _maxOSVersion: String?
@NSManaged public var appBundleID: String
@NSManaged public var sourceID: String?
/* Relationships */
@NSManaged public private(set) var app: StoreApp?
@NSManaged public private(set) var latestVersionApp: StoreApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey
{
case version
case date
case localizedDescription
case downloadURL
case size
}
public required init(from decoder: Decoder) throws
{
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: AppVersion.entity(), insertInto: context)
do
{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.date = try container.decode(Date.self, forKey: .date)
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
self.size = try container.decode(Int64.self, forKey: .size)
}
catch
{
if let context = self.managedObjectContext
{
context.delete(self)
}
throw error
}
}
}
public extension AppVersion
{
@nonobjc class func fetchRequest() -> NSFetchRequest<AppVersion>
{
return NSFetchRequest<AppVersion>(entityName: "AppVersion")
}
class func makeAppVersion(
version: String,
date: Date,
localizedDescription: String? = nil,
downloadURL: URL,
size: Int64,
appBundleID: String,
sourceID: String? = nil,
in context: NSManagedObjectContext) -> AppVersion
{
let appVersion = AppVersion(context: context)
appVersion.version = version
appVersion.date = date
appVersion.localizedDescription = localizedDescription
appVersion.downloadURL = downloadURL
appVersion.size = size
appVersion.appBundleID = appBundleID
appVersion.sourceID = sourceID
return appVersion
}
}

View File

@@ -11,6 +11,15 @@ import CoreData
import AltSign import AltSign
import Roxas import Roxas
extension CFNotificationName
{
fileprivate static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString)
}
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
DatabaseManager.shared.receivedWillAccessDatabaseNotification()
}
fileprivate class PersistentContainer: RSTPersistentContainer fileprivate class PersistentContainer: RSTPersistentContainer
{ {
override class func defaultDirectoryURL() -> URL override class func defaultDirectoryURL() -> URL
@@ -43,10 +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 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()
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately)
} }
} }
@@ -73,6 +87,10 @@ 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.
self.ignoreWillAccessDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true)
self.migrateDatabaseToAppGroupIfNeeded { (result) in self.migrateDatabaseToAppGroupIfNeeded { (result) in
switch result switch result
{ {
@@ -122,6 +140,27 @@ public extension DatabaseManager
} }
} }
} }
func purgeLoggedErrors(before date: Date? = nil, completion: @escaping (Result<Void, Error>) -> Void)
{
self.persistentContainer.performBackgroundTask { context in
do
{
let predicate = date.map { NSPredicate(format: "%K <= %@", #keyPath(LoggedError.date), $0 as NSDate) }
let loggedErrors = LoggedError.all(satisfying: predicate, in: context, requestProperties: [\.returnsObjectsAsFaults: true])
loggedErrors.forEach { context.delete($0) }
try context.save()
completion(.success(()))
}
catch
{
completion(.failure(error))
}
}
}
} }
public extension DatabaseManager public extension DatabaseManager
@@ -129,10 +168,7 @@ public extension DatabaseManager
var viewContext: NSManagedObjectContext { var viewContext: NSManagedObjectContext {
return self.persistentContainer.viewContext return self.persistentContainer.viewContext
} }
}
public extension DatabaseManager
{
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account? func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
{ {
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount)) let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
@@ -193,7 +229,7 @@ private extension DatabaseManager
else else
{ {
storeApp = StoreApp.makeAltStoreApp(in: context) storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.version = localApp.version storeApp.latestVersion?.version = localApp.version
storeApp.source = altStoreSource storeApp.source = altStoreSource
} }
@@ -380,4 +416,14 @@ private extension DatabaseManager
} }
} }
} }
func receivedWillAccessDatabaseNotification()
{
defer { self.ignoreWillAccessDatabaseNotification = false }
// Ignore notifications sent by the current process.
guard !self.ignoreWillAccessDatabaseNotification else { return }
exit(104)
}
} }

View File

@@ -53,6 +53,8 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
@NSManaged public var team: Team? @NSManaged public var team: Team?
@NSManaged public var appExtensions: Set<InstalledExtension> @NSManaged public var appExtensions: Set<InstalledExtension>
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
public var isSideloaded: Bool { public var isSideloaded: Bool {
return self.storeApp == nil return self.storeApp == nil
} }
@@ -77,6 +79,8 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
self.bundleIdentifier = originalBundleIdentifier self.bundleIdentifier = originalBundleIdentifier
print("InstalledApp `self.bundleIdentifier`: \(self.bundleIdentifier)")
self.refreshedDate = Date() self.refreshedDate = Date()
self.installedDate = Date() self.installedDate = Date()
@@ -144,7 +148,7 @@ public extension InstalledApp
{ {
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp> let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K", fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version)) #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestVersion.version))
return fetchRequest return fetchRequest
} }
@@ -152,13 +156,14 @@ public extension InstalledApp
{ {
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp> let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive)) fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
print("Active Apps Fetch Request: \(String(describing: fetchRequest.predicate))")
return fetchRequest return fetchRequest
} }
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp? class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp?
{ {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID) let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
print("Fetch 'AltStore' Predicate: \(String(describing: predicate))")
let altStore = InstalledApp.first(satisfying: predicate, in: context) let altStore = InstalledApp.first(satisfying: predicate, in: context)
return altStore return altStore
} }
@@ -172,6 +177,7 @@ public extension InstalledApp
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp] class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
{ {
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID) var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated // if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
// { // {
@@ -205,6 +211,7 @@ public extension InstalledApp
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.isActive),
#keyPath(InstalledApp.refreshedDate), date as NSDate, #keyPath(InstalledApp.refreshedDate), date as NSDate,
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID) #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
print("Active Apps For Background Refresh 'AltStore' predicate: \(String(describing: predicate))")
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated // if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
// { // {
@@ -251,14 +258,15 @@ public extension InstalledApp
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps") let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) } do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
catch { print(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
} }
@@ -271,6 +279,7 @@ public extension InstalledApp
class func refreshedIPAURL(for app: AppProtocol) -> URL class func refreshedIPAURL(for app: AppProtocol) -> URL
{ {
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa") let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
print("`ipaURL`: \(ipaURL.absoluteString)")
return ipaURL return ipaURL
} }

View File

@@ -0,0 +1,126 @@
//
// LoggedError.swift
// AltStoreCore
//
// Created by Riley Testut on 9/6/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import CoreData
extension LoggedError
{
public enum Operation: String
{
case install
case update
case refresh
case activate
case deactivate
case backup
case restore
}
}
@objc(LoggedError)
public class LoggedError: NSManagedObject, Fetchable
{
/* Properties */
@NSManaged public private(set) var date: Date
@nonobjc public var operation: Operation? {
guard let rawOperation = self._operation else { return nil }
let operation = Operation(rawValue: rawOperation)
return operation
}
@NSManaged @objc(operation) private var _operation: String?
@NSManaged public private(set) var domain: String
@NSManaged public private(set) var code: Int32
@NSManaged public private(set) var userInfo: [String: Any]
@NSManaged public private(set) var appName: String
@NSManaged public private(set) var appBundleID: String
/* Relationships */
@NSManaged public private(set) var storeApp: StoreApp?
@NSManaged public private(set) var installedApp: InstalledApp?
private static let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .none
return dateFormatter
}()
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
public init(error: Error, app: AppProtocol, date: Date = Date(), operation: Operation? = nil, context: NSManagedObjectContext)
{
super.init(entity: LoggedError.entity(), insertInto: context)
self.date = date
self._operation = operation?.rawValue
let nsError = error as NSError
self.domain = nsError.domain
self.code = Int32(nsError.code)
self.userInfo = nsError.userInfo
self.appName = app.name
self.appBundleID = app.bundleIdentifier
switch app
{
case let storeApp as StoreApp: self.storeApp = storeApp
case let installedApp as InstalledApp: self.installedApp = installedApp
default: break
}
}
}
public extension LoggedError
{
var app: AppProtocol {
// `as AppProtocol` needed to fix "cannot convert AnyApp to StoreApp" compiler error with Xcode 14.
let app = self.installedApp ?? self.storeApp ?? AnyApp(name: self.appName, bundleIdentifier: self.appBundleID, url: nil) as AppProtocol
return app
}
var error: Error {
let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo)
return nsError
}
@objc
var localizedDateString: String {
let localizedDateString = LoggedError.dateFormatter.string(from: self.date)
return localizedDateString
}
var localizedFailure: String? {
guard let operation = self.operation else { return nil }
switch operation
{
case .install: return String(format: NSLocalizedString("Install %@ Failed", comment: ""), self.appName)
case .update: return String(format: NSLocalizedString("Update %@ Failed", comment: ""), self.appName)
case .refresh: return String(format: NSLocalizedString("Refresh %@ Failed", comment: ""), self.appName)
case .activate: return String(format: NSLocalizedString("Activate %@ 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 .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName)
}
}
}
public extension LoggedError
{
@nonobjc class func fetchRequest() -> NSFetchRequest<LoggedError>
{
return NSFetchRequest<LoggedError>(entityName: "LoggedError")
}
}

View File

@@ -31,6 +31,24 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
{ {
permission.managedObjectContext?.delete(permission) permission.managedObjectContext?.delete(permission)
} }
// Delete previous versions (different than below).
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil
{
appVersion.managedObjectContext?.delete(appVersion)
}
}
case is AppVersion where conflict.conflictingObjects.count == 2:
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }),
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
{
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
} }
default: default:
@@ -55,6 +73,16 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
permission.managedObjectContext?.delete(permission) permission.managedObjectContext?.delete(permission)
} }
if let contextApp = conflict.conflictingObjects.first as? StoreApp
{
let contextVersions = Set(contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version)
{
print("[ALTLog] Deleting cached app version: \(appVersion.appBundleID + "_" + appVersion.version), not in:", contextApp.versions.map { $0.appBundleID + "_" + $0.version })
appVersion.managedObjectContext?.delete(appVersion)
}
}
case let databaseObject as Source: case let databaseObject as Source:
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break } guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,111 @@
//
// StoreApp10ToStoreApp11Policy.swift
// AltStoreCore
//
// Created by Riley Testut on 9/13/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import CoreData
// Can't use NSManagedObject subclasses, so add convenience accessors for KVC.
fileprivate extension NSManagedObject
{
var storeAppBundleID: String? {
let bundleID = self.value(forKey: #keyPath(StoreApp.bundleIdentifier)) as? String
return bundleID
}
var storeAppSourceID: String? {
let sourceID = self.value(forKey: #keyPath(StoreApp.sourceIdentifier)) as? String
return sourceID
}
var storeAppVersion: String? {
let version = self.value(forKey: #keyPath(StoreApp._version)) as? String
return version
}
var storeAppVersionDate: Date? {
let versionDate = self.value(forKey: #keyPath(StoreApp._versionDate)) as? Date
return versionDate
}
var storeAppVersionDescription: String? {
let versionDescription = self.value(forKey: #keyPath(StoreApp._versionDescription)) as? String
return versionDescription
}
var storeAppSize: NSNumber? {
let size = self.value(forKey: #keyPath(StoreApp._size)) as? NSNumber
return size
}
var storeAppDownloadURL: URL? {
let downloadURL = self.value(forKey: #keyPath(StoreApp._downloadURL)) as? URL
return downloadURL
}
func setStoreAppLatestVersion(_ appVersion: NSManagedObject)
{
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestVersion))
let versions = NSOrderedSet(array: [appVersion])
self.setValue(versions, forKey: #keyPath(StoreApp._versions))
}
class func makeAppVersion(version: String,
date: Date,
localizedDescription: String?,
downloadURL: URL,
size: Int64,
appBundleID: String,
sourceID: String,
in context: NSManagedObjectContext) -> NSManagedObject
{
let appVersion = NSEntityDescription.insertNewObject(forEntityName: AppVersion.entity().name!, into: context)
appVersion.setValue(version, forKey: #keyPath(AppVersion.version))
appVersion.setValue(date, forKey: #keyPath(AppVersion.date))
appVersion.setValue(localizedDescription, forKey: #keyPath(AppVersion.localizedDescription))
appVersion.setValue(downloadURL, forKey: #keyPath(AppVersion.downloadURL))
appVersion.setValue(size, forKey: #keyPath(AppVersion.size))
appVersion.setValue(appBundleID, forKey: #keyPath(AppVersion.appBundleID))
appVersion.setValue(sourceID, forKey: #keyPath(AppVersion.sourceID))
return appVersion
}
}
@objc(StoreApp10ToStoreApp11Policy)
class StoreApp10ToStoreApp11Policy: NSEntityMigrationPolicy
{
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
{
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
guard let appBundleID = sInstance.storeAppBundleID,
let sourceID = sInstance.storeAppSourceID,
let version = sInstance.storeAppVersion,
let versionDate = sInstance.storeAppVersionDate,
// let versionDescription = sInstance.storeAppVersionDescription, // Optional
let downloadURL = sInstance.storeAppDownloadURL,
let size = sInstance.storeAppSize as? Int64
else { return }
guard
let destinationStoreApp = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first,
let context = destinationStoreApp.managedObjectContext
else { fatalError("A destination StoreApp and its managedObjectContext must exist.") }
let appVersion = NSManagedObject.makeAppVersion(
version: version,
date: versionDate,
localizedDescription: sInstance.storeAppVersionDescription,
downloadURL: downloadURL,
size: Int64(size),
appBundleID: appBundleID,
sourceID: sourceID,
in: context)
destinationStoreApp.setStoreAppLatestVersion(appVersion)
}
}

View File

@@ -103,22 +103,41 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged public private(set) var developerName: String @NSManaged public private(set) var developerName: String
@NSManaged public private(set) var localizedDescription: String @NSManaged public private(set) var localizedDescription: String
@NSManaged public private(set) var size: Int32 @NSManaged @objc(size) internal var _size: Int32
@NSManaged public private(set) var iconURL: URL @NSManaged public private(set) var iconURL: URL
@NSManaged public private(set) var screenshotURLs: [URL] @NSManaged public private(set) var screenshotURLs: [URL]
@NSManaged public var version: String @NSManaged @objc(version) internal var _version: String
@NSManaged public private(set) var versionDate: Date @NSManaged @objc(versionDate) internal var _versionDate: Date
@NSManaged public private(set) var versionDescription: String? @NSManaged @objc(versionDescription) internal var _versionDescription: String?
@NSManaged public private(set) var downloadURL: URL @NSManaged @objc(downloadURL) internal var _downloadURL: URL
@NSManaged public private(set) var platformURLs: PlatformURLs? @NSManaged public private(set) var platformURLs: PlatformURLs?
@NSManaged public private(set) var tintColor: UIColor? @NSManaged public private(set) var tintColor: UIColor?
@NSManaged public private(set) var isBeta: Bool @NSManaged public private(set) var isBeta: Bool
@NSManaged public var sourceIdentifier: String? @objc public internal(set) var sourceIdentifier: String? {
get {
self.willAccessValue(forKey: #keyPath(sourceIdentifier))
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
let sourceIdentifier = self.primitiveSourceIdentifier
return sourceIdentifier
}
set {
self.willChangeValue(forKey: #keyPath(sourceIdentifier))
self.primitiveSourceIdentifier = newValue
self.didChangeValue(forKey: #keyPath(sourceIdentifier))
for version in self.versions
{
version.sourceID = newValue
}
}
}
@NSManaged private var primitiveSourceIdentifier: String?
@NSManaged public var sortIndex: Int32 @NSManaged public var sortIndex: Int32
@@ -129,6 +148,11 @@ 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(versions) public private(set) var _versions: NSOrderedSet
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
@nonobjc public var source: Source? { @nonobjc public var source: Source? {
set { set {
self._source = newValue self._source = newValue
@@ -143,6 +167,35 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
return self._permissions.array as! [AppPermission] return self._permissions.array as! [AppPermission]
} }
@nonobjc public var versions: [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)
@@ -166,6 +219,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
case permissions case permissions
case size case size
case isBeta = "beta" case isBeta = "beta"
case versions
} }
public required init(from decoder: Decoder) throws public required init(from decoder: Decoder) throws
@@ -185,10 +239,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle) self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
self.version = try container.decode(String.self, forKey: .version)
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
self.iconURL = try container.decode(URL.self, forKey: .iconURL) self.iconURL = try container.decode(URL.self, forKey: .iconURL)
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? [] self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
@@ -198,14 +248,14 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
self.platformURLs = platformURLs self.platformURLs = platformURLs
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way) // Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
if let first = platformURLs.sorted().first { if let first = platformURLs.sorted().first {
self.downloadURL = first.downloadURL self._downloadURL = first.downloadURL
} else { } else {
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries") throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
} }
} else if let downloadURL = downloadURL { } else if let downloadURL = downloadURL {
self.downloadURL = downloadURL self._downloadURL = downloadURL
} else { } else {
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.") throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
} }
@@ -219,11 +269,40 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
self.tintColor = tintColor self.tintColor = tintColor
} }
self.size = try container.decode(Int32.self, forKey: .size)
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? [] let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
self._permissions = NSOrderedSet(array: permissions) self._permissions = NSOrderedSet(array: permissions)
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
{
//TODO: Throw error if there isn't at least one version.
for version in versions
{
version.appBundleID = self.bundleIdentifier
}
self.setVersions(versions)
}
else
{
let version = try container.decode(String.self, forKey: .version)
let versionDate = try container.decode(Date.self, forKey: .versionDate)
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
let size = try container.decode(Int32.self, forKey: .size)
let appVersion = AppVersion.makeAppVersion(version: version,
date: versionDate,
localizedDescription: versionDescription,
downloadURL: downloadURL,
size: Int64(size),
appBundleID: self.bundleIdentifier,
in: context)
self.setVersions([appVersion])
}
} }
catch catch
{ {
@@ -237,6 +316,24 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
} }
} }
private extension StoreApp
{
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)
// Preserve backwards compatibility by assigning legacy property values.
self._version = latestVersion.version
self._versionDate = latestVersion.date
self._versionDescription = latestVersion.localizedDescription
self._downloadURL = latestVersion.downloadURL
self._size = Int32(latestVersion.size)
}
}
public extension StoreApp public extension StoreApp
{ {
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp> @nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
@@ -253,9 +350,18 @@ public extension StoreApp
app.localizedDescription = "SideStore is an alternative App Store." app.localizedDescription = "SideStore is an alternative App Store."
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")! app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
app.screenshotURLs = [] app.screenshotURLs = []
app.version = "1.0" app.sourceIdentifier = Source.altStoreIdentifier
app.versionDate = Date()
app.downloadURL = URL(string: "http://rileytestut.com")! let appVersion = AppVersion.makeAppVersion(version: "0.1.2",
date: Date(),
downloadURL: URL(string: "http://rileytestut.com")!,
size: 0,
appBundleID: app.bundleIdentifier,
sourceID: Source.altStoreIdentifier,
in: context)
app.setVersions([appVersion])
print("makeAltStoreApp StoreApp: \(String(describing: app))")
#if BETA #if BETA
app.isBeta = true app.isBeta = true

View File

@@ -13,16 +13,16 @@ public protocol AppProtocol
{ {
var name: String { get } var name: String { get }
var bundleIdentifier: String { get } var bundleIdentifier: String { get }
var url: URL { get } var url: URL? { get }
} }
public struct AnyApp: AppProtocol public struct AnyApp: AppProtocol
{ {
public var name: String public var name: String
public var bundleIdentifier: String public var bundleIdentifier: String
public var url: URL public var url: URL?
public init(name: String, bundleIdentifier: String, url: URL) public init(name: String, bundleIdentifier: String, url: URL?)
{ {
self.name = name self.name = name
self.bundleIdentifier = bundleIdentifier self.bundleIdentifier = bundleIdentifier
@@ -32,21 +32,21 @@ public struct AnyApp: AppProtocol
extension ALTApplication: AppProtocol extension ALTApplication: AppProtocol
{ {
public var url: URL { public var url: URL? {
return self.fileURL return self.fileURL
} }
} }
extension StoreApp: AppProtocol extension StoreApp: AppProtocol
{ {
public var url: URL { public var url: URL? {
return self.downloadURL return self.downloadURL
} }
} }
extension InstalledApp: AppProtocol extension InstalledApp: AppProtocol
{ {
public var url: URL { public var url: URL? {
return self.fileURL return self.fileURL
} }
} }

View File

@@ -189,9 +189,9 @@ struct HomeScreenWidget: Widget
} }
} }
struct LockScreenWidget: Widget struct TextLockScreenWidget: Widget
{ {
private let kind: String = "LockAppDetail" private let kind: String = "TextLockAppDetail"
public var body: some WidgetConfiguration { public var body: some WidgetConfiguration {
if #available(iOSApplicationExtension 16, *) if #available(iOSApplicationExtension 16, *)
@@ -199,10 +199,10 @@ struct LockScreenWidget: Widget
return IntentConfiguration(kind: kind, return IntentConfiguration(kind: kind,
intent: ViewAppIntent.self, intent: ViewAppIntent.self,
provider: Provider()) { (entry) in provider: Provider()) { (entry) in
ComplicationView(entry: entry) ComplicationView(entry: entry, style: .text)
} }
.supportedFamilies([.accessoryCircular]) .supportedFamilies([.accessoryCircular])
.configurationDisplayName("AltWidget") .configurationDisplayName("AltWidget (Text)")
.description("View remaining days until SideStore expires.") .description("View remaining days until SideStore expires.")
} }
else else
@@ -212,11 +212,58 @@ struct LockScreenWidget: Widget
} }
} }
struct IconLockScreenWidget: Widget
{
private let kind: String = "IconLockAppDetail"
public var body: some WidgetConfiguration {
if #available(iOSApplicationExtension 16, *)
{
return IntentConfiguration(kind: kind,
intent: ViewAppIntent.self,
provider: Provider()) { (entry) in
ComplicationView(entry: entry, style: .icon)
}
.supportedFamilies([.accessoryCircular])
.configurationDisplayName("AltWidget (Icon)")
.description("View remaining days until SideStore expires.")
}
else
{
return EmptyWidgetConfiguration()
}
}
}
//
//struct LockScreenWidget: Widget
//{
// private let kind: String = "LockAppDetail"
//
// public var body: some WidgetConfiguration {
// if #available(iOSApplicationExtension 16, *)
// {
// return IntentConfiguration(kind: kind,
// intent: ViewAppIntent.self,
// provider: Provider()) { (entry) in
// ComplicationView(entry: entry, style: .icon)
// }
// .supportedFamilies([.accessoryCircular])
// .configurationDisplayName("AltWidget")
// .description("View remaining days until SideStore expires.")
// }
// else
// {
// return EmptyWidgetConfiguration()
// }
// }
//}
@main @main
struct AltWidgets: WidgetBundle struct AltWidgets: WidgetBundle
{ {
var body: some Widget { var body: some Widget {
HomeScreenWidget() HomeScreenWidget()
LockScreenWidget() IconLockScreenWidget()
TextLockScreenWidget()
} }
} }

View File

@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "altminicon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

Binary file not shown.

View File

@@ -9,10 +9,21 @@
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
@available(iOS 16, *)
extension ComplicationView
{
enum Style
{
case text
case icon
}
}
@available(iOS 16, *) @available(iOS 16, *)
struct ComplicationView: View struct ComplicationView: View
{ {
let entry: AppEntry let entry: AppEntry
let style: Style
var body: some View { var body: some View {
let refreshedDate = self.entry.app?.refreshedDate ?? .now let refreshedDate = self.entry.app?.refreshedDate ?? .now
@@ -23,26 +34,53 @@ struct ComplicationView: View
let progress = Double(daysRemaining) / Double(totalDays) let progress = Double(daysRemaining) / Double(totalDays)
ZStack(alignment: .center) { Gauge(value: progress) {
ProgressRing(progress: progress) { if daysRemaining < 0
if daysRemaining < 0 {
{ Text("Expired")
Text("Expired") .font(.system(size: 10, weight: .bold))
.font(.system(size: 10, weight: .bold)) }
} else
else {
switch self.style
{ {
case .text:
VStack(spacing: -1) { VStack(spacing: -1) {
let fontSize = daysRemaining > 99 ? 18.0 : 20.0
Text("\(daysRemaining)") Text("\(daysRemaining)")
.font(.system(size: 20, weight: .bold, design: .rounded)) .font(.system(size: fontSize, weight: .bold, design: .rounded))
Text(daysRemaining == 1 ? "DAY" : "DAYS") Text(daysRemaining == 1 ? "DAY" : "DAYS")
.font(.caption) .font(.caption)
} }
.fixedSize()
.offset(y: -1) .offset(y: -1)
case .icon:
ZStack {
// Destination
Image("SmallIcon")
.resizable()
.aspectRatio(1.0, contentMode: .fill)
.scaleEffect(x: 0.8, y: 0.8)
// Source
(
daysRemaining > 7 ?
Text("7+")
.font(.system(size: 18, weight: .bold, design: .rounded))
.kerning(-2) :
Text("\(daysRemaining)")
.font(.system(size: 20, weight: .bold, design: .rounded))
)
.foregroundColor(Color.black)
.blendMode(.destinationOut) // Clip text out of image.
}
} }
} }
} }
.gaugeStyle(.accessoryCircularCapacity)
.unredacted() .unredacted()
} }
} }
@@ -73,14 +111,23 @@ struct ComplicationView_Previews: PreviewProvider {
icon: UIImage(named: "AltStore")) icon: UIImage(named: "AltStore"))
return Group { return Group {
ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore)) ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore), style: .icon)
.previewContext(WidgetPreviewContext(family: .accessoryCircular)) .previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore)) ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore), style: .icon)
.previewContext(WidgetPreviewContext(family: .accessoryCircular)) .previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore)) ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore), style: .icon)
.previewContext(WidgetPreviewContext(family: .accessoryCircular)) .previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore), style: .text)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore), style: .text)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore), style: .text)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
} }
} }
} }

View File

@@ -1,54 +0,0 @@
//
// ProgressRing.swift
// AltWidgetExtension
//
// Created by Riley Testut on 8/17/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import SwiftUI
import WidgetKit
struct ProgressRing<Content: View>: View
{
let progress: Double
private let content: Content
init(progress: Double, @ViewBuilder content: () -> Content)
{
self.progress = progress
self.content = content()
}
var body: some View {
ZStack(alignment: .center) {
ring(progress: 1.0)
.opacity(0.3)
ring(progress: self.progress)
content
}
}
@ViewBuilder
private func ring(progress: Double) -> some View {
let strokeStyle = StrokeStyle(lineWidth: 4.0, lineCap: .round, lineJoin: .round)
Circle()
.inset(by: 2.0)
.trim(from: 0.0, to: progress)
.rotation(Angle(degrees: -90), anchor: .center)
.stroke(style: strokeStyle)
}
}
struct ProgressRing_Previews: PreviewProvider {
static var previews: some View {
ProgressRing(progress: 0.5) {
EmptyView()
}
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

View File

@@ -1,3 +1,3 @@
#include "Build.xcconfig" #include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(EXTENSION_PREFIX).AltWidget PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltWidget

View File

@@ -1,8 +1,8 @@
// Configuration settings file format documentation can be found at: // Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974 // https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 0.1.1 MARKETING_VERSION = 0.1.2
CURRENT_PROJECT_VERSION = 3011 CURRENT_PROJECT_VERSION = 3012
// Vars to be overwritten by `CodeSigning.xcconfig` if exists // Vars to be overwritten by `CodeSigning.xcconfig` if exists
DEVELOPMENT_TEAM = S32Z3HMYVQ DEVELOPMENT_TEAM = S32Z3HMYVQ
@@ -17,8 +17,8 @@ PRODUCT_NAME = SideStore
EXTENSION_PREFIX = $(ORG_PREFIX).SideStore EXTENSION_PREFIX = $(ORG_PREFIX).SideStore
//PRODUCT_NAME[configuration=Debug] = Prov Debug //PRODUCT_NAME[configuration=Debug] = Prov Debug
PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME) PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).SideStore
//PRODUCT_BUNDLE_IDENTIFIER[configuration=Debug] = $(ORG_PREFIX).$(PROJECT_NAME:lower)-debug //PRODUCT_BUNDLE_IDENTIFIER[configuration=Debug] = $(ORG_PREFIX).$(PROJECT_NAME:lower)-debug
APP_GROUP_IDENTIFIER = $(ORG_PREFIX).$(PRODUCT_NAME) APP_GROUP_IDENTIFIER = $(ORG_PREFIX).SideStore
ICLOUD_CONTAINER_IDENTIFIER = iCloud.$(ORG_PREFIX).$(PROJECT_NAME) ICLOUD_CONTAINER_IDENTIFIER = iCloud.$(ORG_PREFIX).$(PROJECT_NAME)

View File

@@ -41,6 +41,7 @@ public class ConnectionManager<RequestHandlerType: RequestHandler>
public var isStarted = false public var isStarted = false
private var connections = [Connection]() private var connections = [Connection]()
private let connectionsLock = NSLock()
public init(requestHandler: RequestHandlerType, connectionHandlers: [ConnectionHandler]) public init(requestHandler: RequestHandlerType, connectionHandlers: [ConnectionHandler])
{ {
@@ -88,6 +89,9 @@ private extension ConnectionManager
{ {
func prepare(_ connection: Connection) func prepare(_ connection: Connection)
{ {
self.connectionsLock.lock()
defer { self.connectionsLock.unlock() }
guard !self.connections.contains(where: { $0 === connection }) else { return } guard !self.connections.contains(where: { $0 === connection }) else { return }
self.connections.append(connection) self.connections.append(connection)
@@ -96,6 +100,9 @@ private extension ConnectionManager
func disconnect(_ connection: Connection) func disconnect(_ connection: Connection)
{ {
self.connectionsLock.lock()
defer { self.connectionsLock.unlock() }
guard let index = self.connections.firstIndex(where: { $0 === connection }) else { return } guard let index = self.connections.firstIndex(where: { $0 === connection }) else { return }
self.connections.remove(at: index) self.connections.remove(at: index)
} }

View File

@@ -305,6 +305,8 @@ public struct BeginInstallationRequest: ServerMessageProtocol
{ {
self.activeProfiles = activeProfiles self.activeProfiles = activeProfiles
self.bundleIdentifier = bundleIdentifier self.bundleIdentifier = bundleIdentifier
print("BeginInstallationRequest `activeProfiles`: \(String(describing: activeProfiles))")
print("BeginInstallationRequest `bundleIdentifier`: \(String(describing: bundleIdentifier))")
} }
} }
@@ -346,6 +348,9 @@ public struct InstallProvisioningProfilesRequest: ServerMessageProtocol
self.udid = udid self.udid = udid
self.provisioningProfiles = provisioningProfiles self.provisioningProfiles = provisioningProfiles
self.activeProfiles = activeProfiles self.activeProfiles = activeProfiles
print("InstallProvisioningProfilesRequest `self.udid`: \(self.udid)")
print("InstallProvisioningProfilesRequest `self.provisioningProfiles`: \(self.provisioningProfiles)")
print("InstallProvisioningProfilesRequest `self.activeProfiles`: \(String(describing: self.activeProfiles))")
} }
public init(from decoder: Decoder) throws public init(from decoder: Decoder) throws