diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6fbd6c5b..fee5ac40 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -21,7 +21,9 @@ jobs: steps: - name: Set current build as BETA - run: echo "IS_BETA=1" >> $GITHUB_ENV + run: | + echo "IS_BETA=1" >> $GITHUB_ENV + echo "RELEASE_CHANNEL=beta" >> $GITHUB_ENV - name: Checkout code uses: actions/checkout@v4 @@ -133,14 +135,40 @@ jobs: - name: Build SideStore - run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]} + # using 'tee' to intercept stdout and log for detailed build-log + run: | + NSUnbufferedIO=YES make build 2>&1 | tee build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]} - name: Fakesign app - run: make fakesign + run: make fakesign | tee -a build.log - name: Convert to IPA - run: make ipa + run: make ipa | tee -a build.log + - name: Encrypt build.log generated from SideStore build for upload + run: | + DEFAULT_BUILD_LOG_PASSWORD=12345 + + BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }} + BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD} + + if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then + echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'." + fi + + if [ ! -f build.log ]; then + echo "Warning: build.log is missing, creating a dummy log..." + echo "Error: build.log was missing, This is a dummy placeholder file..." > build.log + fi + + zip -e -P "$BUILD_LOG_ZIP_PASSWORD" encrypted-build_log.zip build.log + + - name: List Files after SideStore build + run: | + echo ">>>>>>>>> Workdir <<<<<<<<<<" + ls -la . + echo "" + - name: Get current date id: date run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT @@ -159,7 +187,7 @@ jobs: release: "Nightly" tag: "nightly" prerelease: true - files: SideStore.ipa SideStore.dSYMs.zip + files: SideStore.ipa SideStore.dSYMs.zip encrypted-build_log.zip body: | This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}). @@ -189,6 +217,12 @@ jobs: name: SideStore-${{ steps.version.outputs.version }}-dSYM path: ./SideStore.xcarchive/dSYMs/* + - name: Upload encrypted-build_log.zip + uses: actions/upload-artifact@v4 + with: + name: encrypted-build_log.zip + path: encrypted-build_log.zip + # Check if PUBLISH_BETA_UPDATES secret is set to non-zero - name: Check if PUBLISH_BETA_UPDATES is set id: check_publish @@ -242,44 +276,42 @@ jobs: run: | echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV - echo "BETA=true" >> $GITHUB_ENV echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV echo "LOCALIZED_DESCRIPTION=This is nightly release for revision: ${{ github.sha }}" >> $GITHUB_ENV echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/nightly/SideStore.ipa" >> $GITHUB_ENV - # - name: Checkout SideStore/apps-v2.json - # if: ${{ steps.check_publish.outcome == 'success' }} - # uses: actions/checkout@v4 - # with: - # # Repository name with owner. For example, actions/checkout - # # Default: ${{ github.repository }} - # repository: 'SideStore/apps-v2.json' - # # ref: 'main' # TODO: use branches for alpha and beta tracks? so as to avoid push collision? - # ref: 'nightly' # TODO: use branches for alpha and beta tracks? so as to avoid push collision? - # # token: ${{ github.token }} - # token: ${{ secrets.APPS_DEPLOY_KEY }} - # path: 'SideStore/apps-v2.json' + - name: Checkout SideStore/apps-v2.json + if: ${{ steps.check_publish.outcome == 'success' }} + uses: actions/checkout@v4 + with: + # Repository name with owner. For example, actions/checkout + # Default: ${{ github.repository }} + repository: 'SideStore/apps-v2.json' + ref: 'main' # TODO: use branches for alpha and beta tracks? so as to avoid push collision? + # ref: 'nightly' # TODO: use branches for alpha and beta tracks? so as to avoid push collision? + # token: ${{ github.token }} + token: ${{ secrets.APPS_DEPLOY_KEY }} + path: 'SideStore/apps-v2.json' - # - name: Publish to SideStore/apps-v2.json - # if: ${{ steps.check_publish.outcome == 'success' }} - # run: | - # # Copy and execute the update script - # pushd SideStore/apps-v2.json/ + - name: Publish to SideStore/apps-v2.json + if: ${{ steps.check_publish.outcome == 'success' }} + run: | + # Copy and execute the update script + pushd SideStore/apps-v2.json/ - # # Configure Git user (committer details) - # git config user.name "GitHub Actions" - # git config user.email "github-actions@github.com" + # Configure Git user (committer details) + git config user.name "GitHub Actions" + git config user.email "github-actions@github.com" - # # Make the update script executable and run it - # python3 ../../update_apps.py "./_includes/source.json" + # update the source.json + python3 ../../update_apps.py "./_includes/source.json" - # # Commit changes and push using SSH - # git add ./_includes/source.json - # git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit" + # Commit changes and push using SSH + git add ./_includes/source.json + git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit" - # git status - # # git push origin HEAD:main - # git push origin HEAD:nightly - # popd + git status + git push origin HEAD:main + popd diff --git a/.gitignore b/.gitignore index 2fcc4bde..d7f580cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # macOS # -*.DS_Store +**/*.DS_Store # Xcode # diff --git a/AltBackup/ViewController.swift b/AltBackup/ViewController.swift index eca547bb..c0cf2565 100644 --- a/AltBackup/ViewController.swift +++ b/AltBackup/ViewController.swift @@ -82,7 +82,7 @@ class ViewController: UIViewController self.activityIndicatorView.color = .altstoreText self.activityIndicatorView.startAnimating() - // TODO: @mahee96: Disabled this buttons which were present for debugging purpose. + // TODO: @mahee96: Disabled these backup/restore buttons in altbackup.app screen which were present for debugging purpose. // Can find something useful for these later, but these are not required by this backup/restore app // #if DEBUG // let button1 = UIButton(type: .system) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 87e53fa4..914ff9a8 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; }; 0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E05025B2BEC947000879B5C /* String+SideStore.swift */; }; - 0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */; }; 0E1A1F912AE36A9700364CAD /* bytearray.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E1A1F902AE36A9600364CAD /* bytearray.c */; }; 0EA1665B2ADFE0D2003015C1 /* out-limd.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166472ADFE0D1003015C1 /* out-limd.c */; }; 0EA1665C2ADFE0D2003015C1 /* out-default.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166522ADFE0D2003015C1 /* out-default.c */; }; @@ -50,17 +49,37 @@ 551A15E55999499418AC1022 /* Pods_AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 707E746318F0B6F1A44935D3 /* Pods_AltStoreCore.framework */; }; A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; }; A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */; }; + A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8087E742D2D2958002DB21B /* ImportExport.swift */; }; + A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */; }; + A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */; }; A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; }; A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; }; A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; }; A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; }; + A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; }; + A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; }; A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; }; A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; }; A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */; }; + A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; }; + A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; }; + A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */; }; + A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */; }; A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; }; A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; }; + A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */; }; + A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; }; + A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; }; + A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; }; A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; }; + A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; }; + A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; }; + A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C282D206AC100E83DBD /* OutputStream.swift */; }; + A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */; }; + A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C312D206B2500E83DBD /* FileOutputStream.swift */; }; + A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */; }; A8C6D50C2D1EE87600DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */; }; A8C6D5122D1EE8AF00DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D5112D1EE8AF00DF01F1 /* AltSign-Static */; }; A8C6D5132D1EE8D700DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; }; @@ -68,6 +87,7 @@ A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; }; A8C6D5182D1EE95B00DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A8D484D82D0CD306002C691D /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = A8D484D72D0CD306002C691D /* AltBackup.ipa */; }; + A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */; }; A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; }; A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; }; A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; }; @@ -555,9 +575,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = ""; }; 0E05025B2BEC947000879B5C /* String+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SideStore.swift"; sourceTree = ""; }; - 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+SideStore.swift"; sourceTree = ""; }; 0E1A1F902AE36A9600364CAD /* bytearray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = bytearray.c; path = src/bytearray.c; sourceTree = ""; }; 0EA166412ADFE0D1003015C1 /* jplist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = jplist.c; path = Dependencies/libplist/src/jplist.c; sourceTree = SOURCE_ROOT; }; 0EA166422ADFE0D1003015C1 /* Date.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Date.cpp; path = Dependencies/libplist/src/Date.cpp; sourceTree = SOURCE_ROOT; }; @@ -608,12 +626,17 @@ 7935E4499B2FC11DA8BAB2CC /* Pods-AltStoreCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStoreCore.release.xcconfig"; path = "Target Support Files/Pods-AltStoreCore/Pods-AltStoreCore.release.xcconfig"; sourceTree = ""; }; A800F7032CE28E2F00208744 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = ""; }; A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_SideStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A8087E742D2D2958002DB21B /* ImportExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExport.swift; sourceTree = ""; }; + A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUpdateIntent.swift; sourceTree = ""; }; + A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveAppsTimelineProvider+Simulator.swift"; sourceTree = ""; }; A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = ""; }; A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = ""; }; A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = ""; }; A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "minimuxer-helpers.swift"; sourceTree = ""; }; A809F6A62D04DA1900F0F0F3 /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = ""; }; A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = ""; }; + A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = ""; }; + A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = ""; }; A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = ""; }; A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = ""; }; A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = ""; }; @@ -623,8 +646,24 @@ A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltWidgetExtension.xcconfig; sourceTree = ""; }; A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = ""; }; A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = ""; }; + A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorProcessing.swift; sourceTree = ""; }; + A868CFE32D319988002F1201 /* SingletonGenericMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonGenericMap.swift; sourceTree = ""; }; + A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppExtensionsOperation.swift; sourceTree = ""; }; + A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingContolView.swift; sourceTree = ""; }; + A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingControl.swift; sourceTree = ""; }; A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsTimelineProvider.swift; sourceTree = ""; }; + A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = ""; }; + A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = ""; }; + A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = ""; }; + A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; + A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = ""; }; + A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = ""; }; + A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractClassError.swift; sourceTree = ""; }; + A8C38C312D206B2500E83DBD /* FileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOutputStream.swift; sourceTree = ""; }; + A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogView.swift; sourceTree = ""; }; A8D484D72D0CD306002C691D /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = ""; }; + A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+AltStore.swift"; sourceTree = ""; }; A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; sourceTree = ""; }; A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = minimuxer.xcodeproj; sourceTree = ""; }; A8FD915B2D046EF100322782 /* ProcessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessError.swift; sourceTree = ""; }; @@ -1118,6 +1157,24 @@ path = Extensions; sourceTree = ""; }; + A8087E712D2D291B002DB21B /* importexport */ = { + isa = PBXGroup; + children = ( + A8087E742D2D2958002DB21B /* ImportExport.swift */, + ); + path = importexport; + sourceTree = ""; + }; + A8096D1D2D30ADD5000C39C6 /* Providers */ = { + isa = PBXGroup; + children = ( + D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */, + A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */, + A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */, + ); + path = Providers; + sourceTree = ""; + }; A809F68A2D04D71200F0F0F3 /* Products */ = { isa = PBXGroup; children = ( @@ -1135,6 +1192,15 @@ name = Products; sourceTree = ""; }; + A80D790B2D2F209700A40F40 /* Intents */ = { + isa = PBXGroup; + children = ( + A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */, + A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */, + ); + path = Intents; + sourceTree = ""; + }; A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = { isa = PBXGroup; children = ( @@ -1150,6 +1216,22 @@ path = xcconfigs; sourceTree = ""; }; + A86315DD2D3EB2BD0048FA40 /* errors */ = { + isa = PBXGroup; + children = ( + A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */, + ); + path = errors; + sourceTree = ""; + }; + A88B8C532D35F1E800F53F9D /* operations */ = { + isa = PBXGroup; + children = ( + A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */, + ); + path = operations; + sourceTree = ""; + }; A8A543222D04F0C100D72399 /* Products */ = { isa = PBXGroup; children = ( @@ -1161,6 +1243,81 @@ name = Products; sourceTree = ""; }; + A8A853AD2D3050CC00995795 /* pagination */ = { + isa = PBXGroup; + children = ( + A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */, + ); + path = pagination; + sourceTree = ""; + }; + A8AD35562D31BE8F003A28B4 /* Manager */ = { + isa = PBXGroup; + children = ( + A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + A8AD35572D31BEB2003A28B4 /* datastructures */ = { + isa = PBXGroup; + children = ( + A868CFE32D319988002F1201 /* SingletonGenericMap.swift */, + ); + path = datastructures; + sourceTree = ""; + }; + A8B516DE2D2666900047047C /* dignostics */ = { + isa = PBXGroup; + children = ( + A86315DD2D3EB2BD0048FA40 /* errors */, + A88B8C532D35F1E800F53F9D /* operations */, + A8B516DF2D2666A00047047C /* database */, + ); + path = dignostics; + sourceTree = ""; + }; + A8B516DF2D2666A00047047C /* database */ = { + isa = PBXGroup; + children = ( + A8B516E22D2666CA0047047C /* CoreDataHelper.swift */, + ); + path = database; + sourceTree = ""; + }; + A8C38C1C2D2068D100E83DBD /* Utils */ = { + isa = PBXGroup; + children = ( + A8AD35572D31BEB2003A28B4 /* datastructures */, + A8A853AD2D3050CC00995795 /* pagination */, + A8087E712D2D291B002DB21B /* importexport */, + A8B516DE2D2666900047047C /* dignostics */, + A8C38C272D206AA500E83DBD /* common */, + A8C38C202D206A3A00E83DBD /* iostreams */, + ); + path = Utils; + sourceTree = ""; + }; + A8C38C202D206A3A00E83DBD /* iostreams */ = { + isa = PBXGroup; + children = ( + A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */, + A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */, + ); + path = iostreams; + sourceTree = ""; + }; + A8C38C272D206AA500E83DBD /* common */ = { + isa = PBXGroup; + children = ( + A8B516E52D2668020047047C /* DateTimeUtil.swift */, + A8C38C282D206AC100E83DBD /* OutputStream.swift */, + A8C38C312D206B2500E83DBD /* FileOutputStream.swift */, + A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */, + ); + path = common; + sourceTree = ""; + }; A8F66C072D04C025009689E6 /* SideStore */ = { isa = PBXGroup; children = ( @@ -1170,6 +1327,7 @@ B343F84D295F6323002B1159 /* em_proxy.xcodeproj */, 19104DB32909C06D00C49C7B /* EmotionalDamage */, B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */, + A8C38C1C2D2068D100E83DBD /* Utils */, ); path = SideStore; sourceTree = ""; @@ -1572,12 +1730,12 @@ BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */, BF66EEE42501AED0007EE018 /* UserDefaults+AltStore.swift */, BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */, - 0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */, 0E05025B2BEC947000879B5C /* String+SideStore.swift */, D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */, D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */, D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */, D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */, + A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */, D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */, D552EB052AF453F900A3AB4D /* URL+Normalized.swift */, ); @@ -1619,10 +1777,12 @@ BF98916C250AABF3002ACF50 /* AltWidget */ = { isa = PBXGroup; children = ( + A8AD35562D31BE8F003A28B4 /* Manager */, + A80D790B2D2F209700A40F40 /* Intents */, + A8096D1D2D30ADD5000C39C6 /* Providers */, A800F6FE2CE28DE300208744 /* Extensions */, BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */, D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */, - D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */, D50C29F22A8ECD71009AB488 /* Widgets */, D51AF9752A97D29100471312 /* Model */, D577AB802A968B7E007FE952 /* Components */, @@ -1846,7 +2006,6 @@ BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */, D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */, B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */, - 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */, D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */, D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */, D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */, @@ -1858,6 +2017,7 @@ BFDB69FB22A9A7A6007EA6D6 /* Settings */ = { isa = PBXGroup; children = ( + A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */, BFE60737231ADF49002B0E8E /* Settings.storyboard */, BFE60739231ADF82002B0E8E /* SettingsViewController.swift */, 0EA426392C2230150026D7FB /* AnisetteServerList.swift */, @@ -1881,29 +2041,30 @@ children = ( D513F6152A12CE210061EAA1 /* Errors */, BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */, - BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */, BF770E5322BC044E002A40FE /* OperationContexts.swift */, + BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */, BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */, + D561AF812B21669400BF59C6 /* VerifyAppPledgeOperation.swift */, BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */, - BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, + BFCCB519245E3401001853EA /* VerifyAppOperation.swift */, + A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */, + BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */, BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */, - BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */, + BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */, BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, - BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, - BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */, + BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */, BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */, - BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */, - BFCCB519245E3401001853EA /* VerifyAppOperation.swift */, - BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */, + BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, + D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */, BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */, BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */, + BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */, + BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */, BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */, D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */, D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */, - D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */, D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */, - D561AF812B21669400BF59C6 /* VerifyAppPledgeOperation.swift */, BF7B44062725A4B8005288A4 /* Patch App */, ); path = Operations; @@ -2095,6 +2256,7 @@ D589170128C7D93500E39C8B /* Error Log */ = { isa = PBXGroup; children = ( + A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */, D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */, D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */, 0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */, @@ -2556,7 +2718,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#!/bin/sh\n\necho \"Build directory: $BUILD_DIR\"\necho \"Configuration build directory: $CONFIGURATION_BUILD_DIR\"\n\n# diagnostics\n# echo \">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<\"\n# find \"$BUILD_DIR\" -maxdepth 7 -exec ls -ld {} + || true # List contents if directory exists \n# # ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists \n# echo \"\"\n\n# diagnostics\n# exit 0\n\n# Define the path to your Makefile\nMAKEFILE_PATH=\"${PROJECT_DIR}/\"\n\n# Navigate to the directory containing the Makefile\ncd \"$MAKEFILE_PATH\" || exit 1\n\n# Run the make target 'ipa-altbackup'\nmake -B copy-altbackup ipa-altbackup\n\n# Ensure that the ipa-altbackup process finishes before continuing\nif [ $? -ne 0 ]; then\n echo \"Error: ipa-altbackup failed\"\n exit 1\nelse\n echo \"ipa-altbackup completed successfully\"\nfi\n# Type a script or drag a script file from your workspace to insert its path.\n"; + shellScript = "#!/bin/sh\n\necho \"Build directory: $BUILD_DIR\"\necho \"Configuration build directory: $CONFIGURATION_BUILD_DIR\"\n\n# diagnostics\n# echo \">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<\"\n# find \"$BUILD_DIR\" -maxdepth 7 -exec ls -ld {} + || true # List contents if directory exists \n# # ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists \n# echo \"\"\n\n# diagnostics\n# exit 0\n\n# Define the path to your Makefile\nMAKEFILE_PATH=\"${PROJECT_DIR}/\"\n\n# Navigate to the directory containing the Makefile\ncd \"$MAKEFILE_PATH\" || exit 1\n\n# Run the make target 'ipa-altbackup'\nmake -B clean-altbackup copy-altbackup ipa-altbackup\n\n# Ensure that the ipa-altbackup process finishes before continuing\nif [ $? -ne 0 ]; then\n echo \"Error: ipa-altbackup failed\"\n exit 1\nelse\n echo \"ipa-altbackup completed successfully\"\nfi\n# Type a script or drag a script file from your workspace to insert its path.\n"; }; AEDB4E9409D2CEE1EA126980 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -2714,6 +2876,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */, A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */, D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */, BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */, @@ -2823,12 +2986,19 @@ D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */, BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */, D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, + A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */, + A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */, D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */, + A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */, D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */, + A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */, + A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */, + A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */, + A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2838,14 +3008,18 @@ files = ( BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, + A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */, + A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */, BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */, + A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */, + A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */, D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */, BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */, D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */, @@ -2865,11 +3039,15 @@ BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */, BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, + A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */, + A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */, D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */, D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */, + A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */, A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */, D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */, BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */, + A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, @@ -2909,7 +3087,7 @@ BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */, 19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */, - 0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */, + A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */, D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */, BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */, BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */, @@ -2919,6 +3097,7 @@ BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */, BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */, D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */, + A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */, D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */, D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */, BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */, @@ -2929,15 +3108,18 @@ BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */, BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */, + A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */, BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, 0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */, BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, + A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, 0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */, D561AF822B21669400BF59C6 /* VerifyAppPledgeOperation.swift in Sources */, + A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */, BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */, diff --git a/AltStore/AltStore.entitlements b/AltStore/AltStore.entitlements index 27c6a878..02a703df 100644 --- a/AltStore/AltStore.entitlements +++ b/AltStore/AltStore.entitlements @@ -2,14 +2,22 @@ + com.apple.developer.kernel.extended-virtual-addressing com.apple.developer.kernel.increased-debugging-memory-limit com.apple.developer.kernel.increased-memory-limit - aps-environment - development + aps-environment + development com.apple.developer.siri com.apple.security.application-groups diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 49c1bda1..b436b686 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -41,8 +41,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { private let intentHandler = IntentHandler() private let viewAppIntentHandler = ViewAppIntentHandler() + public let consoleLog = ConsoleLog() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // navigation bar buttons spacing is too much (so hack it to use minimal spacing) + // this is swift-5 specific behavior and might change + // https://stackoverflow.com/a/64988363/11971304 + // + // Warning: this affects all screens through out the app, and basically overrides storyboard + let stackViewAppearance = UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self]) + stackViewAppearance.spacing = -8 // adjust as needed + + consoleLog.startCapturing() + print("===================================================") + print("| App is Starting up |") + print("===================================================") + print("| Console Logger started capturing output streams |") + print("===================================================") + print("\n ") + // Override point for customization after application launch. // UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug") // UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug") @@ -81,9 +99,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String -// #if DEBUG || BETA + #if DEBUG && (targetEnvironment(simulator) || BETA) UserDefaults.standard.isDebugModeEnabled = true -// #endif + #endif self.prepareForBackgroundFetch() @@ -130,6 +148,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { default: return nil } } + + func applicationWillTerminate(_ application: UIApplication) { + // Stop console logging and clean up resources + print("\n ") + print("===================================================") + print("| Console Logger stopped capturing output streams |") + print("===================================================") + print("| App is being terminated |") + print("===================================================") + consoleLog.stopCapturing() + } } extension AppDelegate @@ -269,7 +298,7 @@ extension AppDelegate UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in } - #if DEBUG + #if DEBUG && targetEnvironment(simulator) UIApplication.shared.registerForRemoteNotifications() #endif } diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift index d1db2e18..3e275e0e 100644 --- a/AltStore/Authentication/AuthenticationViewController.swift +++ b/AltStore/Authentication/AuthenticationViewController.swift @@ -125,6 +125,7 @@ private extension AuthenticationViewController let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: "")) let toastView = ToastView(error: error) toastView.show(in: self) + toastView.backgroundColor = .white toastView.textLabel.textColor = .altPrimary toastView.detailTextLabel.textColor = .altPrimary self.toastView = toastView diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift index 77c93212..f940b4ac 100644 --- a/AltStore/Components/ToastView.swift +++ b/AltStore/Components/ToastView.swift @@ -67,30 +67,10 @@ class ToastView: RSTToastView convenience init(error: Error) { - var error = error as NSError - var underlyingError = error.underlyingError + let error = error as NSError - if - let unwrappedUnderlyingError = underlyingError, - error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue - { - // Treat underlyingError as the primary error, but keep localized title + failure. - - let nsError = error as NSError - error = unwrappedUnderlyingError as NSError - - if let localizedTitle = nsError.localizedTitle { - error = error.withLocalizedTitle(localizedTitle) - } - if let localizedFailure = nsError.localizedFailure { - error = error.withLocalizedFailure(localizedFailure) - } - - underlyingError = nil - } let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "") - let detailText = error.localizedDescription - + let detailText = ErrorProcessing(.fullError).getDescription(error: error) self.init(text: text, detailText: detailText) } diff --git a/AltStore/LaunchViewController.swift b/AltStore/LaunchViewController.swift index 38fae3a3..13c42c80 100644 --- a/AltStore/LaunchViewController.swift +++ b/AltStore/LaunchViewController.swift @@ -248,7 +248,9 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg target_minimuxer_address() let documentsDirectory = FileManager.default.documentsDirectory.absoluteString do { - try start(pairing_file, documentsDirectory) + // enable minimuxer console logging only if enabled in settings + let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled + try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled) } catch { try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")) displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")") @@ -311,6 +313,9 @@ extension LaunchViewController guard case .failure(let error) = result else { return } Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)") + let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError) + print("Failed to update sources on launch. \(errorDesc)") + let toastView = ToastView(error: error) toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside) toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController) @@ -318,6 +323,7 @@ extension LaunchViewController self.updateKnownSources() + // Ask widgets to be refreshed WidgetCenter.shared.reloadAllTimelines() // Add view controller as child (rather than presenting modally) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index bea66ab7..39fc0c47 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -626,6 +626,11 @@ extension AppManager self.fetchSources() { (result) in do { + // Check if the result is failure and rethrow + if case .failure(let error) = result { + throw error // Rethrow the error + } + do { let (_, context) = try result.get() @@ -1146,27 +1151,28 @@ private extension AppManager case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough case .refresh(let app): // Check if backup app is installed in place of real app. - let uti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary? +// let altBackupUti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary? - if app.certificateSerialNumber != group.context.certificate?.serialNumber || - uti != nil || - app.needsResign || +// if app.certificateSerialNumber != group.context.certificate?.serialNumber || +// altBackupUti != nil || // why would altbackup requires reinstall? it shouldn't cause we are just renewing profiles +// app.needsResign || // why would an app require resign during refresh? it shouldn't! // We need to reinstall ourselves on refresh to ensure the new provisioning profile is used - app.bundleIdentifier == StoreApp.altstoreAppID - { + // => mahee96: jkcoxson confirmed misagent manages profiles independently without requiring lockdownd or installd intervention, so sidestore profile renewal shouldn't require reinstall +// app.bundleIdentifier == StoreApp.altstoreAppID +// { // Resign app instead of just refreshing profiles because either: - // * Refreshing using different certificate - // * Backup app is still installed - // * App explicitly needs resigning + // * Refreshing using different certificate // when can this happen?, lets assume, refreshing with different certificate, why not just ask user to re-install manually? (probably we need re-install button) + // * Backup app is still installed // but why? I mean the AltBackup was put in place for a reason? ie during refresh just renew appIDs don't care about the app itself. + // * App explicitly needs resigning // when can this happen? // * Device is jailbroken and using AltDaemon on iOS 14.0 or later (b/c refreshing with provisioning profiles is broken) - let installProgress = self._install(app, operation: operation, group: group) { (result) in - self.finish(operation, result: result, group: group, progress: progress) - } - progress?.addChild(installProgress, withPendingUnitCount: 80) - } - else - { +// let installProgress = self._install(app, operation: operation, group: group) { (result) in +// self.finish(operation, result: result, group: group, progress: progress) +// } +// progress?.addChild(installProgress, withPendingUnitCount: 80) +// } +// else +// { // Refreshing with same certificate as last time, and backup app isn't still installed, // so we can just refresh provisioning profiles. @@ -1174,7 +1180,7 @@ private extension AppManager self.finish(operation, result: result, group: group, progress: progress) } progress?.addChild(refreshProgress, withPendingUnitCount: 80) - } +// } case .activate(let app): let activateProgress = self._activate(app, operation: operation, group: group) { (result) in @@ -1234,132 +1240,6 @@ private extension AppManager return group } - func removeAppExtensions( - from application: ALTApplication, - existingApp: InstalledApp?, - extensions: Set, - _ presentingViewController: UIViewController?, - completion: @escaping (Result) -> Void - ) { - - // App-Extensions: Ensure existing app's extensions and currently installing app's extensions must match - if let existingApp { - _ = RSTAsyncBlockOperation { _ in - let existingAppEx: Set = existingApp.appExtensions - let currentAppEx: Set = application.appExtensions - - let currentAppExNames = currentAppEx.map{ appEx in appEx.bundleIdentifier} - let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier} - - let excessExtensions = currentAppEx.filter{ - !(existingAppExNames.contains($0.bundleIdentifier)) - } - - - let isMatching = (currentAppEx.count == existingAppEx.count) && excessExtensions.isEmpty - let diagnosticsMsg = "AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \(isMatching)\n" - + "AppManager.removeAppExtensions: existingAppEx: \(existingAppExNames); currentAppEx: \(String(describing: currentAppExNames))\n" - print(diagnosticsMsg) - - - // if background mode, then remove only the excess extensions - guard let presentingViewController: UIViewController = presentingViewController else { - // perform silent extensions cleanup for those that aren't already present in existing app - print("\n Performing background mode Extensions removal \n") - print("AppManager.removeAppExtensions: Excess Extensions: \(excessExtensions)") - - do { - for appExtension in excessExtensions { - print("Deleting extension \(appExtension.bundleIdentifier)") - try FileManager.default.removeItem(at: appExtension.fileURL) - } - return completion(.success(())) - } catch { - return completion(.failure(error)) - } - } - } - } - - guard !application.appExtensions.isEmpty else { return completion(.success(())) } - - DispatchQueue.main.async { - let firstSentence: String - - if UserDefaults.standard.activeAppLimitIncludesExtensions - { - firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "") - } - else - { - firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "") - } - - let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "") - - let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in - completion(.failure(OperationError.cancelled)) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in - completion(.success(())) - }) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in - do - { - for appExtension in application.appExtensions - { - print("Deleting extension \(appExtension.bundleIdentifier)") - try FileManager.default.removeItem(at: appExtension.fileURL) - } - - completion(.success(())) - } - catch - { - completion(.failure(error)) - } - }) - - if let presentingViewController { - alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in - let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in - do - { - for appExtension in selection - { - print("Deleting extension \(appExtension.bundleIdentifier)") - - try FileManager.default.removeItem(at: appExtension.fileURL) - } - completion(.success(())) - } - catch - { - completion(.failure(error)) - } - return nil - } - - let suiview = popoverContentController.view! - suiview.translatesAutoresizingMaskIntoConstraints = false - - popoverContentController.modalPresentationStyle = .popover - - if let popoverPresentationController = popoverContentController.popoverPresentationController { - popoverPresentationController.sourceView = presentingViewController.view - popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4) - popoverPresentationController.delegate = popoverContentController - - presentingViewController.present(popoverContentController, animated: true) - } - }) - - presentingViewController.present(alertController, animated: true) - } - } - } - private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, @@ -1469,52 +1349,20 @@ private extension AppManager verifyOperation.addDependency(downloadOperation) /* Remove App Extensions */ - - let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in - do - { - if let error = context.error - { - throw error - } -/* - guard case .install = appOperation else { - operation.finish() - return - } -*/ - guard let extensions = context.app?.appExtensions else { - throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil") - } - - guard let currentApp = context.app else { - throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app is nil") - } - - - self?.removeAppExtensions(from: currentApp, - existingApp: app as? InstalledApp, - extensions: extensions, - context.authenticatedContext.presentingViewController - ) { result in - switch result { - case .success(): break - case .failure(let error): context.error = error - } - operation.finish() - } - - } - catch + let localAppExtensions = (app as? ALTApplication)?.appExtensions + let removeAppExtensionsOperation = RemoveAppExtensionsOperation(context: context, + localAppExtensions: localAppExtensions) + removeAppExtensionsOperation.resultHandler = { (result) in + switch result { + case .failure(let error): context.error = error - operation.finish() + case .success: break } } - removeAppExtensionsOperation.addDependency(verifyOperation) - + /* Refresh Anisette Data */ let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context) refreshAnisetteDataOperation.resultHandler = { (result) in @@ -1529,7 +1377,7 @@ private extension AppManager /* Fetch Provisioning Profiles */ - let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context) + let fetchProvisioningProfilesOperation = FetchProvisioningProfilesInstallOperation(context: context) fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements fetchProvisioningProfilesOperation.resultHandler = { (result) in switch result @@ -1763,7 +1611,7 @@ private extension AppManager private func exportResginedAppsToDocsDir(_ resignedApp: ALTApplication) { // Check if the user has enabled exporting resigned apps to the Documents directory and continue - guard UserDefaults.standard.isResignedAppExportEnabled else { + guard UserDefaults.standard.isExportResignedAppEnabled else { return } @@ -1815,26 +1663,27 @@ private extension AppManager let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context) context.app = ALTApplication(fileURL: app.fileURL) - //App-Extensions: Ensure DB data and disk state must match - let dbAppEx: Set = Set(app.appExtensions) - let diskAppEx: Set = Set(context.app!.appExtensions) - let diskAppExNames = diskAppEx.map { $0.bundleIdentifier } - let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier } - let isMatching = Set(dbAppExNames) == Set(diskAppExNames) + // Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path + //App-Extensions: Ensure DB data and disk state must match + let dbAppEx: Set = Set(app.appExtensions) + let diskAppEx: Set = Set(context.app!.appExtensions) + let diskAppExNames = diskAppEx.map { $0.bundleIdentifier } + let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier } + let isMatching = Set(dbAppExNames) == Set(diskAppExNames) - let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in - - let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n" - + "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n" - print(errMessage) - if(!isMatching){ - completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage))) - } - op.finish() - } + let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in + + let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n" + + "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n" + print(errMessage) + if(!isMatching){ + completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage))) + } + op.finish() + } /* Fetch Provisioning Profiles */ - let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context) + let fetchProvisioningProfilesOperation = FetchProvisioningProfilesRefreshOperation(context: context) fetchProvisioningProfilesOperation.resultHandler = { (result) in switch result { @@ -1844,7 +1693,7 @@ private extension AppManager } } progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60) - fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation) + // fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation) /* Refresh */ let refreshAppOperation = RefreshAppOperation(context: context) @@ -1853,7 +1702,10 @@ private extension AppManager { case .success(let installedApp): completionHandler(.success(installedApp)) - + + + // refreshing local app's provisioning profile means talking to misagent daemon + // which requires loopback vpn case .failure(MinimuxerError.ProfileInstall): completionHandler(.failure(OperationError.noWiFi)) @@ -1878,7 +1730,8 @@ private extension AppManager progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40) refreshAppOperation.addDependency(fetchProvisioningProfilesOperation) - let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation] +// let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation] + let operations = [fetchProvisioningProfilesOperation, refreshAppOperation] group.add(operations) self.run(operations, context: group.context) @@ -2280,6 +2133,7 @@ private extension AppManager AnalyticsManager.shared.trackEvent(event) } + // Ask widgets to be refreshed WidgetCenter.shared.reloadAllTimelines() do diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 7f7f2948..24dd159c 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -1151,7 +1151,9 @@ private extension MyAppsViewController func refresh(_ installedApp: InstalledApp) { - guard minimuxerStatus else { return } + // we do need minimuxer, coz it needs to talk to misagent daemon which manages profiles + // so basically loopback vpn is still required + guard minimuxerStatus else { return } // we don't need minimuxer when renewing appIDs only do we, heck we can even do it on mobile internet let previousProgress = AppManager.shared.refreshProgress(for: installedApp) guard previousProgress == nil else { @@ -1357,6 +1359,50 @@ private extension MyAppsViewController self.present(alertController, animated: true, completion: nil) } + func importBackup(for installedApp: InstalledApp){ + ImportExport.importBackup(presentingViewController: self, for: installedApp) { result in + var toast: ToastView + switch(result){ + case .failure(let error): + toast = ToastView(error: error, opensLog: false) + break + case .success: + toast = ToastView(text: "Import Backup successful for \(installedApp.name)", + detailText: "Use 'Restore Backup' option to restore data from this imported backup") + } + DispatchQueue.main.async { + toast.show(in: self) + } + } + } + + private func getPreviousBackupURL(_ installedApp: InstalledApp) -> URL + { + let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)! + let backupBakURL = ImportExport.getPreviousBackupURL(backupURL) + return backupBakURL + } + + func restorePreviousBackup(for installedApp: InstalledApp){ + let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)! + let backupBakURL = ImportExport.getPreviousBackupURL(backupURL) + + // backupBakURL is expected to exist at this point, this needs to be ensured by caller logic + // or invoke this action only when backupBakURL exists + + // delete the current backup + if(FileManager.default.fileExists(atPath: backupURL.path)){ + try! FileManager.default.removeItem(at: backupURL) + } + + // restore the previously saved backup as current backup + // (don't delete the N-1 backup yet so copy instead of move) + try! FileManager.default.copyItem(at: backupBakURL, to: backupURL) + + //perform restore of data from the backup + restore(installedApp) + } + func restore(_ installedApp: InstalledApp) { guard minimuxerStatus else { return } @@ -1423,7 +1469,10 @@ private extension MyAppsViewController do { let tempApp = context.object(with: installedApp.objectID) as! InstalledApp - tempApp.needsResign = true + tempApp.needsResign = true // why do we want to resign it during refresh ?!!!! + // I see now, so here we just mark that icon needs to be changed but leave it for refresh/install to do it + // this is bad, coz now the weight of installing goes to refresh step !!! which is not what we want + tempApp.hasAlternateIcon = (image != nil) if let image = image @@ -1459,27 +1508,27 @@ private extension MyAppsViewController } } - func enableJIT(for installedApp: InstalledApp) - { - + func enableJIT(for installedApp: InstalledApp) { let sidejitenabled = UserDefaults.standard.sidejitenable - if #unavailable(iOS 17) { + if #unavailable(iOS 17), !sidejitenabled { guard minimuxerStatus else { return } } - if #available(iOS 17, *), !sidejitenabled { - ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self) - AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp) + let error = OperationError.tooNewError as NSError + let localizedError = error.withLocalizedTitle("No iOS 17 On Device JIT!") + + ToastView(error: localizedError, opensLog: true).show(in: self) + AppManager.shared.log(error, operation: .enableJIT, app: installedApp) return } AppManager.shared.enableJIT(for: installedApp) { result in DispatchQueue.main.async { - switch result - { - case .success: break + switch result { + case .success: + break case .failure(let error): ToastView(error: error, opensLog: true).show(in: self) AppManager.shared.log(error, operation: .enableJIT, app: installedApp) @@ -1810,9 +1859,17 @@ extension MyAppsViewController self.exportBackup(for: installedApp) } - let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in + let importBackupAction = UIAction(title: NSLocalizedString("Import Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in + self.importBackup(for: installedApp) + } + + let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: "Restores the last or current backup of this app"), image: UIImage(systemName: "arrow.down.doc")) { (action) in self.restore(installedApp) } + + let restorePreviousBackupAction = UIAction(title: NSLocalizedString("Restore Previous Backup", comment: "Restores the backup saved before the current backup was created."), image: UIImage(systemName: "arrow.down.doc")) { (action) in + self.restorePreviousBackup(for: installedApp) + } let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in self.chooseIcon(for: installedApp) @@ -1878,7 +1935,8 @@ extension MyAppsViewController var outError: NSError? = nil self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in - #if DEBUG + + #if DEBUG && targetEnvironment(simulator) backupExists = true #else backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path) @@ -1903,15 +1961,21 @@ extension MyAppsViewController if installedApp.isActive { actions.append(deactivateAction) + // import backup into shared backups dir is allowed + actions.append(importBackupAction) } - #if DEBUG + // have an option to restore the n-1 backup + if FileManager.default.fileExists(atPath: getPreviousBackupURL(installedApp).path){ + actions.append(restorePreviousBackupAction) + } + + #if DEBUG && targetEnvironment(simulator) if installedApp.bundleIdentifier != StoreApp.altstoreAppID { actions.append(removeAction) } - #else if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier) @@ -1929,6 +1993,26 @@ extension MyAppsViewController #endif } + // Change the order of entries to make changes to how the context menu is displayed + let orderedActions = [ + openMenu, + refreshAction, + activateAction, + jitAction, + changeIconMenu, + backupAction, + exportBackupAction, + importBackupAction, + restoreBackupAction, + restorePreviousBackupAction, + deactivateAction, + removeAction, + ] + + // remove non-selected actions from the all-actions ordered list + // this way the declaration of the action in the above code doesn't determine the context menu order + actions = orderedActions.filter{ action in actions.contains(action)} + var title: String? if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index 730edd11..3e8fa711 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -715,9 +715,10 @@ private extension AuthenticationOperation // If we're not using the same certificate used to install AltStore, warn user that they need to refresh. guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) } -#if DEBUG - completionHandler(false) -#else +// #if DEBUG && targetEnvironment(simulator) +// completionHandler(false) +// #else + DispatchQueue.main.async { let context = AuthenticatedOperationContext(context: self.context) context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish. @@ -733,7 +734,7 @@ private extension AuthenticationOperation completionHandler(false) } } -#endif +// #endif } } diff --git a/AltStore/Operations/BackgroundRefreshAppsOperation.swift b/AltStore/Operations/BackgroundRefreshAppsOperation.swift index 47c6c1ea..0d6cbde1 100644 --- a/AltStore/Operations/BackgroundRefreshAppsOperation.swift +++ b/AltStore/Operations/BackgroundRefreshAppsOperation.swift @@ -103,7 +103,14 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result: ResultOperation guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil"))) } - if #available(iOS 17, *) { - let sideJITenabled = UserDefaults.standard.sidejitenable - let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? "" - - if sideJITenabled { - installedApp.managedObjectContext?.perform { - EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in - switch result { - case .failure(let error): - switch error { - case .invalidURL, .errorConnecting: - self.finish(.failure(OperationError.unableToConnectSideJIT)) - case .deviceNotFound: - self.finish(.failure(OperationError.unableToRespondSideJITDevice)) - case .other(let message): - if let startRange = message.range(of: "

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

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

", range: startRange.upperBound..: ResultOperation } @available(iOS 17, *) -func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result) -> Void) { +func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result) -> Void) { guard let udid = fetch_udid()?.toString() else { completion(.failure(.other("Unable to get UDID"))) return } - var SJSURL = serverurl + let serverURLWithUDID = serverURL.appendingPathComponent(udid) + let fullURL = serverURLWithUDID.appendingPathComponent(installedApp.resignedBundleIdentifier) - if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty { - SJSURL = "http://sidejitserver._http._tcp.local:8080" - } - - if !SJSURL.hasPrefix("http") { - completion(.failure(.invalidURL)) - return - } - - let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier - - let url = URL(string: fullurl)! - - let task = URLSession.shared.dataTask(with: url) {(data, response, error) in + let task = URLSession.shared.dataTask(with: fullURL) { (data, response, error) in if let error = error { completion(.failure(.errorConnecting)) return } - guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return } + guard let data = data, let dataString = String(data: data, encoding: .utf8) else { + return + } - if datastring == "Enabled JIT for '\(installedapp.name)'!" { + if dataString == "Enabled JIT for '\(installedApp.name)'!" { let content = UNMutableNotificationContent() content.title = "JIT Successfully Enabled" - content.subtitle = "JIT Enabled For \(installedapp.name)" - content.sound = UNNotificationSound.default - + content.subtitle = "JIT Enabled For \(installedApp.name)" + content.sound = .default + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil) - UNUserNotificationCenter.current().add(request) + completion(.success(())) } else { - let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring) + let errorType: SideJITServerErrorType = dataString == "Could not find device!" + ? .deviceNotFound + : .other(dataString) completion(.failure(errorType)) } } diff --git a/AltStore/Operations/Errors/OperationError.swift b/AltStore/Operations/Errors/OperationError.swift index 775ba34a..52486a1b 100644 --- a/AltStore/Operations/Errors/OperationError.swift +++ b/AltStore/Operations/Errors/OperationError.swift @@ -58,6 +58,8 @@ extension OperationError case anisetteV3Error//(message: String) case cacheClearError//(errors: [String]) case noWiFi + + case invalidOperationContext } static var cancelled: CancellationError { CancellationError() } @@ -130,6 +132,10 @@ extension OperationError OperationError(code: .invalidParameters, failureReason: message) } + static func invalidOperationContext(_ message: String? = nil) -> OperationError { + OperationError(code: .invalidOperationContext, failureReason: message) + } + static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError { OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line) } @@ -232,7 +238,10 @@ struct OperationError: ALTLocalizedError { case .invalidParameters: let message = self._failureReason.map { ": \n\($0)" } ?? "." - return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message) + return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message) + case .invalidOperationContext: + let message = self._failureReason.map { ": \n\($0)" } ?? "." + return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message) case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "") case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "") case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "") diff --git a/AltStore/Operations/FetchAnisetteDataOperation.swift b/AltStore/Operations/FetchAnisetteDataOperation.swift index 37d25eb1..93e10383 100644 --- a/AltStore/Operations/FetchAnisetteDataOperation.swift +++ b/AltStore/Operations/FetchAnisetteDataOperation.swift @@ -14,6 +14,8 @@ import AltStoreCore import AltSign import Roxas +class ANISETTE_VERBOSITY: Operation {} // dummy tag iface + @objc(FetchAnisetteDataOperation) final class FetchAnisetteDataOperation: ResultOperation, WebSocketDelegate { @@ -58,7 +60,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc UserDefaults.standard.menuAnisetteURL = urlString let url = URL(string: urlString) self.url = url - print("Anisette URL: \(self.url!.absoluteString)") + self.printOut("Anisette URL: \(self.url!.absoluteString)") if let identifier = Keychain.shared.identifier, let adiPb = Keychain.shared.adiPb { @@ -107,7 +109,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc guard let url = URL(string: currentServerUrlString) else { // Invalid URL, skip to next let errmsg = "Skipping invalid URL: \(currentServerUrlString)" - print(errmsg) + self.printOut(errmsg) showToast(viewContext: viewContext, message: errmsg) tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion) return @@ -118,7 +120,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc if success { // If the server is reachable, return the URL let okmsg = "Found working server: \(url.absoluteString)" - print(okmsg) + self.printOut(okmsg) if(currentIndex > 0){ // notify user if available server is different the user-specified one self.showToast(viewContext: viewContext, message: okmsg) @@ -127,7 +129,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc } else { // If not, try the next URL let errmsg = "Failed to reach server: \(url.absoluteString), trying next server." - print(errmsg) + self.printOut(errmsg) self.showToast(viewContext: viewContext, message: errmsg) self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion) } @@ -170,10 +172,10 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc if v3 { if json["result"] == "GetHeadersError" { let message = json["message"] - print("Error getting V3 headers: \(message ?? "no message")") + self.printOut("Error getting V3 headers: \(message ?? "no message")") if let message = message, message.contains("-45061") { - print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying") + self.printOut("Error message contains -45061 (not provisioned), resetting adi.pb and retrying") Keychain.shared.adiPb = nil return provision() } else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") } @@ -214,16 +216,16 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc if let response = response, let version = response.value(forHTTPHeaderField: "Implementation-Version") { - print("Implementation-Version: \(version)") - } else { print("No Implementation-Version header") } + self.printOut("Implementation-Version: \(version)") + } else { self.printOut("No Implementation-Version header") } - print("Anisette used: \(formattedJSON)") - print("Original JSON: \(json)") + self.printOut("Anisette used: \(formattedJSON)") + self.printOut("Original JSON: \(json)") if let anisette = ALTAnisetteData(json: formattedJSON) { - print("Anisette is valid!") + self.printOut("Anisette is valid!") self.finish(.success(anisette)) } else { - print("Anisette is invalid!!!!") + self.printOut("Anisette is invalid!!!!") if v3 { throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)") } else { @@ -242,22 +244,22 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc // MARK: - V1 func handleV1() { - print("Server is V1") + self.printOut("Server is V1") if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString { - print("Server has already been trusted, fetching anisette") + self.printOut("Server has already been trusted, fetching anisette") return self.fetchAnisetteV1() } - print("Alerting user about outdated server") + self.printOut("Alerting user about outdated server") let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in - print("Fetching anisette via V1") + self.printOut("Fetching anisette via V1") UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString self.fetchAnisetteV1() })) alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in - print("Cancelled anisette operation") + self.printOut("Cancelled anisette operation") self.finish(.failure(OperationError.cancelled)) })) @@ -273,14 +275,14 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc } func fetchAnisetteV1() { - print("Fetching anisette V1") + self.printOut("Fetching anisette V1") URLSession.shared.dataTask(with: self.url!) { data, response, error in do { guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") } try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false) } catch let error as NSError { - print("Failed to load: \(error.localizedDescription)") + self.printOut("Failed to load: \(error.localizedDescription)") self.finish(.failure(error)) } }.resume() @@ -290,7 +292,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc func provision() { fetchClientInfo { - print("Getting provisioning URLs") + self.printOut("Getting provisioning URLs") var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!) request.httpMethod = "GET" URLSession.shared.dataTask(with: request) { data, response, error in @@ -302,12 +304,12 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc let endProvisioningURL = URL(string: endProvisioningString) { self.startProvisioningURL = startProvisioningURL self.endProvisioningURL = endProvisioningURL - print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)") - print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)") - print("Starting a provisioning session") + self.printOut("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)") + self.printOut("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)") + self.printOut("Starting a provisioning session") self.startProvisioningSession() } else { - print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")") + self.printOut("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")") self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil))) } }.resume() @@ -329,19 +331,19 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc do { if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] { guard let result = json["result"] as? String else { - print("The server didn't give us a result") + self.printOut("The server didn't give us a result") client.disconnect(closeCode: 0) self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil))) return } - print("Received result: \(result)") + self.printOut("Received result: \(result)") switch result { case "GiveIdentifier": - print("Giving identifier") + self.printOut("Giving identifier") client.json(["identifier": Keychain.shared.identifier!]) case "GiveStartProvisioningData": - print("Getting start provisioning data") + self.printOut("Getting start provisioning data") let body = [ "Header": [String: Any](), "Request": [String: Any](), @@ -353,19 +355,19 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc if let data = data, let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary>, let spim = plist["Response"]?["spim"] as? String { - print("Giving start provisioning data") + self.printOut("Giving start provisioning data") client.json(["spim": spim]) } else { - print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")") + self.printOut("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")") client.disconnect(closeCode: 0) self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil))) } }.resume() case "GiveEndProvisioningData": - print("Getting end provisioning data") + self.printOut("Getting end provisioning data") guard let cpim = json["cpim"] as? String else { - print("The server didn't give us a cpim") + self.printOut("The server didn't give us a cpim") client.disconnect(closeCode: 0) self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil))) return @@ -384,20 +386,20 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary>, let ptm = plist["Response"]?["ptm"] as? String, let tk = plist["Response"]?["tk"] as? String { - print("Giving end provisioning data") + self.printOut("Giving end provisioning data") client.json(["ptm": ptm, "tk": tk]) } else { - print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")") + self.printOut("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")") client.disconnect(closeCode: 0) self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil))) } }.resume() case "ProvisioningSuccess": - print("Provisioning succeeded!") + self.printOut("Provisioning succeeded!") client.disconnect(closeCode: 0) guard let adiPb = json["adi_pb"] as? String else { - print("The server didn't give us an adi.pb file") + self.printOut("The server didn't give us an adi.pb file") self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil))) return } @@ -406,27 +408,27 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc default: if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" { - print("Failing because of \(result)") + self.printOut("Failing because of \(result)") self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String))) } } } } catch let error as NSError { - print("Failed to handle text: \(error.localizedDescription)") + self.printOut("Failed to handle text: \(error.localizedDescription)") self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil))) } case .connected: - print("Connected") + self.printOut("Connected") case .disconnected(let string, let code): - print("Disconnected: \(code); \(string)") + self.printOut("Disconnected: \(code); \(string)") case .error(let error): - print("Got error: \(String(describing: error))") + self.printOut("Got error: \(String(describing: error))") default: - print("Unknown event: \(event)") + self.printOut("Unknown event: \(event)") } } @@ -460,10 +462,10 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc self.mdLu != nil && self.deviceId != nil && Keychain.shared.identifier != nil { - print("Skipping client_info fetch since all the properties we need aren't nil") + self.printOut("Skipping client_info fetch since all the properties we need aren't nil") return callback() } - print("Trying to get client_info") + self.printOut("Trying to get client_info") let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info") URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in do { @@ -473,20 +475,20 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] { if let clientInfo = json["client_info"] { - print("Server is V3") + self.printOut("Server is V3") self.clientInfo = clientInfo self.userAgent = json["user_agent"]! - print("Client-Info: \(self.clientInfo!)") - print("User-Agent: \(self.userAgent!)") + self.printOut("Client-Info: \(self.clientInfo!)") + self.printOut("User-Agent: \(self.userAgent!)") if Keychain.shared.identifier == nil { - print("Generating identifier") + self.printOut("Generating identifier") var bytes = [Int8](repeating: 0, count: 16) let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) if status != errSecSuccess { - print("ERROR GENERATING IDENTIFIER!!! \(status)") + self.printOut("ERROR GENERATING IDENTIFIER!!! \(status)") return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil))) } @@ -495,16 +497,16 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc let decoded = Data(base64Encoded: Keychain.shared.identifier!)! self.mdLu = decoded.sha256().hexEncodedString() - print("X-Apple-I-MD-LU: \(self.mdLu!)") + self.printOut("X-Apple-I-MD-LU: \(self.mdLu!)") let uuid: UUID = decoded.object() self.deviceId = uuid.uuidString.uppercased() - print("X-Mme-Device-Id: \(self.deviceId!)") + self.printOut("X-Mme-Device-Id: \(self.deviceId!)") callback() } else { self.handleV1() } } else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) } } catch let error as NSError { - print("Failed to load: \(error.localizedDescription)") + self.printOut("Failed to load: \(error.localizedDescription)") self.handleV1() } }.resume() @@ -512,7 +514,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc func fetchAnisetteV3(_ identifier: String, _ adiPb: String) { fetchClientInfo { - print("Fetching anisette V3") + self.printOut("Fetching anisette V3") let url = UserDefaults.standard.menuAnisetteURL var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers")) request.httpMethod = "POST" @@ -527,12 +529,21 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true) } catch let error as NSError { - print("Failed to load: \(error.localizedDescription)") + self.printOut("Failed to load: \(error.localizedDescription)") self.finish(.failure(error)) } }.resume() } } + + + private func printOut(_ text: String?){ + let isInternalLoggingEnabled = OperationsLoggingControl.getFromDatabase(for: ANISETTE_VERBOSITY.self) + if(isInternalLoggingEnabled){ + // logging enabled, so log it + text.map{ _ in print(text!) } ?? print() + } + } } extension WebSocketClient { diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift index 990914c2..096110c2 100644 --- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift +++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift @@ -13,15 +13,16 @@ import AltSign import Roxas @objc(FetchProvisioningProfilesOperation) -final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]> +class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]> { let context: AppOperationContext var additionalEntitlements: [ALTEntitlement: Any]? - private let appGroupsLock = NSLock() + internal let appGroupsLock = NSLock() - init(context: AppOperationContext) + // this class is abstract or shouldn't be instantiated outside, use the subclasses + fileprivate init(context: AppOperationContext) { self.context = context @@ -40,11 +41,13 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv return } - guard - let team = self.context.team, - let session = self.context.session - else { - return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) } + guard let team = self.context.team, + let session = self.context.session else { + + return self.finish(.failure( + OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil")) + ) + } guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) } @@ -120,7 +123,11 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv extension FetchProvisioningProfilesOperation { - func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + private func prepareProvisioningProfile(for app: ALTApplication, + parentApp: ALTApplication?, + team: ALTTeam, + session: ALTAppleAPISession, c + completionHandler: @escaping (Result) -> Void) { DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in @@ -134,19 +141,21 @@ extension FetchProvisioningProfilesOperation // or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier. let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier)) -// #if DEBUG -// -// if app.isAltStoreApp -// { -// // Use legacy bundle ID format for AltStore. -// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil -// } -// else -// { -// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil -// } -// -// #else + // TODO: @mahee96: Try to keep the debug build and release build operations similar, refactor later with proper reasoning + // for now, restricted it to debug on simulator only + #if DEBUG && targetEnvironment(simulator) + + if app.isAltStoreApp + { + // Use legacy bundle ID format for AltStore. + preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil + } + else + { + preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil + } + + #else if teamsMatch { @@ -160,7 +169,7 @@ extension FetchProvisioningProfilesOperation preferredBundleID = nil } - // #endif + #endif } else { @@ -211,35 +220,22 @@ extension FetchProvisioningProfilesOperation { case .failure(let error): completionHandler(.failure(error)) case .success(let appID): - - // Update features - self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let appID): - - // Update app groups - self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let appID): - - // Fetch Provisioning Profile - self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in - completionHandler(result) - } - } - } - } - } + + //process + self.fetchProvisioningProfile( + for: appID, team: team, session: session, completionHandler: completionHandler + ) } } } } - func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + private func registerAppID(for application: ALTApplication, + name: String, + bundleIdentifier: String, + team: ALTTeam, + session: ALTAppleAPISession, + completionHandler: @escaping (Result) -> Void) { ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in do @@ -333,7 +329,81 @@ extension FetchProvisioningProfilesOperation } } - func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + internal func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in + switch Result(profile, error) + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let profile): + + // Delete existing profile + ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in + switch Result(success, error) + { + case .failure: + // As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it. + // So instead, we just return the fetched profile from above. + completionHandler(.success(profile)) + + case .success: + Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).") + + // Fetch new provisioning profile + ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in + completionHandler(Result(profile, error)) + } + } + } + } + } + } +} + +class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable { + override init(context: AppOperationContext) + { + super.init(context: context) + } +} + +class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{ + override init(context: AppOperationContext) + { + super.init(context: context) + } + + // modify Operations are allowed for the app groups and other stuffs + func fetchProvisioningProfile(appID: ALTAppID, + for app: ALTApplication, + team: ALTTeam, + session: ALTAppleAPISession, + completionHandler: @escaping (Result) -> Void) + { + + // Update features + self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Update app groups + self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Fetch Provisioning Profile + super.fetchProvisioningProfile(for: appID, team: team, session: session, completionHandler: completionHandler) + } + } + } + } + } + + private func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { var entitlements = app.entitlements for (key, value) in additionalEntitlements ?? [:] @@ -412,7 +482,7 @@ extension FetchProvisioningProfilesOperation } } - func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + private func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { var entitlements = app.entitlements for (key, value) in additionalEntitlements ?? [:] @@ -511,7 +581,7 @@ extension FetchProvisioningProfilesOperation Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).") groups.append(group) - case .failure(let error): + case .failure(let error): Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)") errors.append(error) } @@ -547,34 +617,4 @@ extension FetchProvisioningProfilesOperation } } } - - func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) - { - ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in - switch Result(profile, error) - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let profile): - - // Delete existing profile - ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in - switch Result(success, error) - { - case .failure: - // As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it. - // So instead, we just return the fetched profile from above. - completionHandler(.success(profile)) - - case .success: - Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).") - - // Fetch new provisioning profile - ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in - completionHandler(Result(profile, error)) - } - } - } - } - } - } } diff --git a/AltStore/Operations/Operation.swift b/AltStore/Operations/Operation.swift index 0bf322dc..4ba9c94f 100644 --- a/AltStore/Operations/Operation.swift +++ b/AltStore/Operations/Operation.swift @@ -38,10 +38,14 @@ class ResultOperation: Operation result = .failure(error) } - // diagnostics logging - let resultStatus = String(describing: result).prefix("success".count).uppercased() - print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" + - " Result: \(result)\n") + // Diagnostics: perform verbose logging of the operations only if enabled (so as to not flood console logs) + let isLoggingEnabledForThisOperation = OperationsLoggingControl.getFromDatabase(for: type(of: self)) + if UserDefaults.standard.isVerboseOperationsLoggingEnabled && isLoggingEnabledForThisOperation { + // diagnostics logging + let resultStatus = String(describing: result).prefix("success".count).uppercased() + print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" + + " Result: \(result)\n") + } self.resultHandler?(result) diff --git a/AltStore/Operations/RemoveAppBackupOperation.swift b/AltStore/Operations/RemoveAppBackupOperation.swift index 45827c01..113b130a 100644 --- a/AltStore/Operations/RemoveAppBackupOperation.swift +++ b/AltStore/Operations/RemoveAppBackupOperation.swift @@ -58,17 +58,18 @@ final class RemoveAppBackupOperation: ResultOperation } catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile { - #if DEBUG - - // When debugging, it's expected that app groups don't match, so ignore. - self.finish(.success(())) - - #else + // TODO: @mahee96: Find out why should in debug builds the app-groups is not expected to match +// #if DEBUG +// +// // When debugging, it's expected that app groups don't match, so ignore. +// self.finish(.success(())) +// +// #else Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)") self.finish(.failure(error)) - #endif +// #endif } catch { diff --git a/AltStore/Operations/RemoveAppExtensionsOperation.swift b/AltStore/Operations/RemoveAppExtensionsOperation.swift new file mode 100644 index 00000000..da12500a --- /dev/null +++ b/AltStore/Operations/RemoveAppExtensionsOperation.swift @@ -0,0 +1,219 @@ +// +// RefreshAppOperation.swift +// AltStore +// +// Created by Riley Testut on 2/27/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltStoreCore +import Roxas +import AltSign + +@objc(RemoveAppExtensionsOperation) +final class RemoveAppExtensionsOperation: ResultOperation +{ + let context: AppOperationContext + let localAppExtensions: Set? + + init(context: AppOperationContext, localAppExtensions: Set?) + { + self.context = context + self.localAppExtensions = localAppExtensions + super.init() + } + + override func main() + { + super.main() + + if let error = self.context.error + { + self.finish(.failure(error)) + return + } + + guard let targetAppBundle = context.app else { + return self.finish(.failure( + OperationError.invalidParameters("RemoveAppExtensionsOperation: context.app is nil") + )) + } + + self.removeAppExtensions(from: targetAppBundle, + localAppExtensions: localAppExtensions, + extensions: targetAppBundle.appExtensions, + context.authenticatedContext.presentingViewController) + + } + + + private static func removeExtensions(from extensions: Set) throws { + for appExtension in extensions { + print("Deleting extension \(appExtension.bundleIdentifier)") + try FileManager.default.removeItem(at: appExtension.fileURL) + } + } + + + + private func removeAppExtensions(from targetAppBundle: ALTApplication, + localAppExtensions: Set?, + extensions: Set, + _ presentingViewController: UIViewController?) + { + + // target App Bundle doesn't contain extensions so don't bother + guard !targetAppBundle.appExtensions.isEmpty else { + return self.finish(.success(())) + } + + // process extensionsInfo + let excessExtensions = processExtensionsInfo(from: targetAppBundle, localAppExtensions: localAppExtensions) + + DispatchQueue.main.async { + guard let presentingViewController: UIViewController = presentingViewController, + presentingViewController.viewIfLoaded?.window != nil else { + // background mode: remove only the excess extensions automatically for re-installs + // keep all extensions for fresh install (localAppBundle = nil) + return self.backgroundModeExtensionsCleanup(excessExtensions: excessExtensions) + } + + // present prompt to the user if we have a view context + let alertController = self.createAlertDialog(from: targetAppBundle, extensions: extensions, presentingViewController) + presentingViewController.present(alertController, animated: true){ + + // if for any reason the view wasn't presented, then just signal that as error + if presentingViewController.presentedViewController == nil { + let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." + + "\nDid you move to different screen or background after starting the operation?" + self.finish(.failure( + OperationError.invalidOperationContext(errMsg) + )) + } + } + } + } + + private func createAlertDialog(from targetAppBundle: ALTApplication, + extensions: Set, + _ presentingViewController: UIViewController) -> UIAlertController + { + + /// Foreground prompt: + let firstSentence: String + + if UserDefaults.standard.activeAppLimitIncludesExtensions + { + firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "") + } + else + { + firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "") + } + + let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "") + + + + let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in + self.finish(.failure(OperationError.cancelled)) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in + self.finish(.success(())) + }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in + do { + try Self.removeExtensions(from: targetAppBundle.appExtensions) + return self.finish(.success(())) + } catch { + return self.finish(.failure(error)) + } + }) + + + + alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in + + + let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in + do { + try Self.removeExtensions(from: Set(selection)) + return self.finish(.success(())) + } catch { + return self.finish(.failure(error)) + } + } + + let suiview = popoverContentController.view! + suiview.translatesAutoresizingMaskIntoConstraints = false + + popoverContentController.modalPresentationStyle = .popover + + if let popoverPresentationController = popoverContentController.popoverPresentationController { + popoverPresentationController.sourceView = presentingViewController.view + popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4) + popoverPresentationController.delegate = popoverContentController + + DispatchQueue.main.async { + presentingViewController.present(popoverContentController, animated: true) + } + }else{ + self.finish(.failure( + OperationError.invalidParameters("RemoveAppExtensionsOperation: popoverContentController.popoverPresentationController is nil")) + ) + } + }) + + return alertController + } + + struct ExtensionsInfo{ + let excessInTarget: Set + let necessaryInExisting: Set + } + + private func processExtensionsInfo(from targetAppBundle: ALTApplication, + localAppExtensions: Set?) -> Set + { + //App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match + let targetAppEx: Set = targetAppBundle.appExtensions + let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier} + + guard let extensionsInExistingApp = localAppExtensions else { + let diagnosticsMsg = "RemoveAppExtensionsOperation: ExistingApp is nil, Hence keeping all app extensions from targetAppBundle" + + "RemoveAppExtensionsOperation: ExistingAppEx: nil; targetAppBundleEx: \(targetAppExNames)" + print(diagnosticsMsg) + return Set() // nothing is excess since we are keeping all, so returning empty + } + + let existingAppEx: Set = extensionsInExistingApp + let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier} + + let excessExtensionsInTargetApp = targetAppEx.filter{ + !(existingAppExNames.contains($0.bundleIdentifier)) + } + + let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty + let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in localAppBundle and targetAppBundle are matching: \(isMatching)\n" + + "RemoveAppExtensionsOperation: \nlocalAppBundleEx: \(existingAppExNames); \ntargetAppBundleEx: \(String(describing: targetAppExNames))\n" + print(diagnosticsMsg) + + return excessExtensionsInTargetApp + } + + private func backgroundModeExtensionsCleanup(excessExtensions: Set) { + // perform silent extensions cleanup for those that aren't already present in existing app + print("\n Performing background mode Extensions removal \n") + print("RemoveAppExtensionsOperation: Excess Extensions In TargetAppBundle: \(excessExtensions.map{$0.bundleIdentifier})") + + do { + try Self.removeExtensions(from: excessExtensions) + return self.finish(.success(())) + } catch { + return self.finish(.failure(error)) + } + } +} diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index e4b899ad..7f165297 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -244,6 +244,7 @@ private extension ResignAppOperation { for case let fileURL as URL in enumerator { + // for both sim and device, in debug mode builds, remove the tests bundles (if any) #if DEBUG guard !fileURL.lastPathComponent.lowercased().contains(".xctest") else { // Remove embedded XCTest (+ dSYM) bundle from copied app bundle. diff --git a/AltStore/Settings/Error Log/ConsoleLogView.swift b/AltStore/Settings/Error Log/ConsoleLogView.swift new file mode 100644 index 00000000..772560cf --- /dev/null +++ b/AltStore/Settings/Error Log/ConsoleLogView.swift @@ -0,0 +1,226 @@ +// +// ConsoleLogView.swift +// AltStore +// +// Created by Magesh K on 29/12/24. +// Copyright © 2024 SideStore. All rights reserved. +// +import SwiftUI + +class ConsoleLogViewModel: ObservableObject { + @Published var logLines: [String] = [] + + @Published var searchTerm: String = "" + @Published var currentSearchIndex: Int = 0 + @Published var searchResults: [Int] = [] // Stores indices of matching lines + + private var fileWatcher: DispatchSourceFileSystemObject? + + private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background) + private var logURL: URL + + init(logURL: URL) { + self.logURL = logURL + startFileWatcher() // Start monitoring the log file for changes + reloadLogData() // Load initial log data + } + + private func startFileWatcher() { + let fileDescriptor = open(logURL.path, O_RDONLY) + guard fileDescriptor != -1 else { + print("Unable to open file for reading.") + return + } + + fileWatcher = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: backgroundQueue) + fileWatcher?.setEventHandler { + self.reloadLogData() + } + fileWatcher?.resume() + } + + private func reloadLogData() { + if let fileContents = try? String(contentsOf: logURL) { + let lines = fileContents.split(whereSeparator: \.isNewline).map { String($0) } + DispatchQueue.main.async { + self.logLines = lines + } + } + } + + deinit { + fileWatcher?.cancel() + } + + + func performSearch() { + searchResults = logLines.enumerated() + .filter { $0.element.localizedCaseInsensitiveContains(searchTerm) } + .map { $0.offset } + } + + func nextSearchResult() { + guard !searchResults.isEmpty else { return } + currentSearchIndex = (currentSearchIndex + 1) % searchResults.count + } + + func previousSearchResult() { + guard !searchResults.isEmpty else { return } + currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count + } +} + + +public struct ConsoleLogView: View { + + @ObservedObject var viewModel: ConsoleLogViewModel + @State private var scrollToBottom: Bool = false // State variable to trigger scroll + @State private var searchBarState: Bool = false + @FocusState private var isSearchFieldFocused: Bool + + @State private var searchText: String = "" + @State private var scrollToIndex: Int? + + private let resultHighlightColor = Color.orange + private let resultHighlightOpacity = 0.5 + private let otherResultsColor = Color.yellow + private let otherResultsOpacity = 0.3 + + init(logURL: URL) { + self.viewModel = ConsoleLogViewModel(logURL: logURL) + } + + public var body: some View { + VStack { + + // Custom Header Bar (similar to QuickLook's preview screen) + HStack { + Text("Console Log") + .font(.system(size: 22, weight: .semibold)) + .foregroundColor(.white) + Spacer() + + if(!searchBarState){ + SwiftUI.Button(action: { + searchBarState.toggle() + }) { + Image(systemName: "magnifyingglass") + .foregroundColor(.white) + .imageScale(.large) + } + .padding(.trailing) + } + SwiftUI.Button(action: { + scrollToBottom.toggle() + }) { + Image(systemName: "ellipsis") + .foregroundColor(.white) + .imageScale(.large) + } + } + .padding(15) + .padding(.top, 5) + .padding(.bottom, 2.5) + .background(Color.black.opacity(0.9)) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom + ) + + if(searchBarState){ + // Search bar + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + .padding(.trailing, 4) + + TextField("Search", text: $searchText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onChange(of: searchText) { newValue in + viewModel.searchTerm = newValue + viewModel.performSearch() + } + .keyboardShortcut("f", modifiers: .command) // Focus search field + + if !searchText.isEmpty { + // Search navigation buttons + SwiftUI.Button(action: { + viewModel.previousSearchResult() + scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex] + }) { + Image(systemName: "chevron.up") + } + .keyboardShortcut(.return, modifiers: [.command, .shift]) + .disabled(viewModel.searchResults.isEmpty) + + SwiftUI.Button(action: { + viewModel.nextSearchResult() + scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex] + }) { + Image(systemName: "chevron.down") + } + .keyboardShortcut(.return, modifiers: .command) + .disabled(viewModel.searchResults.isEmpty) + + // Results counter + Text("\(viewModel.currentSearchIndex + 1)/\(viewModel.searchResults.count)") + .foregroundColor(.gray) + .font(.caption) + } + + SwiftUI.Button(action: { + searchBarState.toggle() + }) { + Image(systemName: "xmark") + } + } + .padding(.horizontal, 15) + } + + + + // Main Log Display (scrollable area) + ScrollView(.vertical) { + ScrollViewReader { proxy in + LazyVStack(alignment: .leading, spacing: 4) { + ForEach(viewModel.logLines.indices, id: \.self) { index in + Text(viewModel.logLines[index]) + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(.white) + .background( + viewModel.searchResults.contains(index) ? + otherResultsColor.opacity(otherResultsOpacity) : Color.clear + ) + .background( + viewModel.searchResults[safe: viewModel.currentSearchIndex] == index ? + resultHighlightColor.opacity(resultHighlightOpacity) : Color.clear + ) + } + } + .onChange(of: scrollToIndex) { newIndex in + if let index = newIndex { + withAnimation { + proxy.scrollTo(index, anchor: .center) + } + } + } + .onChange(of: scrollToBottom) { _ in + viewModel.logLines.indices.last.map { last in + proxy.scrollTo(last, anchor: .bottom) + } + } + } + } + } + .background(Color.black) // Set background color to mimic QL's dark theme + .edgesIgnoringSafeArea(.all) + } +} + +// Helper extension for safe array access +extension Array { + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } +} diff --git a/AltStore/Settings/Error Log/ErrorLogViewController.swift b/AltStore/Settings/Error Log/ErrorLogViewController.swift index 498609c7..546299b2 100644 --- a/AltStore/Settings/Error Log/ErrorLogViewController.swift +++ b/AltStore/Settings/Error Log/ErrorLogViewController.swift @@ -16,6 +16,7 @@ import Roxas import Nuke import QuickLook +import SwiftUI final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate { @@ -59,8 +60,46 @@ final class ErrorLogViewController: UITableViewController, QLPreviewControllerDe // Assign just clearLogButton to hide export button. self.navigationItem.rightBarButtonItems = [self.clearLogButton] } + +// // Adjust the width of the right bar button items +// adjustRightBarButtonWidth() } + +// func adjustRightBarButtonWidth() { +// // Access the current rightBarButtonItems +// if let rightBarButtonItems = self.navigationItem.rightBarButtonItems { +// for barButtonItem in rightBarButtonItems { +// // Check if the button is a system button, and if so, replace it with a custom button +// if barButtonItem.customView == nil { +// // Replace with a custom UIButton for each bar button item +// let customButton = UIButton(type: .custom) +// if let image = barButtonItem.image { +// customButton.setImage(image, for: .normal) +// } +// if let action = barButtonItem.action{ +// customButton.addTarget(barButtonItem.target, action: action, for: .touchUpInside) +// } +// +// // Calculate the original size based on the system button +// let originalSize = customButton.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)) +// +// let scaleFactor = 0.7 +// +// // Scale the size by 0.7 (70%) +// let scaledSize = CGSize(width: originalSize.width * scaleFactor, height: originalSize.height * scaleFactor) +// +// // Adjust the custom button's width +//// customButton.frame.size = CGSize(width: 22, height: 22) // Adjust width as needed +// customButton.frame.size = scaledSize // Adjust width as needed +// +// // Set the custom button as the custom view for the UIBarButtonItem +// barButtonItem.customView = customButton +// } +// } +// } +// } + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return } @@ -216,15 +255,86 @@ private extension ErrorLogViewController } } - @IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem) - { - // Show minimuxer.log - let previewController = QLPreviewController() - previewController.dataSource = self - let navigationController = UINavigationController(rootViewController: previewController) - present(navigationController, animated: true, completion: nil) + + enum LogView: String { + case consoleLog = "console-log" + case minimuxerLog = "minimuxer-log" + + // This class will manage the QLPreviewController and the timer. + private class LogViewManager { + var previewController: QLPreviewController + var refreshTimer: Timer? + var logView: LogView + + init(previewController: QLPreviewController, logView: LogView) { + self.previewController = previewController + self.logView = logView + } + + // Start refreshing the preview controller every second + func startRefreshing() { + refreshTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(refreshPreview), userInfo: nil, repeats: true) + } + + @objc private func refreshPreview() { + previewController.reloadData() + } + + // Stop the timer to prevent leaks + func stopRefreshing() { + refreshTimer?.invalidate() + refreshTimer = nil + } + + func updateLogPath() { + // Force the QLPreviewController to reload by changing the file path + previewController.reloadData() + } + } + + // Method to get the QLPreviewController for this log type + func getViewController(_ dataSource: QLPreviewControllerDataSource) -> QLPreviewController { + let previewController = QLPreviewController() + previewController.restorationIdentifier = self.rawValue + previewController.dataSource = dataSource + + // Create LogViewManager and start refreshing + let manager = LogViewManager(previewController: previewController, logView: self) +// manager.startRefreshing() // DO NOT REFRESH the full view contents causing flickering + + return previewController + } + + func getLogPath() -> URL { + switch self { + case .consoleLog: + let appDelegate = UIApplication.shared.delegate as! AppDelegate + return appDelegate.consoleLog.logFileURL + case .minimuxerLog: + return FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log") + } + } } + @IBAction func showConsoleLogs(_ sender: UIBarButtonItem) { + // Create the SwiftUI ConsoleLogView with the URL + let consoleLogView = ConsoleLogView(logURL: (UIApplication.shared.delegate as! AppDelegate).consoleLog.logFileURL) + + // Create the UIHostingController + let consoleLogController = UIHostingController(rootView: consoleLogView) + + // Configure the bottom sheet presentation + consoleLogController.modalPresentationStyle = .pageSheet + if let sheet = consoleLogController.sheetPresentationController { + sheet.detents = [.medium(), .large()] // You can adjust the size of the sheet (medium/large) + sheet.prefersGrabberVisible = true // Optional: Shows a grabber at the top of the sheet + sheet.selectedDetentIdentifier = .large // Default size when presented + } + + // Present the bottom sheet + present(consoleLogController, animated: true, completion: nil) + } + @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) @@ -285,58 +395,74 @@ private extension ErrorLogViewController self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError) } - @available(iOS 15, *) - @IBAction func exportDetailedLog(_ sender: UIBarButtonItem) + @IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem) { - self.exportLogButton.isIndicatingActivity = true - - Task.detached(priority: .userInitiated) { - do - { - let store = try OSLogStore(scope: .currentProcessIdentifier) - - // All logs since the app launched. - let position = store.position(timeIntervalSinceLatestBoot: 0) - let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem) - - let entries = try store.getEntries(at: position, matching: predicate) - .compactMap { $0 as? OSLogEntryLog } - .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" } - - let outputText = entries.joined(separator: "\n") - - let outputDirectory = FileManager.default.uniqueTemporaryURL() - try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - - let outputURL = outputDirectory.appendingPathComponent("altstore.log") - try outputText.write(to: outputURL, atomically: true, encoding: .utf8) - - await MainActor.run { - self._exportedLogURL = outputURL - - let previewController = QLPreviewController() - previewController.delegate = self - previewController.dataSource = self - previewController.view.tintColor = .altPrimary - self.present(previewController, animated: true) - } - } - catch - { - Logger.main.error("Failed to export OSLog entries. \(error.localizedDescription, privacy: .public)") - - await MainActor.run { - let alertController = UIAlertController(title: NSLocalizedString("Unable to Export Detailed Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert) - alertController.addAction(.ok) - self.present(alertController, animated: true) - } - } - - await MainActor.run { - self.exportLogButton.isIndicatingActivity = false - } - } + // Show minimuxer.log + let previewController = LogView.minimuxerLog.getViewController(self) + let navigationController = UINavigationController(rootViewController: previewController) + present(navigationController, animated: true, completion: nil) } + +// @available(iOS 15, *) +// @IBAction func exportDetailedLog(_ sender: UIBarButtonItem) +// { +// self.exportLogButton.isIndicatingActivity = true +// +// Task.detached(priority: .userInitiated) { +// do +// { +// let store = try OSLogStore(scope: .currentProcessIdentifier) +// +// // All logs since the app launched. +// let position = store.position(timeIntervalSinceLatestBoot: 0) +//// let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem) +//// +//// let entries = try store.getEntries(at: position, matching: predicate) +//// .compactMap { $0 as? OSLogEntryLog } +//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" } +//// +// // Remove the predicate to get all log entries +//// let entries = try store.getEntries(at: position) +//// .compactMap { $0 as? OSLogEntryLog } +//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" } +// +// let entries = try store.getEntries(at: position) +// +//// let outputText = entries.joined(separator: "\n") +// let outputText = entries.map { $0.description }.joined(separator: "\n") +// +// let outputDirectory = FileManager.default.uniqueTemporaryURL() +// try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) +// +// let outputURL = outputDirectory.appendingPathComponent("altstore.log") +// try outputText.write(to: outputURL, atomically: true, encoding: .utf8) +// +// await MainActor.run { +// self._exportedLogURL = outputURL +// +// let previewController = QLPreviewController() +// previewController.delegate = self +// previewController.dataSource = self +// previewController.view.tintColor = .altPrimary +// self.present(previewController, animated: true) +// } +// } +// catch +// { +// Logger.main.error("Failed to export OSLog entries. \(error.localizedDescription, privacy: .public)") +// +// await MainActor.run { +// let alertController = UIAlertController(title: NSLocalizedString("Unable to Export Detailed Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert) +// alertController.addAction(.ok) +// self.present(alertController, animated: true) +// } +// } +// +// await MainActor.run { +// self.exportLogButton.isIndicatingActivity = false +// } +// } +// } } extension ErrorLogViewController @@ -412,9 +538,13 @@ extension ErrorLogViewController: QLPreviewControllerDataSource { return 1 } - func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { - let fileURL = FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log") - return fileURL as QLPreviewItem + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem + { + guard let identifier = controller.restorationIdentifier, + let logView = LogView(rawValue: identifier) else { + fatalError("Invalid restorationIdentifier") + } + return logView.getLogPath() as QLPreviewItem } } diff --git a/AltStore/Settings/OperationsLoggingContolView.swift b/AltStore/Settings/OperationsLoggingContolView.swift new file mode 100644 index 00000000..84680809 --- /dev/null +++ b/AltStore/Settings/OperationsLoggingContolView.swift @@ -0,0 +1,284 @@ +// +// SettingsView.swift +// AltStore +// +// Created by Magesh K on 14/01/25. +// Copyright © 2025 SideStore. All rights reserved. +// + + +import SwiftUI +import AltStoreCore + + +private final class DummyConformance: EnableJITContext +{ + private init(){} // non instantiatable + var installedApp: AltStoreCore.InstalledApp? + var error: (any Error)? +} + + +struct OperationsLoggingControlView: View { + let TITLE = "Operations Logging" + let BACKGROUND_COLOR = Color(.settingsBackground) + + var viewModel = OperationsLoggingControl() + + var body: some View { + NavigationView { + ZStack { +// BACKGROUND_COLOR.ignoresSafeArea() // Force background to cover the entire screen + VStack{ + Group{}.padding(12) + + CustomList { + CustomSection(header: Text("Install Operations")) + { + CustomToggle("1. Authentication", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: AuthenticationOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: AuthenticationOperation.self, value: value) + } + )) + + CustomToggle("2. VerifyAppPledge", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: VerifyAppPledgeOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: VerifyAppPledgeOperation.self, value: value) + } + )) + + CustomToggle("3. DownloadApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: DownloadAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: DownloadAppOperation.self, value: value) + } + )) + + CustomToggle("4. VerifyApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: VerifyAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: VerifyAppOperation.self, value: value) + } + )) + + CustomToggle("5. RemoveAppExtensions", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: RemoveAppExtensionsOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: RemoveAppExtensionsOperation.self, value: value) + } + )) + + CustomToggle("6. FetchAnisetteData", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: FetchAnisetteDataOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: FetchAnisetteDataOperation.self, value: value) + } + )) + + CustomToggle("7. FetchProvisioningProfiles(I)", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesInstallOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: FetchProvisioningProfilesInstallOperation.self, value: value) + } + )) + + CustomToggle("8. ResignApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: ResignAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: ResignAppOperation.self, value: value) + } + )) + + CustomToggle("9. SendApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: SendAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: SendAppOperation.self, value: value) + } + )) + + CustomToggle("10. InstallApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: InstallAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: InstallAppOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Refresh Operations")) + { + CustomToggle("1. FetchProvisioningProfiles(R)", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesRefreshOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: FetchProvisioningProfilesRefreshOperation.self, value: value) + } + )) + + CustomToggle("2. RefreshApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: RefreshAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: RefreshAppOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("AppIDs related Operations")) + { + CustomToggle("1. FetchAppIDs", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: FetchAppIDsOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: FetchAppIDsOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Sources related Operations")) + { + CustomToggle("1. FetchSource", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: FetchSourceOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: FetchSourceOperation.self, value: value) + } + )) + + CustomToggle("2. UpdateKnownSources", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: UpdateKnownSourcesOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: UpdateKnownSourcesOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Backup Operations")) + { + CustomToggle("1. BackupApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: BackupAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: BackupAppOperation.self, value: value) + } + )) + + CustomToggle("2. RemoveAppBackup", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: RemoveAppBackupOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: RemoveAppBackupOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Activate/Deactive Operations")) + { + CustomToggle("1. RemoveApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: RemoveAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: RemoveAppOperation.self, value: value) + } + )) + CustomToggle("2. DeactivateApp", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: DeactivateAppOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: DeactivateAppOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Background refresh Operations")) + { + CustomToggle("1. BackgroundRefreshApps", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: BackgroundRefreshAppsOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: BackgroundRefreshAppsOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Enable JIT Operations")) + { + CustomToggle("1. EnableJIT", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: EnableJITOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: EnableJITOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Patrons Operations")) + { + CustomToggle("1. UpdatePatrons", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: UpdatePatronsOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: UpdatePatronsOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Cache Operations")) + { + CustomToggle("1. ClearAppCache", isOn: Binding( + get: { self.viewModel.getFromDatabase(for: ClearAppCacheOperation.self) }, + set: { value in + self.viewModel.updateDatabase(for: ClearAppCacheOperation.self, value: value) + } + )) + } + + CustomSection(header: Text("Misc Logging")) + { + CustomToggle("1. Anisette Internal Logging", isOn: Binding( + // enable anisette internal logging by default since it was already printing before + get: { OperationsLoggingControl.getUpdatedFromDatabase( + for: ANISETTE_VERBOSITY.self, defaultVal: true + )}, + set: { value in + self.viewModel.updateDatabase(for: ANISETTE_VERBOSITY.self, value: value) + } + )) + } + } + } + } + .navigationTitle(TITLE) + } + .ignoresSafeArea(edges: .all) + } + + private func CustomList(@ViewBuilder content: () -> Content) -> some View { +// ScrollView { + List { + content() + } +// .listStyle(.plain) +// .listStyle(InsetGroupedListStyle()) // Or PlainListStyle for iOS 15 +// .background(Color.clear) +// .background(Color(.settingsBackground)) +// .onAppear(perform: { +// // cache the current background color +// UITableView.appearance().backgroundColor = UIColor.red +// }) +// .onDisappear(perform: { +// // reset the background color to the cached value +// UITableView.appearance().backgroundColor = UIColor.systemBackground +// }) + } + + private func CustomSection(header: Text, @ViewBuilder content: () -> Content) -> some View { + Section(header: header) { + content() + } +// .listRowBackground(Color.clear) + } + + private func CustomToggle(_ title: String, isOn: Binding) -> some View { + Toggle(title, isOn: isOn) + .padding(3) +// .foregroundColor(.white) // Ensures text color is always white +// .font(.headline) + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + OperationsLoggingControlView() + } +} diff --git a/AltStore/Settings/PatreonViewController.swift b/AltStore/Settings/PatreonViewController.swift index 92f7bdb5..dd945161 100644 --- a/AltStore/Settings/PatreonViewController.swift +++ b/AltStore/Settings/PatreonViewController.swift @@ -318,7 +318,8 @@ extension PatreonViewController case .none: footerView.button.isIndicatingActivity = true case .success?: footerView.button.isHidden = true case .failure?: - #if DEBUG + // In simulator debug builds only enable debug mode flag + #if DEBUG && targetEnvironment(simulator) let debug = true #else let debug = false diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index beede7ed..7a7baddf 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -4,6 +4,7 @@ + @@ -21,7 +22,7 @@ - + -