Compare commits

..

2 Commits

Author SHA1 Message Date
Joseph Mattello
93b1c4d834 WireGuard add extra source files
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:56:43 -05:00
Joseph Mattello
e96245b9d8 WireGuard extension
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:56:22 -05:00
184 changed files with 4732 additions and 5700 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD * @JoeMatt @lonkelle

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Please send logs generated with [idevicedebug](https://github.com/libimobiledevice/libimobiledevice) or Xcode. We will close the issue if a log is missing.
**iDevice (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,39 +0,0 @@
name: Bug Report
description: Report a bug
title: "[BUG] "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea
id: description
attributes:
label: Describe the bug
description: What is the bug and how did you discover it?
placeholder: Please be clear and concise with your description.
validations:
required: true
- type: textarea
id: how-to-reproduce
attributes:
label: Instructions to reproduce
description: Please include clear and consistent instructions for reproducing the bug to make it easier for us to fix it.
validations:
required: true
- type: input
id: app-version
attributes:
label: What version of SideStore are you using?
description: To retrieve this, go to `Settings` in the SideStore app and scroll down to the bottom.
validations:
required: true
- type: textarea
id: other-info
attributes:
label: Other info
description: If you have any other comments, other info that might be useful, or if you found a workaround, please put it here.

View File

@@ -1,10 +0,0 @@
# force issue template usage
blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/sidestore-949183273383395328
about: If you need support, please go here first instead of making an issue!
- name: GitHub Discussions
url: https://github.com/SideStore/SideStore/discussions
about: As an alternative to Discord, you can also make a new GitHub discussion.

View File

@@ -1,32 +0,0 @@
name: Feature Request
description: Suggest a feature
title: "[FEATURE REQUEST] "
labels: ["enhancement"]
assignees: []
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea
id: description
attributes:
label: Describe the feature
description: What is the feature? How would it work?
placeholder: Please be clear and concise with your description.
validations:
required: true
- type: textarea
id: use-cases
attributes:
label: Use cases
description: Please include multiple use cases where this feature would be useful.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives
description: If you have alternative ideas of how this feature could work, you can put them here.

View File

@@ -1,15 +0,0 @@
### Changes
<!-- Fill this list with what your PR changes. Example: -->
- Fix bug
- Change UI for QOL
<!-- If your PR is ready to be merged, you can remove this section. -->
### Todo before merge
<!-- Example: -->
- [x] Finish UI changes
- [ ] Test
<!-- If your PR doesn't close an issue, you can remove the next line. -->
Closes #1234

View File

@@ -1,22 +0,0 @@
name: Add artifact links to pull request and related issues
on:
workflow_run:
workflows: [Pull Request SideStore build]
types: [completed]
jobs:
artifacts-url-comments:
name: add artifact links to pull request and related issues job
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: add artifact links to pull request and related issues step
uses: tonyhallett/artifacts-url-comments@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
prefix: Builds for this Pull Request are available at
suffix: Have a nice day.
format: name
addTo: pull
# addTo: pullandissues

View File

@@ -1,12 +1,16 @@
name: Beta SideStore build name: Beta SideStore build
on: on:
push: push:
tags: branches:
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1 - develop
jobs: jobs:
build: build:
name: Build and upload SideStore Beta name: Build and upload SideStore Beta
if: startsWith(github.event.head_commit.message, '[beta]')
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -21,18 +25,63 @@ jobs:
with: with:
submodules: recursive submodules: recursive
# - name: Cache rust cargo
# id: cache-rust-cargo
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-cargo
# with:
# path: ~/.cargo
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust minimuxer
# id: cache-rust-minimuxer
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-minimuxer
# with:
# path: ./Dependencies/minimuxer/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust em_proxy
# id: cache-rust-em_proxy
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-em_proxy
# with:
# path: ./Dependencies/em_proxy/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Change version to tag - name: Install rustup
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
target: aarch64-apple-ios
- name: Get version # - name: Create emotional damage
id: version # run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version # - name: Build minimuxer
run: echo "${{ steps.version.outputs.version }}" # run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
- name: Add beta suffix to version
run: sed -e '/MARKETING_VERSION = .*/s/$/-beta.${{ github.run_number }}/' -i '' Build.xcconfig
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
@@ -40,13 +89,39 @@ jobs:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: |
xcodebuild -project AltStore.xcodeproj \
-scheme AltStore \
-sdk iphoneos \
archive -archivePath ./archive \
CODE_SIGNING_REQUIRED=NO \
AD_HOC_CODE_SIGNING_ALLOWED=YES \
CODE_SIGNING_ALLOWED=NO \
DEVELOPMENT_TEAM=XYZ0123456 \
ORG_IDENTIFIER=com.SideStore \
| xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: Fakesign app
run: make fakesign run: |
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: |
mkdir Payload
mkdir Payload/SideStore.app
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
zip -r SideStore.ipa Payload
- name: Upload Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
@@ -56,22 +131,22 @@ jobs:
id: date_altstore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new beta release - name: Upload to beta release
uses: softprops/action-gh-release@v1 uses: IsaacShelton/update-existing-release@v1.3.1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }} release: "Beta"
tag_name: ${{ github.ref_name }} tag: "beta"
draft: true
prerelease: true prerelease: true
files: SideStore.ipa files: SideStore.ipa
body: | body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. --> This is an ⚠️ **EXPERIMENTAL** ⚠️ beta build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
## Changelog
- TODO Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal, but with a lower chance of bugs than if you used nightly builds. However, since these changes are newer and less tested, they still have a good chance of bugs, so **use at your own risk**.
If you want to be on the bleeding edge and use the latest development builds, you can look at [SideStore Nightly](https://github.com/${{ github.repository }}/releases/tag/nightly). **Please be aware that these builds have a much higher chance of bugs than beta or stable**.
If you use the `SideStore (Beta)` app, it will use the latest beta build (make sure to update it in "My Apps").
## Build Info ## Build Info
@@ -79,18 +154,3 @@ jobs:
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# Ensure we are in root directory
cd "$(dirname "$0")/../.."
DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1
write() {
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > .nightly-build-num
}
if [ ! -f ".nightly-build-num" ]; then
write
exit 0
fi
LAST_DATE=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
LAST_BUILD_NUM=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
if [[ "$DATE" != "$LAST_DATE" ]]; then
write
else
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
write
fi

View File

@@ -24,24 +24,63 @@ jobs:
with: with:
submodules: recursive submodules: recursive
# - name: Cache rust cargo
# id: cache-rust-cargo
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-cargo
# with:
# path: ~/.cargo
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust minimuxer
# id: cache-rust-minimuxer
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-minimuxer
# with:
# path: ./Dependencies/minimuxer/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust em_proxy
# id: cache-rust-em_proxy
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-em_proxy
# with:
# path: ./Dependencies/em_proxy/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Cache .nightly-build-num - name: Install rustup
uses: actions/cache@v3 uses: actions-rs/toolchain@v1
with: with:
path: .nightly-build-num toolchain: stable
key: nightly-build-num override: true
target: aarch64-apple-ios
- name: Increase nightly build number and set as version # - name: Create emotional damage
run: bash .github/workflows/increase-nightly-build-num.sh # run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
- name: Get version # - name: Build minimuxer
id: version # run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version - name: Add nightly suffix to version
run: echo "${{ steps.version.outputs.version }}" run: sed -e '/MARKETING_VERSION = .*/s/$/-nightly.${{ github.run_number }}/' -i '' Build.xcconfig
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
@@ -49,13 +88,39 @@ jobs:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: |
xcodebuild -project AltStore.xcodeproj \
-scheme AltStore \
-sdk iphoneos \
archive -archivePath ./archive \
CODE_SIGNING_REQUIRED=NO \
AD_HOC_CODE_SIGNING_ALLOWED=YES \
CODE_SIGNING_ALLOWED=NO \
DEVELOPMENT_TEAM=XYZ0123456 \
ORG_IDENTIFIER=com.SideStore \
| xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: Fakesign app
run: make fakesign run: |
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: |
mkdir Payload
mkdir Payload/SideStore.app
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
zip -r SideStore.ipa Payload
- name: Upload Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
@@ -76,9 +141,11 @@ jobs:
body: | body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}). This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha testers. They often contain bugs and experimental features. Use at your own risk!** Nightly builds are built from the most recent commit which means you'll be able to try out new features very early. However, since these changes are much newer and less tested, they have a much higher chance of bugs, so **use at your own risk**.
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases?q=beta). If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Beta](https://github.com/${{ github.repository }}/releases/tag/beta).
If you use the `SideStore (Nightly)` app, it will use the latest nightly build (make sure to update it in "My Apps").
## Build Info ## Build Info
@@ -86,21 +153,3 @@ jobs:
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/
- name: Reset cache for apps.sidestore.io/nightly
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}

View File

@@ -19,20 +19,60 @@ jobs:
with: with:
submodules: recursive submodules: recursive
# - name: Cache rust cargo
# id: cache-rust-cargo
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-cargo
# with:
# path: ~/.cargo
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust minimuxer
# id: cache-rust-minimuxer
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-minimuxer
# with:
# path: ./Dependencies/minimuxer/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust em_proxy
# id: cache-rust-em_proxy
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-em_proxy
# with:
# path: ./Dependencies/em_proxy/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Add PR suffix to version - name: Install rustup
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig uses: actions-rs/toolchain@v1
env: with:
COMMIT: ${{ github.event.pull_request.head.sha }} toolchain: stable
override: true
target: aarch64-apple-ios
- name: Get version # - name: Create emotional damage
id: version # run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version # - name: Build minimuxer
run: echo "${{ steps.version.outputs.version }}" # run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
@@ -40,25 +80,32 @@ jobs:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: |
xcodebuild -project AltStore.xcodeproj \
-scheme AltStore \
-sdk iphoneos \
archive -archivePath ./archive \
CODE_SIGNING_REQUIRED=NO \
AD_HOC_CODE_SIGNING_ALLOWED=YES \
CODE_SIGNING_ALLOWED=NO \
DEVELOPMENT_TEAM=XYZ0123456 \
ORG_IDENTIFIER=com.SideStore \
| xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: Fakesign app
run: make fakesign run: |
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: |
mkdir Payload
mkdir Payload/SideStore.app
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
zip -r SideStore.ipa Payload
- name: Add version to IPA file name - name: Upload Artifact
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-${{ steps.version.outputs.version }}.ipa name: SideStore.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

View File

@@ -2,7 +2,7 @@ name: Stable SideStore build
on: on:
push: push:
tags: tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0 - '[0-9]+.[0-9]+.[0-9]+*'
jobs: jobs:
build: build:
@@ -21,18 +21,60 @@ jobs:
with: with:
submodules: recursive submodules: recursive
# - name: Cache rust cargo
# id: cache-rust-cargo
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-cargo
# with:
# path: ~/.cargo
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust minimuxer
# id: cache-rust-minimuxer
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-minimuxer
# with:
# path: ./Dependencies/minimuxer/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - name: Cache rust em_proxy
# id: cache-rust-em_proxy
# uses: actions/cache@v3
# env:
# cache-name: cache-rust-em_proxy
# with:
# path: ./Dependencies/em_proxy/target
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Change version to tag - name: Install rustup
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
target: aarch64-apple-ios
- name: Get version # - name: Create emotional damage
id: version # run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version # - name: Build minimuxer
run: echo "${{ steps.version.outputs.version }}" # run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
@@ -40,13 +82,39 @@ jobs:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: |
xcodebuild -project AltStore.xcodeproj \
-scheme AltStore \
-sdk iphoneos \
archive -archivePath ./archive \
CODE_SIGNING_REQUIRED=NO \
AD_HOC_CODE_SIGNING_ALLOWED=YES \
CODE_SIGNING_ALLOWED=NO \
DEVELOPMENT_TEAM=XYZ0123456 \
ORG_IDENTIFIER=com.SideStore \
| xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: Fakesign app
run: make fakesign run: |
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: |
mkdir Payload
mkdir Payload/SideStore.app
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
zip -r SideStore.ipa Payload
- name: Upload Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
@@ -61,11 +129,10 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }} name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref }}
draft: true draft: true
files: SideStore.ipa files: SideStore.ipa
body: | body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
## Changelog ## Changelog
- TODO - TODO
@@ -76,18 +143,3 @@ jobs:
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

11
.gitignore vendored
View File

@@ -34,13 +34,4 @@ xcuserdata
## AppCode specific ## AppCode specific
.idea/ .idea/
/.build
Payload/
SideStore.ipa
*.dSYM
Dependencies/.*-prebuilt-fetch-*
Dependencies/minimuxer/*
Dependencies/em_proxy/*
!Dependencies/**/.gitkeep
.nightly-build-num

8
.gitmodules vendored
View File

@@ -9,13 +9,19 @@
url = https://github.com/libimobiledevice/libusbmuxd.git url = https://github.com/libimobiledevice/libusbmuxd.git
[submodule "Dependencies/libplist"] [submodule "Dependencies/libplist"]
path = Dependencies/libplist path = Dependencies/libplist
url = https://github.com/SideStore/libplist.git url = https://github.com/libimobiledevice/libplist.git
[submodule "Dependencies/MarkdownAttributedString"] [submodule "Dependencies/MarkdownAttributedString"]
path = Dependencies/MarkdownAttributedString path = Dependencies/MarkdownAttributedString
url = https://github.com/chockenberry/MarkdownAttributedString.git url = https://github.com/chockenberry/MarkdownAttributedString.git
[submodule "Dependencies/em_proxy"]
path = Dependencies/em_proxy
url = https://github.com/jkcoxson/em_proxy
[submodule "Dependencies/libimobiledevice-glue"] [submodule "Dependencies/libimobiledevice-glue"]
path = Dependencies/libimobiledevice-glue path = Dependencies/libimobiledevice-glue
url = https://github.com/libimobiledevice/libimobiledevice-glue url = https://github.com/libimobiledevice/libimobiledevice-glue
[submodule "Dependencies/minimuxer"]
path = Dependencies/minimuxer
url = https://github.com/jkcoxson/minimuxer
[submodule "Dependencies/libfragmentzip"] [submodule "Dependencies/libfragmentzip"]
path = Dependencies/libfragmentzip path = Dependencies/libfragmentzip
url = https://github.com/SideStore/libfragmentzip.git url = https://github.com/SideStore/libfragmentzip.git

View File

@@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "175", "blue" : "0.518",
"green" : "4", "green" : "0.502",
"red" : "115" "red" : "0.004"
} }
}, },
"idiom" : "universal" "idiom" : "universal"
@@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "150", "blue" : "0.404",
"green" : "3", "green" : "0.322",
"red" : "99" "red" : "0.008"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,35 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:sidestore.io</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array/>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>com.apple.developer.networking.multipath</key>
<true/>
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
</array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>com.apple.developer.shared-with-you</key>
<true/>
<key>com.apple.developer.siri</key> <key>com.apple.developer.siri</key>
<true/> <true/>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.$(APP_GROUP_IDENTIFIER)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
</array> </array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -71,7 +71,7 @@ extension AnalyticsManager
} }
} }
final class AnalyticsManager class AnalyticsManager
{ {
static let shared = AnalyticsManager() static let shared = AnalyticsManager()

View File

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

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class PermissionCollectionViewCell: UICollectionViewCell class PermissionCollectionViewCell: UICollectionViewCell
{ {
@IBOutlet var button: UIButton! @IBOutlet var button: UIButton!
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
@@ -29,7 +29,7 @@ final class PermissionCollectionViewCell: UICollectionViewCell
} }
} }
final class AppContentTableViewCell: UITableViewCell class AppContentTableViewCell: UITableViewCell
{ {
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{ {

View File

@@ -13,7 +13,7 @@ import Roxas
import Nuke import Nuke
final class AppViewController: UIViewController class AppViewController: UIViewController
{ {
var app: StoreApp! var app: StoreApp!
@@ -217,8 +217,8 @@ final class AppViewController: UIViewController
self._shouldResetLayout = false self._shouldResetLayout = false
} }
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 let statusBarHeight = UIApplication.shared.statusBarFrame.height
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
let inset = 12 as CGFloat let inset = 12 as CGFloat
@@ -323,7 +323,7 @@ final class AppViewController: UIViewController
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight self.scrollView.scrollIndicatorInsets.top = statusBarHeight
// Adjust content offset + size. // Adjust content offset + size.
let contentOffset = self.scrollView.contentOffset let contentOffset = self.scrollView.contentOffset
@@ -352,7 +352,7 @@ final class AppViewController: UIViewController
extension AppViewController extension AppViewController
{ {
final class func makeAppViewController(app: StoreApp) -> AppViewController class func makeAppViewController(app: StoreApp) -> AppViewController
{ {
let storyboard = UIStoryboard(name: "Main", bundle: nil) let storyboard = UIStoryboard(name: "Main", bundle: nil)
@@ -384,7 +384,7 @@ private extension AppViewController
button.progress = progress button.progress = progress
} }
if let versionDate = self.app.latestAvailableVersion?.date, versionDate > Date() if let versionDate = self.app.latestVersion?.date, versionDate > Date()
{ {
self.bannerView.button.countdownDate = versionDate self.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = versionDate self.navigationBarDownloadButton.countdownDate = versionDate
@@ -510,7 +510,7 @@ extension AppViewController
catch catch
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error, opensLog: true) let toastView = ToastView(error: error)
toastView.show(in: self) toastView.show(in: self)
} }
} }

View File

@@ -10,7 +10,7 @@ import UIKit
import AltStoreCore import AltStoreCore
final class PermissionPopoverViewController: UIViewController class PermissionPopoverViewController: UIViewController
{ {
var permission: AppPermission! var permission: AppPermission!

View File

@@ -11,7 +11,7 @@ import UIKit
import AltStoreCore import AltStoreCore
import Roxas import Roxas
final class AppIDsViewController: UICollectionViewController class AppIDsViewController: UICollectionViewController
{ {
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
@@ -90,21 +90,14 @@ private extension AppIDsViewController
cell.bannerView.button.isUserInteractionEnabled = false cell.bannerView.button.isUserInteractionEnabled = false
cell.bannerView.buttonLabel.isHidden = false cell.bannerView.buttonLabel.isHidden = false
let currentDate = Date() let currentDate = Date()
let formatter = DateComponentsFormatter() let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
formatter.unitsStyle = .full let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
formatter.includesApproximationPhrase = false cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
formatter.includesTimeRemainingPhrase = false
formatter.allowedUnits = [.minute, .hour, .day]
formatter.maximumUnitCount = 1
cell.bannerView.button.setTitle((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")).uppercased(), for: .normal) attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ")
// formatter.includesTimeRemainingPhrase = true
// attributedAccessibilityLabel.mutableString.append((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " ")
} }
else else
{ {

View File

@@ -30,7 +30,7 @@ extension AppDelegate
} }
@UIApplicationMain @UIApplicationMain
final class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
@@ -61,8 +61,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// Register default settings before doing anything else. // Register default settings before doing anything else.
UserDefaults.registerDefaults() UserDefaults.registerDefaults()
DatabaseManager.shared.start { (error) in DatabaseManager.shared.start { (error) in
if let error = error if let error = error
{ {
@@ -382,7 +380,7 @@ private extension AppDelegate
for update in updates for update in updates
{ {
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue } guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue } guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "") content.title = NSLocalizedString("New Update Available", comment: "")

View File

@@ -10,7 +10,7 @@ import UIKit
import AltSign import AltSign
final class AuthenticationViewController: UIViewController class AuthenticationViewController: UIViewController
{ {
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)? var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)? var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
@@ -108,9 +108,11 @@ private extension AuthenticationViewController
case .failure(let error as NSError): case .failure(let error as NSError):
DispatchQueue.main.async { DispatchQueue.main.async {
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: "")) let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error) let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self) toastView.show(in: self)
self.toastView = toastView self.toastView = toastView

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class InstructionsViewController: UIViewController class InstructionsViewController: UIViewController
{ {
var completionHandler: (() -> Void)? var completionHandler: (() -> Void)?

View File

@@ -12,7 +12,7 @@ import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
final class RefreshAltStoreViewController: UIViewController class RefreshAltStoreViewController: UIViewController
{ {
var context: AuthenticatedOperationContext! var context: AuthenticatedOperationContext!

View File

@@ -14,7 +14,7 @@ import IntentsUI
import AltSign import AltSign
final class SelectTeamViewController: UITableViewController class SelectTeamViewController: UITableViewController
{ {
public var teams: [ALTTeam]? public var teams: [ALTTeam]?
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)? public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21223" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -356,8 +356,8 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW"> <stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW">
<rect key="frame" x="0.0" y="30.5" width="335" height="17"/> <rect key="frame" x="0.0" y="30.5" width="335" height="17"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 0.5.6" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 4.4.2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
<rect key="frame" x="0.0" y="0.0" width="84" height="17"/> <rect key="frame" x="0.0" y="0.0" width="84.5" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -596,7 +596,7 @@ World</string>
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/> <tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
</navigationBar> </navigationBar>
@@ -626,7 +626,7 @@ World</string>
</tabBarItem> </tabBarItem>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -883,7 +883,7 @@ World</string>
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb"> <navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups"> <barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba"> <view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
<rect key="frame" x="16" y="7" width="83" height="42"/> <rect key="frame" x="16" y="1" width="83" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view> </view>
</barButtonItem> </barButtonItem>
@@ -909,7 +909,7 @@ World</string>
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/> <tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/> <edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar> </navigationBar>
@@ -928,7 +928,7 @@ World</string>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -1070,7 +1070,7 @@ World</string>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -1095,13 +1095,13 @@ World</string>
<image name="News" width="19" height="20"/> <image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/> <image name="Settings" width="20" height="20"/>
<namedColor name="Background"> <namedColor name="Background">
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="BlurTint"> <namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/> <color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@@ -12,7 +12,7 @@ import Roxas
import Nuke import Nuke
@objc final class BrowseCollectionViewCell: UICollectionViewCell @objc class BrowseCollectionViewCell: UICollectionViewCell
{ {
var imageURLs: [URL] = [] { var imageURLs: [URL] = [] {
didSet { didSet {

View File

@@ -8,7 +8,6 @@
import UIKit import UIKit
import minimuxer
import AltStoreCore import AltStoreCore
import Roxas import Roxas
@@ -114,9 +113,9 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app) let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress cell.bannerView.button.progress = progress
if let versionDate = app.latestSupportedVersion?.date, versionDate > Date() if let versionDate = app.latestVersion?.date, versionDate > Date()
{ {
cell.bannerView.button.countdownDate = versionDate cell.bannerView.button.countdownDate = app.versionDate
} }
else else
{ {
@@ -265,20 +264,14 @@ private extension BrowseViewController
previousProgress?.cancel() previousProgress?.cancel()
return return
} }
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in _ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result switch result
{ {
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error, opensLog: true) let toastView = ToastView(error: error)
toastView.show(in: self) toastView.show(in: self)
case .success: print("Installed app:", app.bundleIdentifier) case .success: print("Installed app:", app.bundleIdentifier)

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class AppIconImageView: UIImageView class AppIconImageView: UIImageView
{ {
override func awakeFromNib() override func awakeFromNib()
{ {

View File

@@ -8,7 +8,7 @@
import AVFoundation import AVFoundation
final class BackgroundTaskManager class BackgroundTaskManager
{ {
static let shared = BackgroundTaskManager() static let shared = BackgroundTaskManager()

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class BannerCollectionViewCell: UICollectionViewCell class BannerCollectionViewCell: UICollectionViewCell
{ {
private(set) var errorBadge: UIView? private(set) var errorBadge: UIView?
@IBOutlet private(set) var bannerView: AppBannerView! @IBOutlet private(set) var bannerView: AppBannerView!

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class Button: UIButton class Button: UIButton
{ {
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize var size = super.intrinsicContentSize

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class CollapsingTextView: UITextView class CollapsingTextView: UITextView
{ {
var isCollapsed = true { var isCollapsed = true {
didSet { didSet {
@@ -22,7 +22,7 @@ final class CollapsingTextView: UITextView
} }
} }
var lineSpacing: Double = 2 { var lineSpacing: CGFloat = 2 {
didSet { didSet {
self.setNeedsLayout() self.setNeedsLayout()
} }
@@ -34,19 +34,7 @@ final class CollapsingTextView: UITextView
{ {
super.awakeFromNib() super.awakeFromNib()
self.initialize() self.layoutManager.delegate = self
}
private func initialize()
{
if #available(iOS 16, *)
{
self.updateText()
}
else
{
self.layoutManager.delegate = self
}
self.textContainerInset = .zero self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0 self.textContainer.lineFragmentPadding = 0
@@ -120,25 +108,6 @@ private extension CollapsingTextView
{ {
self.isCollapsed.toggle() self.isCollapsed.toggle()
} }
@available(iOS 16, *)
func updateText()
{
do
{
let style = NSMutableParagraphStyle()
style.lineSpacing = self.lineSpacing
var attributedText = try AttributedString(self.attributedText, including: \.uiKit)
attributedText[AttributeScopes.UIKitAttributes.ParagraphStyleAttribute.self] = style
self.attributedText = NSAttributedString(attributedText)
}
catch
{
print("[ALTLog] Failed to update CollapsingTextView line spacing:", error)
}
}
} }
extension CollapsingTextView: NSLayoutManagerDelegate extension CollapsingTextView: NSLayoutManagerDelegate

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class ForwardingNavigationController: UINavigationController class ForwardingNavigationController: UINavigationController
{ {
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
return self.topViewController return self.topViewController

View File

@@ -10,7 +10,7 @@ import UIKit
import Roxas import Roxas
final class NavigationBar: UINavigationBar class NavigationBar: UINavigationBar
{ {
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true @IBInspectable var automaticallyAdjustsItemPositions: Bool = true

View File

@@ -8,13 +8,7 @@
import UIKit import UIKit
extension PillButton class PillButton: UIButton
{
static let minimumSize = CGSize(width: 77, height: 31)
static let contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 13, bottom: 7, trailing: 13)
}
final class PillButton: UIButton
{ {
override var accessibilityValue: String? { override var accessibilityValue: String? {
get { get {
@@ -76,7 +70,9 @@ final class PillButton: UIButton
}() }()
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
let size = self.sizeThatFits(CGSize(width: Double.infinity, height: .infinity)) var size = super.intrinsicContentSize
size.width += 26
size.height += 3
return size return size
} }
@@ -92,8 +88,6 @@ final class PillButton: UIButton
self.layer.masksToBounds = true self.layer.masksToBounds = true
self.accessibilityTraits.formUnion([.updatesFrequently, .button]) self.accessibilityTraits.formUnion([.updatesFrequently, .button])
self.contentEdgeInsets = UIEdgeInsets(top: Self.contentInsets.top, left: Self.contentInsets.leading, bottom: Self.contentInsets.bottom, right: Self.contentInsets.trailing)
self.activityIndicatorView.style = .medium self.activityIndicatorView.style = .medium
self.activityIndicatorView.isUserInteractionEnabled = false self.activityIndicatorView.isUserInteractionEnabled = false
@@ -125,15 +119,6 @@ final class PillButton: UIButton
self.update() self.update()
} }
override func sizeThatFits(_ size: CGSize) -> CGSize
{
var size = super.sizeThatFits(size)
size.width = max(size.width, PillButton.minimumSize.width)
size.height = max(size.height, PillButton.minimumSize.height)
return size
}
} }
private extension PillButton private extension PillButton

View File

@@ -16,19 +16,10 @@ extension TimeInterval
static let longToastViewDuration = 8.0 static let longToastViewDuration = 8.0
} }
final class ToastView: RSTToastView class ToastView: RSTToastView
{ {
static let openErrorLogNotification = Notification.Name("ALTOpenErrorLogNotification")
var preferredDuration: TimeInterval var preferredDuration: TimeInterval
var opensErrorLog: Bool = false
convenience init(text: String, detailText: String?, opensLog: Bool = false) {
self.init(text: text, detailText: detailText)
self.opensErrorLog = opensLog
}
override init(text: String, detailText detailedText: String?) override init(text: String, detailText detailedText: String?)
{ {
if detailedText == nil if detailedText == nil
@@ -52,43 +43,53 @@ final class ToastView: RSTToastView
// RSTToastView does not expose stack view containing labels, // RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview. // so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0 stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
stackView.alignment = .leading
} }
self.addTarget(self, action: #selector(ToastView.showErrorLog), for: .touchUpInside)
} }
convenience init(error: Error, opensLog: Bool = false) {
self.init(error: error)
self.opensErrorLog = opensLog
}
convenience init(error: Error) convenience init(error: Error)
{ {
var error = error as NSError var error = error as NSError
var underlyingError = error.underlyingError var underlyingError = error.underlyingError
var preferredDuration: TimeInterval?
if if
let unwrappedUnderlyingError = underlyingError, let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
{ {
// Treat underlyingError as the primary error, but keep localized title + failure. // Treat underlyingError as the primary error.
let nsError = error as NSError
error = unwrappedUnderlyingError as NSError error = unwrappedUnderlyingError as NSError
if let localizedTitle = nsError.localizedTitle {
error = error.withLocalizedTitle(localizedTitle)
}
if let localizedFailure = nsError.localizedFailure {
error = error.withLocalizedFailure(localizedFailure)
}
underlyingError = nil underlyingError = nil
preferredDuration = .longToastViewDuration
} }
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let detailText = error.localizedDescription let text: String
let detailText: String?
if let failure = error.localizedFailure
{
text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
}
else if let reason = error.localizedFailureReason
{
text = reason
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
}
else
{
text = error.localizedDescription
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
}
self.init(text: text, detailText: detailText) self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration
}
} }
required init(coder aDecoder: NSCoder) { required init(coder aDecoder: NSCoder) {
@@ -111,18 +112,6 @@ final class ToastView: RSTToastView
override func show(in view: UIView, duration: TimeInterval) override func show(in view: UIView, duration: TimeInterval)
{ {
if opensErrorLog, #available(iOS 13.0, *), case let configuration = UIImage.SymbolConfiguration(font: self.textLabel.font),
let icon = UIImage(systemName: "chevron.right.circle", withConfiguration: configuration) {
let tintedIcon = icon.withTintColor(.white, renderingMode: .alwaysOriginal)
let moreIconImageView = UIImageView(image: tintedIcon)
moreIconImageView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(moreIconImageView)
NSLayoutConstraint.activate([
moreIconImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -self.layoutMargins.right),
moreIconImageView.centerYAnchor.constraint(equalTo: self.textLabel.centerYAnchor),
moreIconImageView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.textLabel.trailingAnchor, multiplier: 1.0)
])
}
super.show(in: view, duration: duration) super.show(in: view, duration: duration)
let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "") let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
@@ -138,10 +127,4 @@ final class ToastView: RSTToastView
{ {
self.show(in: view, duration: self.preferredDuration) self.show(in: view, duration: self.preferredDuration)
} }
@objc
func showErrorLog() {
guard self.opensErrorLog else { return }
NotificationCenter.default.post(name: ToastView.openErrorLogNotification, object: self)
}
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
import OSLog import OSLog
public let customLog = OSLog(subsystem: "org.sidestore.sidestore", let customLog = OSLog(subsystem: "org.sidestore.sidestore",
category: "ios") category: "ios")
@@ -18,7 +18,6 @@ public extension OSLog {
/// - Parameters: /// - Parameters:
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable
static func error(_ message: StaticString, _ args: CVarArg...) { static func error(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .error, args) os_log(message, log: customLog, type: .error, args)
} }
@@ -27,7 +26,6 @@ public extension OSLog {
/// - Parameters: /// - Parameters:
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable
static func info(_ message: StaticString, _ args: CVarArg...) { static func info(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .info, args) os_log(message, log: customLog, type: .info, args)
} }
@@ -36,7 +34,6 @@ public extension OSLog {
/// - Parameters: /// - Parameters:
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable
static func debug(_ message: StaticString, _ args: CVarArg...) { static func debug(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .debug, args) os_log(message, log: customLog, type: .debug, args)
} }
@@ -48,7 +45,6 @@ public extension OSLog {
/// - Parameters: /// - Parameters:
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable
public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) { public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.error(message, args) OSLog.error(message, args)
} }
@@ -57,7 +53,6 @@ public func ELOG(_ message: StaticString, file: StaticString = #file, function:
/// - Parameters: /// - Parameters:
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable
public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) { public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.info(message, args) OSLog.info(message, args)
} }

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>ALTAppGroups</key> <key>ALTAppGroups</key>
<array> <array>
<string>group.$(APP_GROUP_IDENTIFIER)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
@@ -11,10 +9,12 @@
</array> </array>
<key>ALTDeviceID</key> <key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string> <string>00008101-000129D63698001E</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTServerID</key> <key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string> <string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key>
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
@@ -44,6 +44,8 @@
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -56,7 +58,7 @@
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>altstore</string> <string>altstore</string>
<string>sidestore</string> <string>sidestore</string>
</array> </array>
</dict> </dict>
<dict> <dict>
@@ -67,7 +69,7 @@
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>altstore-com.rileytestut.AltStore</string> <string>altstore-com.rileytestut.AltStore</string>
<string>sidestore-com.SideStore.SideStore</string> <string>sidestore-com.SideStore.SideStore</string>
</array> </array>
</dict> </dict>
</array> </array>
@@ -91,12 +93,39 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSExceptionDomains</key>
<true/> <dict>
<key>127.0.0.1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>New Exception Domain 1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>apps.sidestore.io</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>sidestore.io</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict> </dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
@@ -134,12 +163,16 @@
<array> <array>
<string>audio</string> <string>audio</string>
<string>fetch</string> <string>fetch</string>
<string>processing</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>

View File

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

View File

@@ -14,9 +14,7 @@ import minimuxer
import AltStoreCore import AltStoreCore
import UniformTypeIdentifiers import UniformTypeIdentifiers
let pairingFileName = "ALTPairingFile.mobiledevicepairing" class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{ {
private var didFinishLaunching = false private var didFinishLaunching = false
@@ -49,44 +47,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable {
DispatchQueue.global().async {
self.isSideJITServerDetected() { result in
DispatchQueue.main.async {
switch result {
case .success():
let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
UserDefaults.standard.sidejitenable = true
})
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
//Add OK button to a dialog message
dialogMessage.addAction(ok)
dialogMessage.addAction(cancel)
// Present Alert to
self.present(dialogMessage, animated: true, completion: nil)
case .failure(_):
print("Cannot find sideJITServer")
}
}
}
}
}
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
DispatchQueue.global().async {
self.askfornetwork()
}
print("SideJITServer Enabled")
}
#if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else { guard let pf = fetchPairingFile() else {
@@ -94,47 +54,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
return return
} }
start_minimuxer_threads(pf) start_minimuxer_threads(pf)
#endif
}
func askfornetwork() {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
var SJSURL = address
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
// Create a network operation at launch to Refresh SideJITServer
let url = URL(string: "\(SJSURL)/re/")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
print(data)
}
task.resume()
}
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
var SJSURL = address
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
// Create a network operation at launch to Refresh SideJITServer
let url = URL(string: SJSURL)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("No SideJITServer on Network")
completion(.failure(error))
return
}
completion(.success(()))
}
task.resume()
return
} }
func fetchPairingFile() -> String? { func fetchPairingFile() -> String? {
@@ -149,17 +68,16 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
fm.fileExists(atPath: appResourcePath.path), fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path), let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8), let contents = String(data: data, encoding: .utf8),
!contents.isEmpty, !contents.isEmpty {
!UserDefaults.standard.isPairingReset {
print("Loaded ALTPairingFile from \(appResourcePath.path)") print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents return contents
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{ } else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
print("Loaded ALTPairingFile from Info.plist") print("Loaded ALTPairingFile from Info.plist")
return plistString return plistString
} else { } else {
// Show an alert explaining the pairing file // Show an alert explaining the pairing file
// Create new Alert // Create new Alert
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/getting-started/#pairing-file", preferredStyle: .alert) let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://youtu.be/dQw4w9WgXcQ", preferredStyle: .alert)
// Create OK button with action handler // Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
@@ -171,7 +89,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
documentPickerController.shouldShowFileExtensions = true documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil) self.present(documentPickerController, animated: true, completion: nil)
UserDefaults.standard.isPairingReset = false
}) })
//Add OK button to a dialog message //Add OK button to a dialog message
@@ -180,13 +97,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
// Present Alert to // Present Alert to
self.present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert)
let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in})
dialogMessage2.addAction(ok2)
self.present(dialogMessage2, animated: true, completion: nil)
return nil return nil
} }
} }
@@ -213,11 +123,14 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
} }
// Save to a file for next launch // Save to a file for next launch
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)") let filename = "ALTPairingFile.mobiledevicepairing"
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8) let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file // Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!) start_minimuxer_threads(pairing_string!)
} catch { } catch {
displayError("Unable to read pairing file") displayError("Unable to read pairing file")
} }
@@ -233,20 +146,12 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
} }
func start_minimuxer_threads(_ pairing_file: String) { func start_minimuxer_threads(_ pairing_file: String) {
target_minimuxer_address() set_usbmuxd_socket()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString let res = start_minimuxer(pairing_file: pairing_file)
do { if res != 0 {
try start(pairing_file, documentsDirectory) displayError("minimuxer failed to start. Incorrect arguments were passed.")
} 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!")")
}
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
}
else {
start_auto_mounter(documentsDirectory)
} }
auto_mount_dev_image()
} }
} }

View File

@@ -14,7 +14,6 @@ import Intents
import Combine import Combine
import WidgetKit import WidgetKit
import minimuxer
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
@@ -29,7 +28,7 @@ extension AppManager
} }
@available(iOS 13, *) @available(iOS 13, *)
final class AppManagerPublisher: ObservableObject class AppManagerPublisher: ObservableObject
{ {
@Published @Published
fileprivate(set) var installationProgress = [String: Progress]() fileprivate(set) var installationProgress = [String: Progress]()
@@ -38,7 +37,12 @@ final class AppManagerPublisher: ObservableObject
fileprivate(set) var refreshProgress = [String: Progress]() fileprivate(set) var refreshProgress = [String: Progress]()
} }
final class AppManager private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
{
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
}
class AppManager
{ {
static let shared = AppManager() static let shared = AppManager()
@@ -303,45 +307,6 @@ extension AppManager
presentingViewController.present(alertController, animated: true, completion: nil) presentingViewController.present(alertController, animated: true, completion: nil)
} }
} }
func clearAppCache(completion: @escaping (Result<Void, Error>) -> Void)
{
let clearAppCacheOperation = ClearAppCacheOperation()
clearAppCacheOperation.resultHandler = { result in
completion(result)
}
self.run([clearAppCacheOperation], context: nil)
}
func log(_ error: Error, operation: LoggedError.Operation, app: AppProtocol)
{
switch error {
case ~OperationError.Code.cancelled: return // Don't log cancelled events
default: break
}
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForSerialization()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: operation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
} }
extension AppManager extension AppManager
@@ -394,7 +359,7 @@ extension AppManager
case .success(let source): fetchedSources.insert(source) case .success(let source): fetchedSources.insert(source)
case .failure(let error): case .failure(let error):
let source = managedObjectContext.object(with: source.objectID) as! Source let source = managedObjectContext.object(with: source.objectID) as! Source
source.error = (error as NSError).sanitizedForSerialization() source.error = (error as NSError).sanitizedForCoreData()
errors[source] = error errors[source] = error
} }
@@ -427,8 +392,7 @@ extension AppManager
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void) func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
{ {
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
// result contains name, email, auth token, OTP and other possibly personal/account specific info. we don't want this logged print("Authenticated for fetching App IDs with result:", result)
//print("Authenticated for fetching App IDs with result:", result)
} }
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context) let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
@@ -482,7 +446,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw context.error ?? OperationError.unknown() } guard let result = results.values.first else { throw context.error ?? OperationError.unknown }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -501,7 +465,7 @@ extension AppManager
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
guard let storeApp = app.storeApp else { guard let storeApp = app.storeApp else {
completionHandler(.failure(OperationError.appNotFound(name: app.name))) completionHandler(.failure(OperationError.appNotFound))
return Progress.discreteProgress(totalUnitCount: 1) return Progress.discreteProgress(totalUnitCount: 1)
} }
@@ -509,7 +473,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown() } guard let result = results.values.first else { throw OperationError.unknown }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -545,8 +509,8 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown() } guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -584,7 +548,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown() } guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -610,8 +574,8 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown() } guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -635,7 +599,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown() } guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -700,25 +664,18 @@ extension AppManager
@available(iOS 14, *) @available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void) func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
{ {
final class Context: OperationContext, EnableJITContext class Context: OperationContext, EnableJITContext
{ {
var installedApp: InstalledApp? var installedApp: InstalledApp?
} }
let appName = installedApp.name
let context = Context() let context = Context()
context.installedApp = installedApp context.installedApp = installedApp
let enableJITOperation = EnableJITOperation(context: context) let enableJITOperation = EnableJITOperation(context: context)
enableJITOperation.resultHandler = { (result) in enableJITOperation.resultHandler = { (result) in
switch result { completionHandler(result)
case .success: completionHandler(.success(()))
case .failure(let nsError as NSError):
let localizedTitle = String(format: NSLocalizedString("Failed to enable JIT for %@", comment: ""), appName)
let error = nsError.withLocalizedTitle(localizedTitle)
self.log(error, operation: .enableJIT, app: installedApp)
}
} }
self.run([enableJITOperation], context: context, requiresSerialQueue: true) self.run([enableJITOperation], context: context, requiresSerialQueue: true)
@@ -727,7 +684,7 @@ extension AppManager
@available(iOS 14.0, *) @available(iOS 14.0, *)
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
{ {
final class Context: InstallAppOperationContext, PatchAppContext class Context: InstallAppOperationContext, PatchAppContext
{ {
} }
@@ -796,12 +753,6 @@ extension AppManager
let progress = self.refreshProgress[app.bundleIdentifier] let progress = self.refreshProgress[app.bundleIdentifier]
return progress return progress
} }
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
{
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
return isActivelyManaging
}
} }
extension AppManager extension AppManager
@@ -854,18 +805,12 @@ private extension AppManager
return bundleIdentifier return bundleIdentifier
} }
}
var loggedErrorOperation: LoggedError.Operation {
switch self { func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
case .install: return .install {
case .update: return .update let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
case .refresh: return .refresh return isActivelyManaging
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}
} }
@discardableResult @discardableResult
@@ -930,9 +875,7 @@ private extension AppManager
if app.certificateSerialNumber != group.context.certificate?.serialNumber || if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
uti != nil || uti != nil ||
app.needsResign || app.needsResign
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
app.bundleIdentifier == StoreApp.altstoreAppID
{ {
// Resign app instead of just refreshing profiles because either: // Resign app instead of just refreshing profiles because either:
// * Refreshing using different certificate // * Refreshing using different certificate
@@ -1002,13 +945,7 @@ private extension AppManager
} }
else else
{ {
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
}
performAppOperations() performAppOperations()
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = false
}
} }
return group return group
@@ -1087,34 +1024,6 @@ private extension AppManager
verifyOperation.addDependency(downloadOperation) verifyOperation.addDependency(downloadOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(verifyOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles):
context.provisioningProfiles = provisioningProfiles
print("PROVISIONING PROFILES \(context.provisioningProfiles)")
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Deactivate Apps (if necessary) */ /* Deactivate Apps (if necessary) */
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do do
@@ -1130,12 +1039,6 @@ private extension AppManager
{ {
throw error throw error
} }
guard let profiles = context.provisioningProfiles else { throw OperationError.invalidParameters }
if !profiles.contains(where: { $1.isFreeProvisioningProfile == true }) {
operation.finish()
return
}
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters } guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
@@ -1155,7 +1058,7 @@ private extension AppManager
operation.finish() operation.finish()
} }
} }
deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation) deactivateAppsOperation.addDependency(verifyOperation)
/* Patch App */ /* Patch App */
@@ -1230,6 +1133,32 @@ private extension AppManager
patchAppOperation.addDependency(deactivateAppsOperation) patchAppOperation.addDependency(deactivateAppsOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(patchAppOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Resign */ /* Resign */
let resignAppOperation = ResignAppOperation(context: context) let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in resignAppOperation.resultHandler = { (result) in
@@ -1239,7 +1168,7 @@ private extension AppManager
case .success(let resignedApp): context.resignedApp = resignedApp case .success(let resignedApp): context.resignedApp = resignedApp
} }
} }
resignAppOperation.addDependency(patchAppOperation) resignAppOperation.addDependency(fetchProvisioningProfilesOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
@@ -1282,7 +1211,7 @@ private extension AppManager
progress.addChild(installOperation.progress, withPendingUnitCount: 30) progress.addChild(installOperation.progress, withPendingUnitCount: 30)
installOperation.addDependency(sendAppOperation) installOperation.addDependency(sendAppOperation)
let operations = [downloadOperation, verifyOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, deactivateAppsOperation, patchAppOperation, resignAppOperation, sendAppOperation, installOperation] let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
group.add(operations) group.add(operations)
self.run(operations, context: group.context) self.run(operations, context: group.context)
@@ -1315,21 +1244,14 @@ private extension AppManager
case .success(let installedApp): case .success(let installedApp):
completionHandler(.success(installedApp)) completionHandler(.success(installedApp))
case .failure(MinimuxerError.ProfileInstall): case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound):
completionHandler(.failure(OperationError.noWiFi))
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound(name: app.name)):
// Fall back to installation if AltServer doesn't support newer provisioning profile requests, // Fall back to installation if AltServer doesn't support newer provisioning profile requests,
// OR if the cached app could not be found and we may need to redownload it. // OR if the cached app could not be found and we may need to redownload it.
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return. app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
if minimuxer.ready() { let installProgress = self._install(app, operation: operation, group: group) { (result) in
let installProgress = self._install(app, operation: operation, group: group) { (result) in completionHandler(result)
completionHandler(result)
}
progress.addChild(installProgress, withPendingUnitCount: 40)
} else {
completionHandler(.failure(OperationError.noWiFi))
} }
progress.addChild(installProgress, withPendingUnitCount: 40)
} }
case .failure(let error): case .failure(let error):
@@ -1596,7 +1518,7 @@ private extension AppManager
} }
guard let application = ALTApplication(fileURL: app.fileURL) else { guard let application = ALTApplication(fileURL: app.fileURL) else {
completionHandler(.failure(OperationError.appNotFound(name: app.name))) completionHandler(.failure(OperationError.appNotFound))
return progress return progress
} }
@@ -1608,8 +1530,8 @@ private extension AppManager
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString) let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: app.name) } guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL) let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp } guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1747,35 +1669,11 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() } do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) } catch { print("Error saving installed app.", error) }
} }
catch let nsError as NSError catch
{ {
var appName: String!
if let app = operation.app as? (NSManagedObject & AppProtocol) {
if let context = app.managedObjectContext {
context.performAndWait {
appName = app.name
}
} else {
appName = NSLocalizedString("Unknown App", comment: "")
}
} else {
appName = operation.app.name
}
let localizedTitle: String
switch operation {
case .install: localizedTitle = String(format: NSLocalizedString("Failed to Install %@", comment: ""), appName)
case .refresh: localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@", comment: ""), appName)
case .update: localizedTitle = String(format: NSLocalizedString("Failed to Update %@", comment: ""), appName)
case .activate: localizedTitle = String(format: NSLocalizedString("Failed to Activate %@", comment: ""), appName)
case .deactivate: localizedTitle = String(format: NSLocalizedString("Failed to Deactivate %@", comment: ""), appName)
case .backup: localizedTitle = String(format: NSLocalizedString("Failed to Backup %@", comment: ""), appName)
case .restore: localizedTitle = String(format: NSLocalizedString("Failed to Restore %@ Backup", comment: ""), appName)
}
let error = nsError.withLocalizedTitle(localizedTitle)
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier) group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, operation: operation.loggedErrorOperation, app: operation.app) self.log(error, for: operation)
} }
} }
@@ -1792,15 +1690,51 @@ private extension AppManager
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("SideStore Expiring Soon", comment: "") content.title = NSLocalizedString("AltStore Expiring Soon", comment: "")
content.body = NSLocalizedString("SideStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "") content.body = NSLocalizedString("AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
content.sound = .default content.sound = .default
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger) let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
func log(_ error: Error, for operation: AppOperation)
{
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForCoreData()
let loggedErrorOperation: LoggedError.Operation = {
switch operation
{
case .install: return .install
case .update: return .update
case .refresh: return .refresh
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = operation.app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false) func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
{ {
// Find "Install AltStore" operation if it already exists in `context` // Find "Install AltStore" operation if it already exists in `context`

View File

@@ -22,27 +22,13 @@ extension AppManager
var managedObjectContext: NSManagedObjectContext? var managedObjectContext: NSManagedObjectContext?
var localizedTitle: String? {
var localizedTitle: String?
self.managedObjectContext?.performAndWait {
if self.sources?.count == 1 {
localizedTitle = NSLocalizedString("Failed to refresh Store", comment: "")
} else if self.errors.count == 1 {
guard let source = self.errors.keys.first else { return }
localizedTitle = String(format: NSLocalizedString("Failed to refresh Source '%@'", comment: ""), source.name)
} else {
localizedTitle = String(format: NSLocalizedString("Failed to refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count))
}
}
return localizedTitle
}
var errorDescription: String? { var errorDescription: String? {
if let error = self.primaryError { if let error = self.primaryError
{
return error.localizedDescription return error.localizedDescription
} else if let error = self.errors.values.first, self.errors.count == 1 { }
return error.localizedDescription else
} else { {
var localizedDescription: String? var localizedDescription: String?
self.managedObjectContext?.performAndWait { self.managedObjectContext?.performAndWait {
@@ -81,14 +67,8 @@ extension AppManager
} }
var errorUserInfo: [String : Any] { var errorUserInfo: [String : Any] {
let errors = Array(self.errors.values) guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
var userInfo = [String: Any]() return [NSUnderlyingErrorKey: error]
userInfo[ALTLocalizedTitleErrorKey] = self.localizedTitle
userInfo[NSUnderlyingErrorKey] = self.primaryError
if #available(iOS 14.5, *), !errors.isEmpty {
userInfo[NSMultipleUnderlyingErrorsKey] = errors
}
return userInfo
} }
init(_ error: Error) init(_ error: Error)

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class InstalledAppsCollectionHeaderView: UICollectionReusableView class InstalledAppsCollectionHeaderView: UICollectionReusableView
{ {
let textLabel: UILabel let textLabel: UILabel
let button: UIButton let button: UIButton

View File

@@ -9,7 +9,7 @@
import UIKit import UIKit
import Roxas import Roxas
final class InstalledAppCollectionViewCell: UICollectionViewCell class InstalledAppCollectionViewCell: UICollectionViewCell
{ {
private(set) var deactivateBadge: UIView? private(set) var deactivateBadge: UIView?
@@ -55,13 +55,13 @@ final class InstalledAppCollectionViewCell: UICollectionViewCell
} }
} }
final class InstalledAppsCollectionFooterView: UICollectionReusableView class InstalledAppsCollectionFooterView: UICollectionReusableView
{ {
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
@IBOutlet var button: UIButton! @IBOutlet var button: UIButton!
} }
final class NoUpdatesCollectionViewCell: UICollectionViewCell class NoUpdatesCollectionViewCell: UICollectionViewCell
{ {
@IBOutlet var blurView: UIVisualEffectView! @IBOutlet var blurView: UIVisualEffectView!
@@ -73,7 +73,7 @@ final class NoUpdatesCollectionViewCell: UICollectionViewCell
} }
} }
final class UpdatesCollectionHeaderView: UICollectionReusableView class UpdatesCollectionHeaderView: UICollectionReusableView
{ {
let button = PillButton(type: .system) let button = PillButton(type: .system)

View File

@@ -10,12 +10,10 @@ import UIKit
import MobileCoreServices import MobileCoreServices
import Intents import Intents
import Combine import Combine
import UniformTypeIdentifiers
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
import minimuxer
import Nuke import Nuke
@@ -32,7 +30,7 @@ extension MyAppsViewController
} }
} }
final class MyAppsViewController: UICollectionViewController class MyAppsViewController: UICollectionViewController
{ {
private let coordinator = NSFileCoordinator() private let coordinator = NSFileCoordinator()
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
@@ -155,13 +153,6 @@ final class MyAppsViewController: UICollectionViewController
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue) @IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{ {
} }
var minimuxerStatus: Bool {
guard minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
return false
}
return true
}
} }
private extension MyAppsViewController private extension MyAppsViewController
@@ -195,7 +186,7 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage> func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{ {
let fetchRequest = InstalledApp.updatesFetchRequest() let fetchRequest = InstalledApp.updatesFetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestSupportedVersion?.date, ascending: false), fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)] NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
@@ -204,21 +195,21 @@ private extension MyAppsViewController
dataSource.cellIdentifierHandler = { _ in "UpdateCell" } dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let self = self else { return } guard let self = self else { return }
guard let app = installedApp.storeApp, let latestSupportedVersion = app.latestSupportedVersion else { return } guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return }
let cell = cell as! UpdateCollectionViewCell let cell = cell as! UpdateCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription cell.versionDescriptionTextView.text = app.versionDescription
cell.bannerView.iconImageView.image = nil cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.configure(for: app) cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter) let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate cell.bannerView.subtitleLabel.text = versionDate
let appName: String let appName: String
@@ -232,7 +223,7 @@ private extension MyAppsViewController
appName = app.name appName = app.name
} }
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.version, versionDate) cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
cell.bannerView.button.isIndicatingActivity = false cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -336,25 +327,21 @@ private extension MyAppsViewController
let currentDate = Date() let currentDate = Date()
let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate) let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate)
let numberOfDaysText: String
let formatter = DateComponentsFormatter() if numberOfDays == 1
formatter.unitsStyle = .full {
formatter.includesApproximationPhrase = false numberOfDaysText = NSLocalizedString("1 day", comment: "")
formatter.includesTimeRemainingPhrase = false }
else
formatter.allowedUnits = [.day, .hour, .minute] {
numberOfDaysText = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
formatter.maximumUnitCount = 1 }
cell.bannerView.button.setTitle(formatter.string(from: currentDate, to: installedApp.expirationDate)?.uppercased(), for: .normal)
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
formatter.includesTimeRemainingPhrase = true cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText)
cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " "
// Make sure refresh button is correct size. // Make sure refresh button is correct size.
cell.layoutIfNeeded() cell.layoutIfNeeded()
@@ -535,9 +522,11 @@ private extension MyAppsViewController
guard !failures.isEmpty else { return } guard !failures.isEmpty else { return }
let toastView: ToastView
if let failure = failures.first, results.count == 1 if let failure = failures.first, results.count == 1
{ {
ToastView(error: failure.value).show(in: self) toastView = ToastView(error: failure.value)
} }
else else
{ {
@@ -555,10 +544,11 @@ private extension MyAppsViewController
let error = failures.first?.value as NSError? let error = failures.first?.value as NSError?
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
let toastView = ToastView(text: localizedText, detailText: detailText, opensLog: true) toastView = ToastView(text: localizedText, detailText: detailText)
toastView.preferredDuration = 4.0 toastView.preferredDuration = 4.0
toastView.show(in: self)
} }
toastView.show(in: self)
} }
self.refreshGroup = nil self.refreshGroup = nil
@@ -649,8 +639,6 @@ private extension MyAppsViewController
@IBAction func refreshAllApps(_ sender: UIBarButtonItem) @IBAction func refreshAllApps(_ sender: UIBarButtonItem)
{ {
guard minimuxerStatus else { return }
self.isRefreshingAllApps = true self.isRefreshingAllApps = true
self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.collectionViewLayout.invalidateLayout()
@@ -694,7 +682,8 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
case .failure(let error): case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
@@ -712,11 +701,18 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem) @IBAction func sideloadApp(_ sender: UIBarButtonItem)
{ {
guard minimuxerStatus else { return } let supportedTypes: [String]
let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil)
let documentPickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes, asCopy: true) if let types = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "ipa" as CFString, nil)?.takeRetainedValue()
{
supportedTypes = (types as NSArray).map { $0 as! String }
}
else
{
supportedTypes = ["com.apple.itunes.ipa"] // Declared by the system.
}
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
documentPickerViewController.delegate = self documentPickerViewController.delegate = self
self.present(documentPickerViewController, animated: true, completion: nil) self.present(documentPickerViewController, animated: true, completion: nil)
} }
@@ -895,8 +891,9 @@ private extension MyAppsViewController
completion(.failure((OperationError.cancelled))) completion(.failure((OperationError.cancelled)))
case .failure(let error): case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
completion(.failure(error)) completion(.failure(error))
} }
} }
@@ -1010,14 +1007,13 @@ private extension MyAppsViewController
UIApplication.shared.open(installedApp.openAppURL) { success in UIApplication.shared.open(installedApp.openAppURL) { success in
guard !success else { return } guard !success else { return }
ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self) let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
toastView.show(in: self)
} }
} }
func refresh(_ installedApp: InstalledApp) func refresh(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
let previousProgress = AppManager.shared.refreshProgress(for: installedApp) let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else { guard previousProgress == nil else {
previousProgress?.cancel() previousProgress?.cancel()
@@ -1039,8 +1035,6 @@ private extension MyAppsViewController
func activate(_ installedApp: InstalledApp) func activate(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
func finish(_ result: Result<InstalledApp, Error>) func finish(_ result: Result<InstalledApp, Error>)
{ {
do do
@@ -1061,7 +1055,8 @@ private extension MyAppsViewController
DispatchQueue.main.async { DispatchQueue.main.async {
installedApp.isActive = false installedApp.isActive = false
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
} }
} }
} }
@@ -1115,8 +1110,7 @@ private extension MyAppsViewController
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil) func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
{ {
guard installedApp.isActive, minimuxerStatus else { return } guard installedApp.isActive else { return }
installedApp.isActive = false installedApp.isActive = false
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
@@ -1129,12 +1123,13 @@ private extension MyAppsViewController
} }
catch catch
{ {
print("Failed to deactivate app:", error) print("Failed to activate app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
installedApp.isActive = true installedApp.isActive = true
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
} }
} }
@@ -1165,7 +1160,8 @@ private extension MyAppsViewController
case .success: break case .success: break
case .failure(let error): case .failure(let error):
DispatchQueue.main.async { DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
} }
} }
} }
@@ -1176,8 +1172,6 @@ private extension MyAppsViewController
func backup(_ installedApp: InstalledApp) func backup(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
let title = NSLocalizedString("Start Backup?", comment: "") let title = NSLocalizedString("Start Backup?", comment: "")
let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "") let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "")
@@ -1199,8 +1193,9 @@ private extension MyAppsViewController
print("Failed to back up app:", error) print("Failed to back up app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue]) self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
} }
} }
@@ -1216,8 +1211,6 @@ private extension MyAppsViewController
func restore(_ installedApp: InstalledApp) func restore(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name) let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet) let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel) alertController.addAction(.cancel)
@@ -1235,7 +1228,8 @@ private extension MyAppsViewController
print("Failed to restore app:", error) print("Failed to restore app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
} }
} }
} }
@@ -1252,11 +1246,8 @@ private extension MyAppsViewController
{ {
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return } guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return }
let documentPicker = UIDocumentPickerViewController(forExporting: [backupURL], asCopy: true) let documentPicker = UIDocumentPickerViewController(url: backupURL, in: .exportToService)
documentPicker.delegate = self
// Don't set delegate to avoid conflicting with import callbacks.
// documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil) self.present(documentPicker, animated: true, completion: nil)
} }
@@ -1310,7 +1301,8 @@ private extension MyAppsViewController
print("Failed to change app icon.", error) print("Failed to change app icon.", error)
DispatchQueue.main.async { DispatchQueue.main.async {
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
} }
} }
} }
@@ -1319,22 +1311,14 @@ private extension MyAppsViewController
@available(iOS 14, *) @available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp) func enableJIT(for installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
if #available(iOS 17, *) {
ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self)
AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result switch result
{ {
case .success: break case .success: break
case .failure(let error): case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp) toastView.show(in: self)
} }
} }
} }
@@ -1481,7 +1465,7 @@ extension MyAppsViewController
let registeredAppIDs = team.appIDs.count let registeredAppIDs = team.appIDs.count
let maximumAppIDCount = 10 let maximumAppIDCount = 10
let remainingAppIDs = maximumAppIDCount - registeredAppIDs let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
if remainingAppIDs == 1 if remainingAppIDs == 1
{ {
@@ -1492,7 +1476,7 @@ extension MyAppsViewController
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs)) footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
} }
footerView.textLabel.isHidden = remainingAppIDs < 0 footerView.textLabel.isHidden = false
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
@unknown default: break @unknown default: break
@@ -2066,8 +2050,15 @@ extension MyAppsViewController: UIDocumentPickerDelegate
{ {
guard let fileURL = urls.first else { return } guard let fileURL = urls.first else { return }
self.sideloadApp(at: fileURL) { (result) in switch controller.documentPickerMode
print("Sideloaded app at \(fileURL) with result:", result) {
case .import, .open:
self.sideloadApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
}
case .exportToService, .moveToService: break
@unknown default: break
} }
} }
} }

View File

@@ -17,7 +17,7 @@ extension UpdateCollectionViewCell
} }
} }
@objc final class UpdateCollectionViewCell: UICollectionViewCell @objc class UpdateCollectionViewCell: UICollectionViewCell
{ {
var mode: Mode = .expanded { var mode: Mode = .expanded {
didSet { didSet {

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class NewsCollectionViewCell: UICollectionViewCell class NewsCollectionViewCell: UICollectionViewCell
{ {
@IBOutlet var titleLabel: UILabel! @IBOutlet var titleLabel: UILabel!
@IBOutlet var captionLabel: UILabel! @IBOutlet var captionLabel: UILabel!

View File

@@ -14,7 +14,7 @@ import Roxas
import Nuke import Nuke
private final class AppBannerFooterView: UICollectionReusableView private class AppBannerFooterView: UICollectionReusableView
{ {
let bannerView = AppBannerView(frame: .zero) let bannerView = AppBannerView(frame: .zero)
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil) let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
@@ -41,7 +41,7 @@ private final class AppBannerFooterView: UICollectionReusableView
} }
} }
final class NewsViewController: UICollectionViewController class NewsViewController: UICollectionViewController
{ {
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero) private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
@@ -313,8 +313,9 @@ private extension NewsViewController
{ {
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case .failure(let error): case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self) let toastView = ToastView(error: error)
toastView.show(in: self)
case .success: print("Installed app:", storeApp.bundleIdentifier) case .success: print("Installed app:", storeApp.bundleIdentifier)
} }
@@ -390,9 +391,9 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp) let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress footerView.bannerView.button.progress = progress
if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date() if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
{ {
footerView.bannerView.button.countdownDate = versionDate footerView.bannerView.button.countdownDate = storeApp.versionDate
} }
else else
{ {
@@ -425,10 +426,6 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
return previousSize return previousSize
} }
// Take layout margins into account.
self.prototypeCell.layoutMargins.left = self.view.layoutMargins.left
self.prototypeCell.layoutMargins.right = self.view.layoutMargins.right
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width) let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
NSLayoutConstraint.activate([widthConstraint]) NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) } defer { NSLayoutConstraint.deactivate([widthConstraint]) }

View File

@@ -12,10 +12,8 @@ import Network
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import minimuxer
typealias AuthenticationError = AuthenticationErrorCode.Error enum AuthenticationError: LocalizedError
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
{ {
case noTeam case noTeam
case noCertificate case noCertificate
@@ -24,11 +22,11 @@ enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
case missingPrivateKey case missingPrivateKey
case missingCertificate case missingCertificate
var errorFailureReason: String { var errorDescription: String? {
switch self { switch self {
case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "") case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "") case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "") case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "") case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
} }
@@ -36,7 +34,7 @@ enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
} }
@objc(AuthenticationOperation) @objc(AuthenticationOperation)
final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)> class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
{ {
let context: AuthenticatedOperationContext let context: AuthenticatedOperationContext
@@ -214,8 +212,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
guard guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context), let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context) let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
else { throw AuthenticationError(.noTeam) } else { throw AuthenticationError.noTeam }
// Account // Account
account.isActiveAccount = true account.isActiveAccount = true
@@ -433,7 +431,7 @@ private extension AuthenticationOperation
} }
else else
{ {
completionHandler(.failure(error ?? OperationError.unknown())) completionHandler(.failure(error ?? OperationError.unknown))
} }
} }
} }
@@ -450,7 +448,7 @@ private extension AuthenticationOperation
if let team = teams.first { if let team = teams.first {
return completionHandler(.success(team)) return completionHandler(.success(team))
} else { } else {
return completionHandler(.failure(AuthenticationError(.noTeam))) return completionHandler(.failure(AuthenticationError.noTeam))
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -461,7 +459,7 @@ private extension AuthenticationOperation
if !self.present(selectTeamViewController) if !self.present(selectTeamViewController)
{ {
return completionHandler(.failure(AuthenticationError(.noTeam))) return completionHandler(.failure(AuthenticationError.noTeam))
} }
} }
} }
@@ -490,20 +488,20 @@ private extension AuthenticationOperation
{ {
func requestCertificate() func requestCertificate()
{ {
let machineName = "SideStore - " + UIDevice.current.name let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
do do
{ {
let certificate = try Result(certificate, error).get() let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) } guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do do
{ {
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else { guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw AuthenticationError(.missingCertificate) throw AuthenticationError.missingCertificate
} }
certificate.privateKey = privateKey certificate.privateKey = privateKey
@@ -524,7 +522,7 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate]) func replaceCertificate(from certificates: [ALTCertificate])
{ {
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "SideStore") == true }) ?? certificates.first else { return completionHandler(.failure(OperationError.notAuthenticated)) } guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success if let error = error, !success
@@ -595,7 +593,7 @@ private extension AuthenticationOperation
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void) func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{ {
guard let udid = fetch_udid()?.toString() else { guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID)) return completionHandler(.failure(OperationError.unknownUDID))
} }

View File

@@ -11,14 +11,12 @@ import CoreData
import AltStoreCore import AltStoreCore
import EmotionalDamage import EmotionalDamage
import minimuxer
typealias RefreshError = RefreshErrorCode.Error enum RefreshError: LocalizedError
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
{ {
case noInstalledApps case noInstalledApps
var errorFailureReason: String { var errorDescription: String? {
switch self switch self
{ {
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "") case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
@@ -53,7 +51,7 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns
} }
@objc(BackgroundRefreshAppsOperation) @objc(BackgroundRefreshAppsOperation)
final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]> class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
{ {
let installedApps: [InstalledApp] let installedApps: [InstalledApp]
private let managedObjectContext: NSManagedObjectContext private let managedObjectContext: NSManagedObjectContext
@@ -95,23 +93,11 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
super.main() super.main()
guard !self.installedApps.isEmpty else { guard !self.installedApps.isEmpty else {
self.finish(.failure(RefreshError(.noInstalledApps))) self.finish(.failure(RefreshError.noInstalledApps))
return return
} }
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
} catch {
self.finish(.failure(error))
}
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
} else {
start_auto_mounter(documentsDirectory)
}
self.managedObjectContext.perform { self.managedObjectContext.perform {
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier)) print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
@@ -203,7 +189,7 @@ private extension BackgroundRefreshAppsOperation
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
var shouldPresentAlert = true var shouldPresentAlert = false
do do
{ {
@@ -219,18 +205,20 @@ private extension BackgroundRefreshAppsOperation
content.title = NSLocalizedString("Refreshed Apps", comment: "") content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "") content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
} }
catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.noInstalledApps catch RefreshError.noInstalledApps
{ {
shouldPresentAlert = false shouldPresentAlert = false
} }
catch catch
{ {
print("Failed to refresh apps in background.", error) print("Failed to refresh apps in background.", error)
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "") content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription content.body = error.localizedDescription
shouldPresentAlert = false
} }
if shouldPresentAlert if shouldPresentAlert
{ {
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)

View File

@@ -29,9 +29,6 @@ class BackupAppOperation: ResultOperation<Void>
private var appName: String? private var appName: String?
private var timeoutTimer: Timer? private var timeoutTimer: Timer?
private weak var applicationWillReturnObserver: NSObjectProtocol?
private weak var backupResponseObserver: NSObjectProtocol?
init(action: Action, context: InstallAppOperationContext) init(action: Action, context: InstallAppOperationContext)
{ {
self.action = action self.action = action
@@ -46,7 +43,10 @@ class BackupAppOperation: ResultOperation<Void>
do do
{ {
if let error = self.context.error { throw error } if let error = self.context.error
{
throw error
}
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters } guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
context.perform { context.perform {
@@ -55,15 +55,13 @@ class BackupAppOperation: ResultOperation<Void>
let appName = installedApp.name let appName = installedApp.name
self.appName = appName self.appName = appName
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
throw OperationError.appNotFound(name: appName)
}
let altstoreOpenURL = altstoreApp.openAppURL let altstoreOpenURL = altstoreApp.openAppURL
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false) var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
returnURLComponents?.host = "appBackupResponse" returnURLComponents?.host = "appBackupResponse"
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) } guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
var openURLComponents = URLComponents() var openURLComponents = URLComponents()
openURLComponents.scheme = installedApp.openAppURL.scheme openURLComponents.scheme = installedApp.openAppURL.scheme
openURLComponents.host = self.action.rawValue openURLComponents.host = self.action.rawValue
@@ -155,11 +153,8 @@ private extension BackupAppOperation
{ {
func registerObservers() func registerObservers()
{ {
self.applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in var applicationWillReturnObserver: NSObjectProtocol!
defer { applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
self?.applicationWillReturnObserver.map { NotificationCenter.default.removeObserver($0) }
}
guard let self = self, !self.isFinished else { return } guard let self = self, !self.isFinished else { return }
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
@@ -171,17 +166,18 @@ private extension BackupAppOperation
self.finish(.failure(OperationError.timedOut)) self.finish(.failure(OperationError.timedOut))
} }
} }
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
} }
self.backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in var backupResponseObserver: NSObjectProtocol!
defer { backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
self?.backupResponseObserver.map { NotificationCenter.default.removeObserver($0) }
}
self?.timeoutTimer?.invalidate() self?.timeoutTimer?.invalidate()
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult) let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
self?.finish(result) self?.finish(result)
NotificationCenter.default.removeObserver(backupResponseObserver!)
} }
} }
} }

View File

@@ -1,208 +0,0 @@
//
// ClearAppCacheOperation.swift
// AltStore
//
// Created by Riley Testut on 9/27/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
/*
struct BatchError: ALTLocalizedError
{
enum Code: Int, ALTErrorCode
{
typealias Error = BatchError
case batchError
}
var code: Code = .batchError
var underlyingErrors: [Error]
var errorTitle: String?
var errorFailure: String?
init(errors: [Error])
{
self.underlyingErrors = errors
}
var errorFailureReason: String {
guard !self.underlyingErrors.isEmpty else { return NSLocalizedString("An unknown error occured.", comment: "") }
let errorMessages = self.underlyingErrors.map { $0.localizedDescription }
let message = errorMessages.joined(separator: "\n\n")
return message
}
}
*/
@objc(ClearAppCacheOperation)
class ClearAppCacheOperation: ResultOperation<Void>
{
private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue()
override init()
{
self.coordinatorQueue.name = "AltStore - ClearAppCacheOperation Queue"
}
override func main()
{
super.main()
var allErrors = [Error]()
self.clearTemporaryDirectory { result in
switch result
{
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
case .failure(let error): allErrors.append(error)
case .success: break
}
self.removeUninstalledAppBackupDirectories { result in
switch result
{
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
case .failure(let error): allErrors.append(error)
case .success: break
}
if allErrors.isEmpty
{
self.finish(.success(()))
}
else
{
self.finish(.failure(OperationError.cacheClearError(errors: allErrors.map({ error in
return error.localizedDescription
}))))
}
}
}
}
}
private extension ClearAppCacheOperation
{
func clearTemporaryDirectory(completion: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: FileManager.default.temporaryDirectory, options: [.forDeleting])
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
do
{
if let error
{
throw error
}
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
includingPropertiesForKeys: [],
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
var errors = [Error]()
for fileURL in fileURLs
{
do
{
print("[ALTLog] Removing item from temporary directory:", fileURL.lastPathComponent)
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("[ALTLog] Failed to remove \(fileURL.lastPathComponent) from temporary directory.", error)
errors.append(error)
}
}
if !errors.isEmpty
{
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
return error.localizedDescription
}))))
}
else
{
completion(.success(()))
}
}
catch
{
completion(.failure(error))
}
}
}
func removeUninstalledAppBackupDirectories(completion: @escaping (Result<Void, Error>) -> Void)
{
guard let backupsDirectory = FileManager.default.appBackupsDirectory else { return completion(.failure(OperationError.missingAppGroup)) }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let installedAppBundleIDs = Set(InstalledApp.all(in: context).map { $0.bundleIdentifier })
let intent = NSFileAccessIntent.writingIntent(with: backupsDirectory, options: [.forDeleting])
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
do
{
if let error
{
throw error
}
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: intent.url.path, isDirectory: &isDirectory), isDirectory.boolValue else {
completion(.success(()))
return
}
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
includingPropertiesForKeys: [.isDirectoryKey, .nameKey],
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
var errors = [Error]()
for backupDirectory in fileURLs
{
do
{
let resourceValues = try backupDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue }
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !AppManager.shared.isActivelyManagingApp(withBundleID: bundleID)
{
print("[ALTLog] Removing backup directory for uninstalled app:", bundleID)
try FileManager.default.removeItem(at: backupDirectory)
}
}
catch
{
print("[ALTLog] Failed to remove app backup directory:", error)
errors.append(error)
}
}
if !errors.isEmpty
{
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
return error.localizedDescription
}))))
}
else
{
completion(.success(()))
}
}
catch
{
print("[ALTLog] Failed to remove app backup directory:", error)
completion(.failure(error))
}
}
}
}
}

View File

@@ -14,7 +14,7 @@ import Roxas
import minimuxer import minimuxer
@objc(DeactivateAppOperation) @objc(DeactivateAppOperation)
final class DeactivateAppOperation: ResultOperation<InstalledApp> class DeactivateAppOperation: ResultOperation<InstalledApp>
{ {
let app: InstalledApp let app: InstalledApp
let context: OperationContext let context: OperationContext
@@ -31,7 +31,11 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
{ {
super.main() super.main()
if let error = self.context.error { return self.finish(.failure(error)) } if let error = self.context.error
{
self.finish(.failure(error))
return
}
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApp = context.object(with: self.app.objectID) as! InstalledApp let installedApp = context.object(with: self.app.objectID) as! InstalledApp
@@ -40,15 +44,20 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
for profile in allIdentifiers { for profile in allIdentifiers {
do { do {
try remove_provisioning_profile(profile) let res = try remove_provisioning_profile(id: profile)
self.progress.completedUnitCount += 1 if case Uhoh.Bad(let code) = res {
installedApp.isActive = false self.finish(.failure(minimuxer_to_operation(code: code)))
self.finish(.success(installedApp)) }
break } catch Uhoh.Bad(let code) {
self.finish(.failure(minimuxer_to_operation(code: code)))
} catch { } catch {
self.finish(.failure(error)) self.finish(.failure(ALTServerError(.unknownResponse)))
} }
} }
self.progress.completedUnitCount += 1
installedApp.isActive = false
self.finish(.success(installedApp))
} }
} }
} }

View File

@@ -12,108 +12,66 @@ import Roxas
import AltStoreCore import AltStoreCore
import AltSign import AltSign
private extension DownloadAppOperation
{
struct DependencyError: ALTLocalizedError
{
let dependency: Dependency
let error: Error
var failure: String? {
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
}
var underlyingError: Error? {
return self.error
}
}
}
@objc(DownloadAppOperation) @objc(DownloadAppOperation)
final class DownloadAppOperation: ResultOperation<ALTApplication> class DownloadAppOperation: ResultOperation<ALTApplication>
{ {
let app: AppProtocol let app: AppProtocol
let context: AppOperationContext let context: AppOperationContext
private let appName: String
private let bundleIdentifier: String private let bundleIdentifier: String
private var sourceURL: URL?
private let destinationURL: URL private let destinationURL: URL
private let session = URLSession(configuration: .default) private let session = URLSession(configuration: .default)
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL() private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext) init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
{ {
self.app = app self.app = app
self.context = context self.context = context
self.appName = app.name
self.bundleIdentifier = app.bundleIdentifier self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = destinationURL self.destinationURL = destinationURL
super.init() super.init()
// App = 3, Dependencies = 1 // App = 3, Dependencies = 1
self.progress.totalUnitCount = 4 self.progress.totalUnitCount = 4
} }
override func main() override func main()
{ {
super.main() super.main()
if let error = self.context.error if let error = self.context.error
{ {
self.finish(.failure(error)) self.finish(.failure(error))
return return
} }
print("Downloading App:", self.bundleIdentifier) print("Downloading App:", self.bundleIdentifier)
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName) guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
guard let storeApp = self.app as? StoreApp else { return self.download(self.app) } self.downloadApp(from: sourceURL) { result in
storeApp.managedObjectContext?.perform {
do {
let latestVersion = try self.verify(storeApp)
self.download(latestVersion)
} catch let error as VerificationError where error.code == .iOSVersionNotSupported {
guard let presentingViewController = self.context.presentingViewController,
let latestSupportedVersion = storeApp.latestSupportedVersion,
case let version = latestSupportedVersion.version,
version != storeApp.installedApp?.version else {
return self.finish(.failure(error))
}
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
DispatchQueue.main.async {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
self.finish(.failure(OperationError.cancelled))
})
alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in
self.download(latestSupportedVersion)
})
presentingViewController.present(alertController, animated: true)
}
} catch {
self.finish(.failure(error))
}
}
}
override func finish(_ result: Result<ALTApplication, any Error>) {
do {
try FileManager.default.removeItem(at: self.temporaryDirectory)
} catch {
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
}
super.finish(result)
}
}
private extension DownloadAppOperation {
func verify(_ storeApp: StoreApp) throws -> AppVersion {
guard let version = storeApp.latestAvailableVersion else {
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
throw OperationError.unknown(failureReason: failureReason)
}
if let minOSVersion = version.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: minOSVersion)
} else if let maxOSVersion = version.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: maxOSVersion)
}
return version
}
func download(@Managed _ app: AppProtocol) {
guard let sourceURL = $app.url else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
self.downloadIPA(from: sourceURL) { result in
do do
{ {
let application = try result.get() let application = try result.get()
@@ -154,7 +112,24 @@ private extension DownloadAppOperation {
} }
} }
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void) override func finish(_ result: Result<ALTApplication, Error>)
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectory)
}
catch
{
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
}
super.finish(result)
}
}
private extension DownloadAppOperation
{
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
{ {
func finishOperation(_ result: Result<URL, Error>) func finishOperation(_ result: Result<URL, Error>)
{ {
@@ -163,8 +138,8 @@ private extension DownloadAppOperation {
let fileURL = try result.get() let fileURL = try result.get()
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) } guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL: URL let appBundleURL: URL
@@ -203,9 +178,6 @@ private extension DownloadAppOperation {
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
do do
{ {
if let response = response as? HTTPURLResponse {
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
}
let (fileURL, _) = try Result((fileURL, response), error).get() let (fileURL, _) = try Result((fileURL, response), error).get()
finishOperation(.success(fileURL)) finishOperation(.success(fileURL))
@@ -280,7 +252,7 @@ private extension DownloadAppOperation
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data) let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set<URL>() var dependencyURLs = Set<URL>()
var dependencyError: Error? var dependencyError: DependencyError?
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1) let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
@@ -313,7 +285,7 @@ private extension DownloadAppOperation
} }
catch let error as DecodingError catch let error as DecodingError
{ {
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name)) let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
completionHandler(.failure(nsError)) completionHandler(.failure(nsError))
} }
catch catch
@@ -322,7 +294,7 @@ private extension DownloadAppOperation
} }
} }
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void) func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
{ {
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do do
@@ -343,10 +315,9 @@ private extension DownloadAppOperation
completionHandler(.success(destinationURL)) completionHandler(.success(destinationURL))
} }
catch let error as NSError catch
{ {
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename) completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
} }
} }
progress.addChild(downloadTask.progress, withPendingUnitCount: 1) progress.addChild(downloadTask.progress, withPendingUnitCount: 1)

View File

@@ -9,17 +9,9 @@
import UIKit import UIKit
import Combine import Combine
import minimuxer import minimuxer
import UniformTypeIdentifiers
import AltStoreCore import AltStoreCore
enum SideJITServerErrorType: Error {
case invalidURL
case errorConnecting
case deviceNotFound
case other(String)
}
@available(iOS 14, *) @available(iOS 14, *)
protocol EnableJITContext protocol EnableJITContext
{ {
@@ -29,7 +21,7 @@ protocol EnableJITContext
} }
@available(iOS 14, *) @available(iOS 14, *)
final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void> class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
{ {
let context: Context let context: Context
@@ -51,105 +43,25 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
} }
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) } guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
if #available(iOS 17, *) {
let sideJITenabled = UserDefaults.standard.sidejitenable installedApp.managedObjectContext?.perform {
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? "" let v = minimuxer_to_operation(code: 1)
do {
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier)
switch x {
case .Good:
self.finish(.success(()))
case .Bad(let code):
self.finish(.failure(minimuxer_to_operation(code: code)))
}
} catch Uhoh.Bad(let code) {
self.finish(.failure(minimuxer_to_operation(code: code)))
} catch {
self.finish(.failure(OperationError.unknown))
}
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: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
let pContent = message[startRange.upperBound..<endRange.lowerBound]
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
print(message + " + " + String(pContent))
} else {
print(message)
self.finish(.failure(OperationError.SideJITIssue(error: message)))
}
}
case .success():
self.finish(.success(()))
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
}
}
return
}
}
} else {
installedApp.managedObjectContext?.perform {
var retries = 3
while (retries > 0){
do {
try debug_app(installedApp.resignedBundleIdentifier)
self.finish(.success(()))
retries = 0
} catch {
retries -= 1
if (retries <= 0){
self.finish(.failure(error))
}
}
}
}
} }
} }
} }
@available(iOS 17, *)
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
guard let udid = fetch_udid()?.toString() else {
completion(.failure(.other("Unable to get UDID")))
return
}
var SJSURL = serverurl
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
if let error = error {
completion(.failure(.errorConnecting))
return
}
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
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
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)
completion(.failure(errorType))
}
}
task.resume()
}

View File

@@ -7,28 +7,15 @@
// //
import Foundation import Foundation
import CommonCrypto
import Starscream
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
@objc(FetchAnisetteDataOperation) @objc(FetchAnisetteDataOperation)
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
{ {
let context: OperationContext let context: OperationContext
var socket: WebSocket!
var url: URL?
var startProvisioningURL: URL?
var endProvisioningURL: URL?
var clientInfo: String?
var userAgent: String?
var mdLu: String?
var deviceId: String?
init(context: OperationContext) init(context: OperationContext)
{ {
@@ -45,413 +32,32 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
return return
} }
self.url = URL(string: UserDefaults.standard.menuAnisetteURL) let url = AnisetteManager.currentURL
print("Anisette URL: \(self.url!.absoluteString)") DLOG("Anisette URL: %@", url.absoluteString)
if let identifier = Keychain.shared.identifier, let task = URLSession.shared.dataTask(with: url) { data, response, error in
let adiPb = Keychain.shared.adiPb { guard let data = data, error == nil else { return }
fetchAnisetteV3(identifier, adiPb)
} else {
provision()
}
}
// MARK: - COMMON
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
// make sure this JSON is in the format we expect
// convert data to json
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
if v3 {
if json["result"] == "GetHeadersError" {
let message = json["message"]
print("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")
Keychain.shared.adiPb = nil
return provision()
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
}
}
// try to read out a dictionary
// for some reason serial number isn't needed but it doesn't work unless it has a value
var formattedJSON: [String: String] = ["deviceSerialNumber": "0"]
if let machineID = json["X-Apple-I-MD-M"] { formattedJSON["machineID"] = machineID }
if let oneTimePassword = json["X-Apple-I-MD"] { formattedJSON["oneTimePassword"] = oneTimePassword }
if let routingInfo = json["X-Apple-I-MD-RINFO"] { formattedJSON["routingInfo"] = routingInfo }
if v3 {
formattedJSON["deviceDescription"] = self.clientInfo!
formattedJSON["localUserID"] = self.mdLu!
formattedJSON["deviceUniqueIdentifier"] = self.deviceId!
// Generate date stuff on client
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.timeZone = TimeZone.current
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
let dateString = formatter.string(from: Date())
formattedJSON["date"] = dateString
formattedJSON["locale"] = Locale.current.identifier
formattedJSON["timeZone"] = TimeZone.current.abbreviation()
} else {
if let deviceDescription = json["X-MMe-Client-Info"] { formattedJSON["deviceDescription"] = deviceDescription }
if let localUserID = json["X-Apple-I-MD-LU"] { formattedJSON["localUserID"] = localUserID }
if let deviceUniqueIdentifier = json["X-Mme-Device-Id"] { formattedJSON["deviceUniqueIdentifier"] = deviceUniqueIdentifier }
if let date = json["X-Apple-I-Client-Time"] { formattedJSON["date"] = date }
if let locale = json["X-Apple-Locale"] { formattedJSON["locale"] = locale }
if let timeZone = json["X-Apple-I-TimeZone"] { formattedJSON["timeZone"] = timeZone }
}
if let response = response,
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
print("Implementation-Version: \(version)")
} else { print("No Implementation-Version header") }
print("Anisette used: \(formattedJSON)")
print("Original JSON: \(json)")
if let anisette = ALTAnisetteData(json: formattedJSON) {
print("Anisette is valid!")
self.finish(.success(anisette))
} else {
print("Anisette is invalid!!!!")
if v3 {
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
} else {
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not have all the required fields)")
}
}
} else {
if v3 {
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not be in JSON)")
} else {
throw OperationError.anisetteV1Error(message: "Invalid anisette (the returned data may not be in JSON)")
}
}
}
// MARK: - V1
func handleV1() {
print("Server is V1")
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
print("Server has already been trusted, fetching anisette")
return self.fetchAnisetteV1()
}
print("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")
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
self.fetchAnisetteV1()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
print("Cancelled anisette operation")
self.finish(.failure(OperationError.cancelled))
}))
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
DispatchQueue.main.async {
if let presentingController = keyWindow?.rootViewController?.presentedViewController {
presentingController.present(alert, animated: true)
} else {
keyWindow?.rootViewController?.present(alert, animated: true)
}
}
}
func fetchAnisetteV1() {
print("Fetching anisette V1")
URLSession.shared.dataTask(with: self.url!) { data, response, error in
do { do {
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") } // make sure this JSON is in the format we expect
// convert data to json
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false) if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
// try to read out a dictionary
//for some reason serial number isn't needed but it doesn't work unless it has a value
let formattedJSON: [String: String] = ["machineID": json["X-Apple-I-MD-M"]!, "oneTimePassword": json["X-Apple-I-MD"]!, "localUserID": json["X-Apple-I-MD-LU"]!, "routingInfo": json["X-Apple-I-MD-RINFO"]!, "deviceUniqueIdentifier": json["X-Mme-Device-Id"]!, "deviceDescription": json["X-MMe-Client-Info"]!, "date": json["X-Apple-I-Client-Time"]!, "locale": json["X-Apple-Locale"]!, "timeZone": json["X-Apple-I-TimeZone"]!, "deviceSerialNumber": "1"]
if let anisette = ALTAnisetteData(json: formattedJSON) {
DLOG("Anisette used: %@", formattedJSON)
self.finish(.success(anisette))
}
}
} catch let error as NSError { } catch let error as NSError {
print("Failed to load: \(error.localizedDescription)") print("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error)) self.finish(.failure(error))
} }
}.resume()
}
// MARK: - V3: PROVISIONING
func provision() {
fetchClientInfo {
print("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
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let startProvisioningString = plist["urls"]?["midStartProvisioning"] as? String,
let startProvisioningURL = URL(string: startProvisioningString),
let endProvisioningString = plist["urls"]?["midFinishProvisioning"] as? String,
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.startProvisioningSession()
} else {
print("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()
}
}
func startProvisioningSession() {
let provisioningSessionURL = self.url!.appendingPathComponent("v3").appendingPathComponent("provisioning_session")
var wsRequest = URLRequest(url: provisioningSessionURL)
wsRequest.timeoutInterval = 5
self.socket = WebSocket(request: wsRequest)
self.socket.delegate = self
self.socket.connect()
}
func didReceive(event: WebSocketEvent, client: WebSocketClient) {
switch event {
case .text(let string):
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")
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)")
switch result {
case "GiveIdentifier":
print("Giving identifier")
client.json(["identifier": Keychain.shared.identifier!])
case "GiveStartProvisioningData":
print("Getting start provisioning data")
let body = [
"Header": [String: Any](),
"Request": [String: Any](),
]
var request = self.buildAppleRequest(url: self.startProvisioningURL!)
request.httpMethod = "POST"
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let spim = plist["Response"]?["spim"] as? String {
print("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")")
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")
guard let cpim = json["cpim"] as? String else {
print("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
}
let body = [
"Header": [String: Any](),
"Request": [
"cpim": cpim,
],
]
var request = self.buildAppleRequest(url: self.endProvisioningURL!)
request.httpMethod = "POST"
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let ptm = plist["Response"]?["ptm"] as? String,
let tk = plist["Response"]?["tk"] as? String {
print("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")")
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!")
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.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
return
}
Keychain.shared.adiPb = adiPb
self.fetchAnisetteV3(Keychain.shared.identifier!, Keychain.shared.adiPb!)
default:
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
print("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.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
}
case .connected:
print("Connected")
case .disconnected(let string, let code):
print("Disconnected: \(code); \(string)")
case .error(let error):
print("Got error: \(String(describing: error))")
default:
print("Unknown event: \(event)")
}
}
func buildAppleRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.setValue(self.clientInfo!, forHTTPHeaderField: "X-Mme-Client-Info")
request.setValue(self.userAgent!, forHTTPHeaderField: "User-Agent")
request.setValue("text/x-xml-plist", forHTTPHeaderField: "Content-Type")
request.setValue("*/*", forHTTPHeaderField: "Accept")
request.setValue(self.mdLu!, forHTTPHeaderField: "X-Apple-I-MD-LU")
request.setValue(self.deviceId!, forHTTPHeaderField: "X-Mme-Device-Id")
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
let dateString = formatter.string(from: Date())
request.setValue(dateString, forHTTPHeaderField: "X-Apple-I-Client-Time")
request.setValue(Locale.current.identifier, forHTTPHeaderField: "X-Apple-Locale")
request.setValue(TimeZone.current.abbreviation(), forHTTPHeaderField: "X-Apple-I-TimeZone")
return request
}
// MARK: - V3: FETCHING
func fetchClientInfo(_ callback: @escaping () -> Void) {
if self.clientInfo != nil &&
self.userAgent != nil &&
self.mdLu != nil &&
self.deviceId != nil &&
Keychain.shared.identifier != nil {
print("Skipping client_info fetch since all the properties we need aren't nil")
return callback()
}
print("Trying to get client_info")
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
do {
guard let data = data, error == nil else {
return self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The server may be down\(error != nil ? " (\(error!.localizedDescription))" : "")")))
}
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
if let clientInfo = json["client_info"] {
print("Server is V3")
self.clientInfo = clientInfo
self.userAgent = json["user_agent"]!
print("Client-Info: \(self.clientInfo!)")
print("User-Agent: \(self.userAgent!)")
if Keychain.shared.identifier == nil {
print("Generating identifier")
var bytes = [Int8](repeating: 0, count: 16)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
if status != errSecSuccess {
print("ERROR GENERATING IDENTIFIER!!! \(status)")
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
}
Keychain.shared.identifier = Data(bytes: &bytes, count: bytes.count).base64EncodedString()
}
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
self.mdLu = decoded.sha256().hexEncodedString()
print("X-Apple-I-MD-LU: \(self.mdLu!)")
let uuid: UUID = decoded.object()
self.deviceId = uuid.uuidString.uppercased()
print("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.handleV1()
}
}.resume()
}
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
fetchClientInfo {
print("Fetching anisette V3")
let url = UserDefaults.standard.menuAnisetteURL
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
request.httpMethod = "POST"
request.httpBody = try! JSONSerialization.data(withJSONObject: [
"identifier": identifier,
"adi_pb": adiPb
], options: [])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { data, response, error in
do {
guard let data = data, error == nil else { throw OperationError.anisetteV3Error(message: "Couldn't fetch anisette") }
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
} }
task.resume()
} }
} }
extension WebSocketClient {
func json(_ dictionary: [String: String]) {
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
self.write(string: String(data: data, encoding: .utf8)!)
}
}
extension Data {
// https://stackoverflow.com/a/25391020
func sha256() -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
self.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
}
return Data(hash)
}
// https://stackoverflow.com/a/40089462
func hexEncodedString() -> String {
return self.map { String(format: "%02hhX", $0) }.joined()
}
// https://stackoverflow.com/a/59127761
func object<T>() -> T { self.withUnsafeBytes { $0.load(as: T.self) } }
}

View File

@@ -13,7 +13,7 @@ import AltSign
import Roxas import Roxas
@objc(FetchAppIDsOperation) @objc(FetchAppIDsOperation)
final class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)> class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{ {
let context: AuthenticatedOperationContext let context: AuthenticatedOperationContext
let managedObjectContext: NSManagedObjectContext let managedObjectContext: NSManagedObjectContext

View File

@@ -13,7 +13,7 @@ import AltSign
import Roxas import Roxas
@objc(FetchProvisioningProfilesOperation) @objc(FetchProvisioningProfilesOperation)
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]> class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{ {
let context: AppOperationContext let context: AppOperationContext
@@ -45,8 +45,8 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
let session = self.context.session let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) } guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count) self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
@@ -260,21 +260,16 @@ extension FetchProvisioningProfilesOperation
{ {
if let expirationDate = sortedExpirationDates.first if let expirationDate = sortedExpirationDates.first
{ {
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate) throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
} }
} }
} }
//App ID name must be ascii. If the name is not ascii, using bundleID instead
let appIDName: String
if !name.allSatisfy({ $0.isASCII }) {
//Contains non ASCII (Such as Chinese/Japanese...), using bundleID
appIDName = bundleIdentifier
}else {
//ASCII text, keep going as usual
appIDName = name
}
ALTAppleAPI.shared.addAppID(withName: appIDName, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
do do
{ {
do do
@@ -286,7 +281,7 @@ extension FetchProvisioningProfilesOperation
{ {
if let expirationDate = sortedExpirationDates.first if let expirationDate = sortedExpirationDates.first
{ {
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate) throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
} }
else else
{ {
@@ -389,39 +384,19 @@ extension FetchProvisioningProfilesOperation
if app.isAltStoreApp if app.isAltStoreApp
{ {
print("Application groups before modifying for SideStore: \(applicationGroups)")
// Remove app groups that contain AltStore since they can be problematic (cause SideStore to expire early)
for (index, group) in applicationGroups.enumerated() {
if group.contains("AltStore") {
print("Removing application group: \(group)")
applicationGroups.remove(at: index)
}
}
// Make sure we add .AltWidget for the widget
var altStoreAppGroupID = Bundle.baseAltStoreAppGroupID
for (_, group) in applicationGroups.enumerated() {
if group.contains("AltWidget") {
altStoreAppGroupID += ".AltWidget"
break
}
}
// Potentially updating app groups for this specific AltStore. // Potentially updating app groups for this specific AltStore.
// Find the (unique) AltStore app group, then replace it // Find the (unique) AltStore app group, then replace it
// with the correct "base" app group ID. // with the correct "base" app group ID.
// Otherwise, we may append a duplicate team identifier to the end. // Otherwise, we may append a duplicate team identifier to the end.
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) }) if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
{ {
applicationGroups[index] = altStoreAppGroupID applicationGroups[index] = Bundle.baseAltStoreAppGroupID
} }
else else
{ {
applicationGroups.append(altStoreAppGroupID) applicationGroups.append(Bundle.baseAltStoreAppGroupID)
} }
} }
print("Application groups: \(applicationGroups)")
// Dispatch onto global queue to prevent appGroupsLock deadlock. // Dispatch onto global queue to prevent appGroupsLock deadlock.
DispatchQueue.global().async { DispatchQueue.global().async {
@@ -503,13 +478,10 @@ extension FetchProvisioningProfilesOperation
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error) switch Result(success, error)
{ {
case .failure: case .failure(let error): completionHandler(.failure(error))
// 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: case .success:
// Fetch new provisioning profile
// Fetch new provisiong profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error)) completionHandler(Result(profile, error))
} }

View File

@@ -13,7 +13,7 @@ import AltStoreCore
import Roxas import Roxas
@objc(FetchSourceOperation) @objc(FetchSourceOperation)
final class FetchSourceOperation: ResultOperation<Source> class FetchSourceOperation: ResultOperation<Source>
{ {
let sourceURL: URL let sourceURL: URL
let managedObjectContext: NSManagedObjectContext let managedObjectContext: NSManagedObjectContext

View File

@@ -32,7 +32,7 @@ extension FetchTrustedSourcesOperation
} }
} }
final class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]> class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
{ {
override func main() override func main()
{ {

View File

@@ -11,10 +11,9 @@ import Network
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
import minimuxer
@objc(InstallAppOperation) @objc(InstallAppOperation)
final class InstallAppOperation: ResultOperation<InstalledApp> class InstallAppOperation: ResultOperation<InstalledApp>
{ {
let context: InstallAppOperationContext let context: InstallAppOperationContext
@@ -41,14 +40,12 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
guard guard
let certificate = self.context.certificate, let certificate = self.context.certificate,
let resignedApp = self.context.resignedApp, let resignedApp = self.context.resignedApp
let provisioningProfiles = self.context.provisioningProfiles
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform { backgroundContext.perform {
/* App */ /* App */
let installedApp: InstalledApp let installedApp: InstalledApp
@@ -118,7 +115,8 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to. // Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
self.cleanUp() self.cleanUp()
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit, provisioningProfiles.contains(where: { $1.isFreeProvisioningProfile == true }) var activeProfiles: Set<String>?
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
{ {
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit. // When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
@@ -143,70 +141,23 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
installedApp.isActive = false installedApp.isActive = false
} }
} }
}
else activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
{ let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
installedApp.isActive = true return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
} }
var installing = true let ns_bundle = NSString(string: installedApp.bundleIdentifier)
if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil { let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if UIApplication.shared.applicationState != .active {
print("We are not in the foreground, let's not do anything")
return
}
if !installing {
print("Installing finished")
return
}
print("We are still installing after 3 seconds")
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch (settings.authorizationStatus) {
case .authorized, .ephemeral, .provisional:
print("Notifications are enabled")
let content = UNMutableNotificationContent()
content.title = "Refreshing..."
content.body = "SideStore will automatically move to the homescreen to finish refreshing!"
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
UNUserNotificationCenter.current().add(notification)
break
default:
print("Notifications are not enabled")
let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
print("Going home")
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}))
DispatchQueue.main.async {
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(alert, animated: true)
} else {
print("No key window? Let's just go home")
}
}
}
}
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
}
do { let res = minimuxer_install_ipa(ns_bundle_ptr)
try install_ipa(installedApp.bundleIdentifier) if res == 0 {
installing = false
installedApp.refreshedDate = Date() installedApp.refreshedDate = Date()
self.finish(.success(installedApp)) self.finish(.success(installedApp))
} catch let error {
installing = false } else {
self.finish(.failure(error)) self.finish(.failure(minimuxer_to_operation(code: res)))
} }
} }
} }
@@ -223,11 +174,10 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
do do
{ {
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
print("Removed refreshed IPA")
} }
catch catch
{ {
print("Failed to remove refreshed .ipa: \(error)") print("Failed to remove refreshed .ipa:", error)
} }
} }

View File

@@ -12,10 +12,7 @@ import Roxas
class ResultOperation<ResultType>: Operation class ResultOperation<ResultType>: Operation
{ {
var resultHandler: ((Result<ResultType, Error>) -> Void)? var resultHandler: ((Result<ResultType, Error>) -> Void)?
// Should only be set by subclasses
var localizedFailure: String?
@available(*, unavailable) @available(*, unavailable)
override func finish() override func finish()
{ {
@@ -25,20 +22,16 @@ class ResultOperation<ResultType>: Operation
func finish(_ result: Result<ResultType, Error>) func finish(_ result: Result<ResultType, Error>)
{ {
guard !self.isFinished else { return } guard !self.isFinished else { return }
var result = result
if self.isCancelled if self.isCancelled
{ {
result = .failure(OperationError.cancelled) self.resultHandler?(.failure(OperationError.cancelled))
} }
else if case .failure(let nsError as NSError) = result, let localizedFailure, nsError.localizedFailure == nil { else
// Error doesn't have its own localizedFailure, so we give it the Operation's (if it exists) {
let error = nsError.withLocalizedFailure(localizedFailure) self.resultHandler?(result)
result = .failure(error)
} }
self.resultHandler?(result)
super.finish() super.finish()
} }
} }

View File

@@ -38,7 +38,7 @@ class OperationContext
} }
} }
final class AuthenticatedOperationContext: OperationContext class AuthenticatedOperationContext: OperationContext
{ {
var session: ALTAppleAPISession? var session: ALTAppleAPISession?

View File

@@ -8,186 +8,81 @@
import Foundation import Foundation
import AltSign import AltSign
import AltStoreCore
import minimuxer
extension OperationError enum OperationError: LocalizedError
{ {
enum Code: Int, ALTErrorCode, CaseIterable { static let domain = OperationError.unknown._domain
typealias Error = OperationError
// General
case unknown = 1000
case unknownResult
case cancelled
case timedOut
case unableToConnectSideJIT
case unableToRespondSideJITDevice
case wrongSideJITIP
case SideJITIssue // (error: String)
case refreshsidejit
case notAuthenticated
case appNotFound
case unknownUDID
case invalidApp
case invalidParameters
case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
case openAppFailed//(name: String)
case missingAppGroup
// Connection
case noWiFi = 1200
case tooNewError
case anisetteV1Error//(message: String)
case provisioningError//(result: String, message: String?)
case anisetteV3Error//(message: String)
case cacheClearError//(errors: [String])
}
static let unknownResult: OperationError = .init(code: .unknownResult)
static let cancelled: OperationError = .init(code: .cancelled)
static let timedOut: OperationError = .init(code: .timedOut)
static let unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
static let unknownUDID: OperationError = .init(code: .unknownUDID)
static let invalidApp: OperationError = .init(code: .invalidApp)
static let invalidParameters: OperationError = .init(code: .invalidParameters)
static let noSources: OperationError = .init(code: .noSources)
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
static let noWiFi: OperationError = .init(code: .noWiFi)
static let tooNewError: OperationError = .init(code: .tooNewError)
static let provisioningError: OperationError = .init(code: .provisioningError)
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
static let cacheClearError: OperationError = .init(code: .cacheClearError)
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
static func appNotFound(name: String?) -> OperationError {
OperationError(code: .appNotFound, appName: name)
}
static func openAppFailed(name: String?) -> OperationError {
OperationError(code: .openAppFailed, appName: name)
}
static func SideJITIssue(error: String?) -> OperationError { case unknown
var o = OperationError(code: .SideJITIssue) case unknownResult
o.errorFailure = error case cancelled
return o case timedOut
}
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError { case notAuthenticated
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate) case appNotFound
}
case unknownUDID
static func provisioningError(result: String, message: String?) -> OperationError {
var o = OperationError(code: .provisioningError, failureReason: result) case invalidApp
o.errorTitle = message case invalidParameters
return o
} case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
static func cacheClearError(errors: [String]) -> OperationError { case noSources
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
} case openAppFailed(name: String)
case missingAppGroup
static func anisetteV1Error(message: String) -> OperationError {
OperationError(code: .anisetteV1Error, failureReason: message) case noDevice
} case createService(name: String)
case getFromDevice(name: String)
static func anisetteV3Error(message: String) -> OperationError { case setArgument(name: String)
OperationError(code: .anisetteV3Error, failureReason: message) case afc
} case install
case uninstall
} case lookupApps
case detach
case functionArguments
struct OperationError: ALTLocalizedError { case profileInstall
case noConnection
let code: Code
var failureReason: String? {
var errorTitle: String? switch self {
var errorFailure: String? case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
var appName: String?
var requiredAppIDs: Int?
var availableAppIDs: Int?
var expirationDate: Date?
var sourceFile: String?
var sourceLine: UInt?
private var _failureReason: String?
private init(code: Code, failureReason: String? = nil,
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
self.code = code
self._failureReason = failureReason
self.appName = appName
self.requiredAppIDs = requiredAppIDs
self.availableAppIDs = availableAppIDs
self.expirationDate = expirationDate
self.sourceFile = sourceFile
self.sourceLine = sourceLine
}
var errorFailureReason: String {
switch self.code {
case .unknown:
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "")
guard let sourceFile, let sourceLine else { return failureReason }
failureReason += " (\(sourceFile) line \(sourceLine)"
return failureReason
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "") case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "") case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "") case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "") case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "") case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "") case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "") case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "") case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
case .appNotFound: case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
let appName = self.appName ?? NSLocalizedString("The app", comment: "") case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName) case .noDevice: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
case .openAppFailed: case .createService(let name): return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
let appName = self.appName ?? NSLocalizedString("The app", comment: "") case .getFromDevice(let name): return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName) case .setArgument(let name): return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "") case .afc: return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "") case .install: return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "") case .uninstall: return NSLocalizedString("Unable to uninstall the app", comment: "")
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "") case .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", comment: "")
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "") case .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "")
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "") case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "") case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "") case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
} }
} }
var recoverySuggestion: String? { var recoverySuggestion: String? {
switch self.code switch self
{ {
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "") case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
case .maximumAppIDLimitReached:
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "") let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage } let message: String
var message: String
if requiredAppIDs > 1 if requiredAppIDs > 1
{ {
let availableText: String let availableText: String
@@ -199,23 +94,23 @@ struct OperationError: ALTLocalizedError {
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs)) default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
} }
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText) let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage + "\n\n" message = prefixMessage + " " + baseMessage
} }
else else
{ {
message = baseMessage + " " let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.maximumUnitCount = 1
dateComponentsFormatter.unitsStyle = .full
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
message = baseMessage + " " + remainingTimeMessage
} }
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
let dateFormatter = DateComponentsFormatter()
dateFormatter.maximumUnitCount = 1
dateFormatter.unitsStyle = .full
let remainingTime = dateFormatter.string(from: dateComponents)!
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
return message return message
default: return nil default: return nil
@@ -223,66 +118,49 @@ struct OperationError: ALTLocalizedError {
} }
} }
extension MinimuxerError: LocalizedError { func minimuxer_to_operation(code: Int32) -> OperationError {
public var failureReason: String? { switch code {
switch self { case -1:
case .NoDevice: return OperationError.noDevice
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "") case -2:
case .NoConnection: return OperationError.createService(name: "debug")
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "") case -3:
case .PairingFile: return OperationError.createService(name: "instproxy")
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "") case -4:
return OperationError.getFromDevice(name: "installed apps")
case .CreateDebug: case -5:
return self.createService(name: "debug") return OperationError.getFromDevice(name: "path to the app")
case .LookupApps: case -6:
return self.getFromDevice(name: "installed apps") return OperationError.getFromDevice(name: "bundle path")
case .FindApp: case -7:
return self.getFromDevice(name: "path to the app") return OperationError.setArgument(name: "max packet")
case .BundlePath: case -8:
return self.getFromDevice(name: "bundle path") return OperationError.setArgument(name: "working directory")
case .MaxPacket: case -9:
return self.setArgument(name: "max packet") return OperationError.setArgument(name: "argv")
case .WorkingDirectory: case -10:
return self.setArgument(name: "working directory") return OperationError.getFromDevice(name: "launch success")
case .Argv: case -11:
return self.setArgument(name: "argv") return OperationError.detach
case .LaunchSuccess: case -12:
return self.getFromDevice(name: "launch success") return OperationError.functionArguments
case .Detach: case -13:
return NSLocalizedString("Unable to detach from the app's process", comment: "") return OperationError.createService(name: "AFC")
case .Attach: case -14:
return NSLocalizedString("Unable to attach to the app's process", comment: "") return OperationError.afc
case -15:
case .CreateInstproxy: return OperationError.install
return self.createService(name: "instproxy") case -16:
case .CreateAfc: return OperationError.uninstall
return self.createService(name: "AFC") case -17:
case .RwAfc: return OperationError.createService(name: "misagent")
return NSLocalizedString("AFC was unable to manage files on the device", comment: "") case -18:
case .InstallApp(let message): return OperationError.profileInstall
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "") case -19:
case .UninstallApp: return OperationError.profileInstall
return NSLocalizedString("Unable to uninstall the app", comment: "") case -20:
return OperationError.noConnection
case .CreateMisagent: default:
return self.createService(name: "misagent") return OperationError.unknown
case .ProfileInstall:
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .ProfileRemove:
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
}
}
fileprivate func createService(name: String) -> String {
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
}
fileprivate func getFromDevice(name: String) -> String {
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
}
fileprivate func setArgument(name: String) -> String {
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
} }
} }

View File

@@ -25,38 +25,22 @@ protocol PatchAppContext
var error: Error? { get } var error: Error? { get }
} }
extension PatchAppError enum PatchAppError: LocalizedError
{ {
enum Code: Int, ALTErrorCode, CaseIterable { case unsupportedOperatingSystemVersion(OperatingSystemVersion)
typealias Error = PatchAppError
var errorDescription: String? {
case unsupportedOperatingSystemVersion switch self
} {
case .unsupportedOperatingSystemVersion(let osVersion):
static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError { var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion) if osVersion.patchVersion != 0
} {
} osVersionString += ".\(osVersion.patchVersion)"
struct PatchAppError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
var osVersion: OperatingSystemVersion?
var errorFailureReason: String {
switch self.code {
case .unsupportedOperatingSystemVersion:
let osVersionString: String
if let osVersion = self.osVersion?.stringValue {
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
} else {
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
} }
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
} }
} }
} }
@@ -68,7 +52,7 @@ private struct OTAUpdate
} }
@available(iOS 14, *) @available(iOS 14, *)
final class PatchAppOperation: ResultOperation<Void> class PatchAppOperation: ResultOperation<Void>
{ {
let context: PatchAppContext let context: PatchAppContext

View File

@@ -29,7 +29,7 @@ extension PatchViewController
} }
@available(iOS 14.0, *) @available(iOS 14.0, *)
final class PatchViewController: UIViewController class PatchViewController: UIViewController
{ {
var patchApp: AnyApp? var patchApp: AnyApp?
var installedApp: InstalledApp? var installedApp: InstalledApp?
@@ -439,7 +439,7 @@ private extension PatchViewController
do do
{ {
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() } guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
_ = try result.get() _ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier) if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)

View File

@@ -14,7 +14,7 @@ import Roxas
import minimuxer import minimuxer
@objc(RefreshAppOperation) @objc(RefreshAppOperation)
final class RefreshAppOperation: ResultOperation<InstalledApp> class RefreshAppOperation: ResultOperation<InstalledApp>
{ {
let context: AppOperationContext let context: AppOperationContext
@@ -35,27 +35,34 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
do do
{ {
if let error = self.context.error { return self.finish(.failure(error)) } if let error = self.context.error
{
throw error
}
guard let profiles = self.context.provisioningProfiles else { return self.finish(.failure(OperationError.invalidParameters)) } guard let profiles = self.context.provisioningProfiles else { throw OperationError.invalidParameters }
guard let app = self.context.app else { return self.finish(.failure(OperationError(.appNotFound(name: nil)))) }
guard let app = self.context.app else { throw OperationError.appNotFound }
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
print("Sending refresh app request...")
for p in profiles { for p in profiles {
do { do {
let bytes = p.value.data.toRustByteSlice() let x = try install_provisioning_profile(plist: p.value.data)
try install_provisioning_profile(bytes.forRust()) if case .Bad(let code) = x {
} catch { self.finish(.failure(minimuxer_to_operation(code: code)))
self.finish(.failure(MinimuxerError.ProfileInstall)) }
} } catch Uhoh.Bad(let code) {
self.finish(.failure(minimuxer_to_operation(code: code)))
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in } catch {
self.finish(.failure(OperationError.unknown))
}
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier) let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
self.managedObjectContext.perform { self.managedObjectContext.perform {
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else { guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
self.finish(.failure(OperationError(.appNotFound(name: app.name))))
return return
} }
installedApp.update(provisioningProfile: p.value) installedApp.update(provisioningProfile: p.value)
@@ -68,5 +75,9 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
} }
} }
} }
catch
{
self.finish(.failure(error))
}
} }
} }

View File

@@ -12,7 +12,7 @@ import CoreData
import AltStoreCore import AltStoreCore
import AltSign import AltSign
final class RefreshGroup: NSObject class RefreshGroup: NSObject
{ {
let context: AuthenticatedOperationContext let context: AuthenticatedOperationContext
let progress = Progress.discreteProgress(totalUnitCount: 0) let progress = Progress.discreteProgress(totalUnitCount: 0)

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
@objc(RemoveAppBackupOperation) @objc(RemoveAppBackupOperation)
final class RemoveAppBackupOperation: ResultOperation<Void> class RemoveAppBackupOperation: ResultOperation<Void>
{ {
let context: InstallAppOperationContext let context: InstallAppOperationContext

View File

@@ -12,7 +12,7 @@ import AltStoreCore
import minimuxer import minimuxer
@objc(RemoveAppOperation) @objc(RemoveAppOperation)
final class RemoveAppOperation: ResultOperation<InstalledApp> class RemoveAppOperation: ResultOperation<InstalledApp>
{ {
let context: InstallAppOperationContext let context: InstallAppOperationContext
@@ -39,11 +39,15 @@ final class RemoveAppOperation: ResultOperation<InstalledApp>
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
do { do {
try remove_app(resignedBundleIdentifier) let res = try remove_app(app_id: resignedBundleIdentifier)
if case Uhoh.Bad(let code) = res {
self.finish(.failure(minimuxer_to_operation(code: code)))
}
} catch Uhoh.Bad(let code) {
self.finish(.failure(minimuxer_to_operation(code: code)))
} catch { } catch {
return self.finish(.failure(error)) self.finish(.failure(ALTServerError(.appDeletionFailed)))
} }
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1

View File

@@ -11,10 +11,9 @@ import Roxas
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import minimuxer
@objc(ResignAppOperation) @objc(ResignAppOperation)
final class ResignAppOperation: ResultOperation<ALTApplication> class ResignAppOperation: ResultOperation<ALTApplication>
{ {
let context: InstallAppOperationContext let context: InstallAppOperationContext
@@ -62,7 +61,6 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
{ {
let destinationURL = InstalledApp.refreshedIPAURL(for: app) let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true) try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
print("Successfully resigned app to \(destinationURL.absoluteString)")
// Use appBundleURL since we need an app bundle, not .ipa. // Use appBundleURL since we need an app bundle, not .ipa.
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp } guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
@@ -116,9 +114,7 @@ private extension ResignAppOperation
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.altBundleID] = identifier infoDictionary[Bundle.Info.altBundleID] = identifier
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>" infoDictionary[Bundle.Info.devicePairingString] = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String
infoDictionary.removeValue(forKey: "DTXcode")
infoDictionary.removeValue(forKey: "DTXcodeBuild")
for (key, value) in additionalInfoDictionaryValues for (key, value) in additionalInfoDictionaryValues
{ {
@@ -151,14 +147,6 @@ private extension ResignAppOperation
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL) try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
// Remove _CodeSignature folder (if it exists) because it will be added when resigning and it may have files that aren't overwritten when resigning
// These files might be the cause of some ApplicationVerificationFailed errors
let codeSignaturePath = bundle.bundleURL.appendingPathComponent("_CodeSignature").absoluteString.replacingOccurrences(of: "file://", with: "")
if FileManager.default.fileExists(atPath: codeSignaturePath) {
try FileManager.default.removeItem(atPath: codeSignaturePath)
print("Removed _CodeSignature folder at \(codeSignaturePath)")
}
} }
DispatchQueue.global().async { DispatchQueue.global().async {
@@ -184,9 +172,9 @@ private extension ResignAppOperation
if app.isAltStoreApp if app.isAltStoreApp
{ {
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID } guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID } guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.devicePairingString] = "<insert pairing file here>" additionalValues[Bundle.Info.devicePairingString] = pairingFileString
additionalValues[Bundle.Info.deviceID] = udid additionalValues[Bundle.Info.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
@@ -205,7 +193,7 @@ private extension ResignAppOperation
// The embedded certificate + certificate identifier are already in app bundle, no need to update them. // The embedded certificate + certificate identifier are already in app bundle, no need to update them.
} }
} }
else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = fetch_udid()?.toString() as? String else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String
{ {
// There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID. // There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID.
additionalValues[Bundle.Info.deviceID] = udid additionalValues[Bundle.Info.deviceID] = udid
@@ -230,7 +218,6 @@ private extension ResignAppOperation
// Prepare app // Prepare app
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues) try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
try self.removeMissingAppExtensionReferences(from: appBundle)
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{ {
@@ -271,28 +258,4 @@ private extension ResignAppOperation
return progress return progress
} }
func removeMissingAppExtensionReferences(from bundle: Bundle) throws
{
// If app extensions have been removed from an app (either by AltStore or the developer),
// we must remove all references to them from SC_Info/Manifest.plist (if it exists).
let scInfoURL = bundle.bundleURL.appendingPathComponent("SC_Info")
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
guard let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL), let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String] else { return }
// Remove references to missing files.
let filteredReplicationPaths = sinfReplicationPaths.filter { path in
guard let fileURL = URL(string: path, relativeTo: bundle.bundleURL) else { return false }
let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
return fileExists
}
manifestPlist["SinfReplicationPaths"] = filteredReplicationPaths
// Save updated Manifest.plist to disk.
try manifestPlist.write(to: manifestPlistURL)
}
} }

View File

@@ -9,10 +9,9 @@ import Foundation
import Network import Network
import AltStoreCore import AltStoreCore
import minimuxer
@objc(SendAppOperation) @objc(SendAppOperation)
final class SendAppOperation: ResultOperation<()> class SendAppOperation: ResultOperation<()>
{ {
let context: InstallAppOperationContext let context: InstallAppOperationContext
@@ -33,7 +32,8 @@ final class SendAppOperation: ResultOperation<()>
if let error = self.context.error if let error = self.context.error
{ {
return self.finish(.failure(error)) self.finish(.failure(error))
return
} }
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) } guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
@@ -44,20 +44,25 @@ final class SendAppOperation: ResultOperation<()>
print("AFC App `fileURL`: \(fileURL.absoluteString)") print("AFC App `fileURL`: \(fileURL.absoluteString)")
let ns_bundle = NSString(string: app.bundleIdentifier)
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
if let data = NSData(contentsOf: fileURL) { if let data = NSData(contentsOf: fileURL) {
do { let pls = UnsafeMutablePointer<UInt8>.allocate(capacity: data.length)
let bytes = Data(data).toRustByteSlice() for (index, data) in data.enumerated() {
try yeet_app_afc(app.bundleIdentifier, bytes.forRust()) pls[index] = data
self.progress.completedUnitCount += 1
self.finish(.success(()))
} catch {
self.finish(.failure(MinimuxerError.RwAfc))
self.progress.completedUnitCount += 1
self.finish(.success(()))
} }
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
if res == 0 {
print("minimuxer_yeet_app_afc `res` == \(res)")
self.progress.completedUnitCount += 1
self.finish(.success(()))
} else {
self.finish(.failure(minimuxer_to_operation(code: res)))
}
} else { } else {
print("IPA doesn't exist????") self.finish(.failure(ALTServerError(.underlyingError)))
self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name))))
} }
} }
} }

View File

@@ -30,7 +30,7 @@ extension UpdatePatronsOperation
} }
} }
final class UpdatePatronsOperation: ResultOperation<Void> class UpdatePatronsOperation: ResultOperation<Void>
{ {
let context: NSManagedObjectContext let context: NSManagedObjectContext

View File

@@ -8,93 +8,54 @@
import Foundation import Foundation
import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
extension VerificationError enum VerificationError: ALTLocalizedError
{ {
enum Code: Int, ALTErrorCode, CaseIterable { case privateEntitlements(ALTApplication, entitlements: [String: Any])
typealias Error = VerificationError case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String)
case iOSVersionNotSupported(ALTApplication)
case privateEntitlements
case mismatchedBundleIdentifiers
case iOSVersionNotSupported
}
static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
}
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
}
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
VerificationError(code: .iOSVersionNotSupported, app: app)
}
}
struct VerificationError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var app: AppProtocol?
var entitlements: [String: Any]?
var sourceBundleID: String?
var deviceOSVersion: OperatingSystemVersion?
var requiredOSVersion: OperatingSystemVersion?
var errorDescription: String? { var app: ALTApplication {
switch self.code { switch self
case .iOSVersionNotSupported: {
guard let deviceOSVersion else { return nil } case .privateEntitlements(let app, _): return app
case .mismatchedBundleIdentifiers(let app, _): return app
var failureReason = self.errorFailureReason case .iOSVersionNotSupported(let app): return app
if self.app == nil {
let firstLetter = failureReason.prefix(1).lowercased()
failureReason = firstLetter + failureReason.dropFirst()
}
return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason)
default: return nil
} }
} }
var errorFailureReason: String { var failure: String? {
switch self.code return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name)
}
var failureReason: String? {
switch self
{ {
case .privateEntitlements: case .privateEntitlements(let app, _):
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "") return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name)
return String(formatted: "“%@” requires private permissions.", appName)
case .mismatchedBundleIdentifiers(let app, let sourceBundleID):
case .mismatchedBundleIdentifiers: return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID)
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID) case .iOSVersionNotSupported(let app):
} else { let name = app.name
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
} var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
if app.minimumiOSVersion.patchVersion > 0
case .iOSVersionNotSupported: {
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "") version += ".\(app.minimumiOSVersion.patchVersion)"
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
guard let requiredOSVersion else {
return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue)
}
if deviceOSVersion > requiredOSVersion {
return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue)
} else {
return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue)
} }
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
return localizedDescription
} }
} }
} }
@objc(VerifyAppOperation) @objc(VerifyAppOperation)
final class VerifyAppOperation: ResultOperation<Void> class VerifyAppOperation: ResultOperation<Void>
{ {
let context: AppOperationContext let context: AppOperationContext
var verificationHandler: ((VerificationError) -> Bool)? var verificationHandler: ((VerificationError) -> Bool)?
@@ -119,14 +80,12 @@ final class VerifyAppOperation: ResultOperation<Void>
guard let app = self.context.app else { throw OperationError.invalidParameters } guard let app = self.context.app else { throw OperationError.invalidParameters }
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) { guard app.bundleIdentifier == self.context.bundleIdentifier else {
guard app.bundleIdentifier == self.context.bundleIdentifier else { throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier)
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
}
} }
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else { guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion) throw VerificationError.iOSVersionNotSupported(app)
} }
if #available(iOS 13.5, *) if #available(iOS 13.5, *)
@@ -157,7 +116,7 @@ final class VerifyAppOperation: ResultOperation<Void>
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any] let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
app.hasPrivateEntitlements = true app.hasPrivateEntitlements = true
let error = VerificationError.privateEntitlements(entitlements, app: app) let error = VerificationError.privateEntitlements(app, entitlements: entitlements)
self.process(error) { (result) in self.process(error) { (result) in
self.finish(result.mapError { $0 as Error }) self.finish(result.mapError { $0 as Error })
} }
@@ -186,10 +145,9 @@ private extension VerifyAppOperation
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) } guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
DispatchQueue.main.async { DispatchQueue.main.async {
switch error.code switch error
{ {
case .privateEntitlements: case .privateEntitlements(_, let entitlements):
guard let entitlements = error.entitlements else { return completion(.failure(error)) }
let permissions = entitlements.keys.sorted().joined(separator: "\n") let permissions = entitlements.keys.sorted().joined(separator: "\n")
let message = String(format: NSLocalizedString(""" let message = String(format: NSLocalizedString("""
You must allow access to these private permissions before continuing: You must allow access to these private permissions before continuing:
@@ -208,7 +166,8 @@ private extension VerifyAppOperation
})) }))
presentingViewController.present(alertController, animated: true, completion: nil) presentingViewController.present(alertController, animated: true, completion: nil)
case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error)) case .mismatchedBundleIdentifiers: return completion(.failure(error))
case .iOSVersionNotSupported: return completion(.failure(error))
} }
} }
} }

Binary file not shown.

View File

@@ -1,158 +1 @@
{ {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
"images" : [
{
"filename" : "40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "riley.jpg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "shane.jpeg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 846 KiB

View File

@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "1024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,269 @@
{
"name": "AltStore",
"identifier": "com.rileytestut.AltStore",
"sourceURL": "https://cdn.altstore.io/file/altstore/apps.json",
"apps": [
{
"name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore",
"developerName": "Riley Testut",
"version": "1.5.1",
"versionDate": "2022-07-14T12:00:00-05:00",
"versionDescription": "This update fixes the following issues:\n\n• Using Apple IDs that contain capital letters\n• Using Apple IDs with 2FA enabled without any trusted devices\n• Repeatedly asking some users to sign in every refresh\n• \"Incorrect Apple ID or password\" error after changing Apple ID email address\n• “Application is missing application-identifier” error when sideloading or (de-)activating certain apps\n• Potential crash when receiving unknown error codes from AltServer",
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_5_1.ipa",
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis version of AltStore allows you to install Delta, an all-in-one emulator for iOS, as well as sideload other .ipa files from the Files app.",
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084",
"size": 5465976,
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
"https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG",
"https://user-images.githubusercontent.com/705880/65605577-332cba80-df5e-11e9-9f00-b369ce974f71.PNG"
],
"permissions": [
{
"type": "background-fetch",
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
},
{
"type": "background-audio",
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
}
]
},
{
"name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
"developerName": "Riley Testut",
"subtitle": "An alternative App Store for iOS.",
"version": "1.6b2",
"versionDate": "2022-09-21T13:00:00-05:00",
"versionDescription": "• Fixed “error migrating persistent store” issue on launch\n\nPREVIOUS VERSION\n\nLock Screen Widget (iOS 16+)\n• Counts down days until AltStore expires\n• Comes in 2 different styles: “icon” and “text”\n\nError Log\n• View past errors in more detail\n• Tap an error to copy the error message or error code\n• Search for error code directly in AltStore FAQ",
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/altstore/1_6_b2.ipa",
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.",
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084",
"size": 5465933,
"beta": true,
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
"https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG",
"https://user-images.githubusercontent.com/705880/65605577-332cba80-df5e-11e9-9f00-b369ce974f71.PNG"
],
"permissions": [
{
"type": "background-fetch",
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
},
{
"type": "background-audio",
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
}
]
},
{
"name": "Delta",
"bundleIdentifier": "com.rileytestut.Delta",
"developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.",
"version": "1.3.1",
"versionDate": "2021-12-02T13:30:00-08:00",
"versionDescription": "• Fixes game artwork not loading\n• Fixes using deprecated DeSmuME core over melonDS core for some users",
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/delta/1_3_1.ipa",
"localizedDescription": "Delta is an all-in-one emulator for iOS. Delta builds upon the strengths of its predecessor, GBA4iOS, while expanding to include support for more game systems such as NES, SNES, and N64.\n\nFEATURES\n\nSupported Game Systems\n• Nintendo Entertainment System\n• Super Nintendo Entertainment System\n• Nintendo 64\n• Game Boy (Color)\n• Game Boy Advance\n• Nintendo DS\n• And plenty more to come!\n\nController Support\n• Supports PS4, PS5, Xbox One S, Xbox Series X, and MFi game controllers.\n• Supports bluetooth (and wired) keyboards, as well as the Apple Smart Keyboard.\n• Completely customize button mappings on a per-system, per-controller basis.\n• Map buttons to special “Quick Save”, “Quick Load,” and “Fast Forward” actions.\n\nSave States\n• Save and load save states for any game from the pause menu.\n• Lock save states to prevent them from being accidentally overwritten.\n• Automatically makes backup save states to ensure you never lose your progress.\n• Support for “Quick Saves,” save states that can be quickly saved/loaded with a single button press (requires external controller).\n\nCheats\n• Supports various types of cheat codes for each supported system:\n• NES: Game Genie\n• SNES: Game Genie, Pro Action Replay\n• N64: GameShark\n• GBC: Game Genie, GameShark\n• GBA: Action Replay, Code Breaker, GameShark\n• DS: Action Replay\n\nDelta Sync\n• Sync your games, game saves, save states, cheats, controller skins, and controller mappings between devices.\n• View version histories of everything you sync and optionally restore them to earlier versions.\n• Supports both Google Drive and Dropbox.\n\nCustom Controller Skins\n• Beautiful built-in controller skins for all systems.\n• Import controller skins made by others, or even make your own to share with the world!\n\nHold Button\n• Choose buttons for Delta to hold down on your behalf, freeing up your thumbs to press other buttons instead.\n• Perfect for games that typically require one button be held down constantly (ex: run button in Mario games, or the A button in Mario Kart).\n\nFast Forward\n• Speed through slower parts of games by running the game much faster than normal.\n• Easily enable or disable from the pause menu, or optionally with a mapped button on an external controller.\n\n3D/Haptic Touch\n• Use 3D or Haptic Touch to “peek” at games, save states, and cheat codes.\n• App icon shortcuts allow quick access to your most recently played games, or optionally customize the shortcuts to always include certain games.\n\nGame Artwork\n• Automatically displays appropriate box art for imported games.\n• Change a games artwork to anything you want, or select from the built-in game artwork database.\n\nMisc.\n• Gyroscope support (WarioWare: Twisted! only)\n• Microphone support (DS only)\n• Support for delta:// URL scheme to jump directly into a specific game.\n\n**Delta and AltStore LLC are in no way affiliated with Nintendo. The name \"Nintendo\" and all associated game console names are registered trademarks of Nintendo Co., Ltd.**",
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
"tintColor": "8A28F7",
"size": 19739373,
"permissions": [
{
"type": "photos",
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
}
],
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65600448-f7d9be00-df54-11e9-9e3e-d4c31296da94.PNG",
"https://user-images.githubusercontent.com/705880/65813009-f2ae8600-e183-11e9-9eb7-704effc11173.png",
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG",
"https://user-images.githubusercontent.com/705880/65601125-5b182000-df56-11e9-9e7e-261480e893c0.PNG"
]
},
{
"name": "Delta",
"bundleIdentifier": "com.rileytestut.Delta.Beta",
"developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.",
"version": "1.4b2",
"versionDate": "2022-08-16T08:00:00-05:00",
"versionDescription": "NEW\n• Supports Split View and Stage Manager multitasking on iPad\n• Automatically pauses + resumes emulation when switching between foreground apps with Stage Manager\n• Optimized full screen-width controller skins when using Split View, Slide Over, or Stage Manager\n• Supports controller skins with new `placement` parameter\n• Supports controller skins with custom screens that dont have explicit `outputFrame`\n\nFIXED\n• Fixed not detecting keyboard presses when remapping inputs\n• Fixed potential crash rendering game screen after changing EAGLContext\n• Fixed incorrect game screen frame when software keyboard appears on iOS 16\n• Fixed software keyboard sometimes appearing when not emulating anything",
"downloadURL": "https://cdn.altstore.io/file/altstore/apps/delta/1_4_b2.ipa",
"localizedDescription": "The next consoles for Delta are coming: this beta version of Delta brings support for playing Nintendo DS and Sega Genesis games!\n\nPlease report any issues you find to support@altstore.io. Thanks!",
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
"tintColor": "8A28F7",
"size": 42968657,
"beta": true,
"permissions": [
{
"type": "photos",
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
}
],
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65600448-f7d9be00-df54-11e9-9e3e-d4c31296da94.PNG",
"https://user-images.githubusercontent.com/705880/65601942-e5ad4f00-df57-11e9-9255-1463e0296e46.PNG",
"https://user-images.githubusercontent.com/705880/65813009-f2ae8600-e183-11e9-9eb7-704effc11173.png",
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG"
]
},
{
"name": "Clip",
"bundleIdentifier": "com.rileytestut.Clip",
"subtitle": "Manage your clipboard history with ease.",
"developerName": "Riley Testut",
"version": "1.0",
"versionDate": "2020-06-17T12:30:00-07:00",
"versionDescription": "Initial version 🎉",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/clip/1_0.ipa",
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
"tintColor": "EC008C",
"size": 445056,
"permissions": [
{
"type": "background-audio",
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
}
],
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png"
]
},
{
"name": "Clip",
"bundleIdentifier": "com.rileytestut.Clip.Beta",
"subtitle": "Manage your clipboard history with ease.",
"developerName": "Riley Testut",
"version": "1.1b1",
"versionDate": "2020-06-17T12:30:00-07:00",
"versionDescription": "This update adds a Custom Keyboard app extension for quick access to clippings when editing text.\n\nTo enable the keyboard, go to Settings > General > Keyboard > Keyboards > Add New Keyboard... and add \"ClipBoard\". Once added, make sure to then enable \"Allow Full Access\" for ClipBoard so it can access your clippings.",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/clip/1_1_b1.ipa",
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
"tintColor": "EC008C",
"size": 462771,
"beta": true,
"permissions": [
{
"type": "background-audio",
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
}
],
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png",
"https://user-images.githubusercontent.com/705880/84842227-70a80b00-aff9-11ea-8b04-bedb1f49c4a7.PNG",
"https://user-images.githubusercontent.com/705880/84842231-7271ce80-aff9-11ea-9272-e128aeceb95b.PNG"
]
}
],
"news": [
{
"title": "Delta Gaining DS Support",
"identifier": "delta-ds-support",
"caption": "Available this Saturday for patrons, coming soon for everyone else.",
"tintColor": "8A28F7",
"imageURL": "https://user-images.githubusercontent.com/705880/65603159-0676a400-df5a-11e9-882e-dc5566f4d50a.png",
"date": "2019-09-25",
"notify": false
},
{
"title": "Delta Now Available",
"identifier": "delta-now-available",
"caption": "Finally, relive your favorite NES, SNES, GB(C), GBA, and N64 games.",
"tintColor": "8A28F7",
"imageURL": "https://user-images.githubusercontent.com/705880/65604130-c1ec0800-df5b-11e9-8150-7657c474e3c3.png",
"appID": "com.rileytestut.Delta",
"date": "2019-09-28",
"notify": true
},
{
"title": "Sideloading is Here!",
"identifier": "sideloading-is-here",
"caption": "Update to AltStore 1.3 to install any app directly from Files.",
"tintColor": "018084",
"imageURL": "https://user-images.githubusercontent.com/705880/79022069-02932380-7b32-11ea-8bad-49907cb97ece.png",
"date": "2020-04-10T13:00:00-07:00",
"notify": true
},
{
"title": "iOS 13.4 Fixes App Crashes",
"identifier": "ios-13-4-now-available",
"caption": "Update to iOS 13.4 to fix some sideloaded apps crashing on launch.",
"tintColor": "34C759",
"date": "2020-04-10T13:30:00-07:00",
"notify": false
},
{
"title": "Clip Now Available!",
"identifier": "clip-now-available",
"caption": "Finally, a clipboard manager that can run in the background — no jailbreak required.",
"tintColor": "EC008C",
"imageURL": "https://user-images.githubusercontent.com/705880/65606598-04afdf00-df60-11e9-8f93-af6345d39557.png",
"appID": "com.rileytestut.Clip",
"date": "2020-06-17",
"notify": true
},
{
"title": "Delta, Meet Nintendo DS",
"identifier": "delta-meet-ds",
"caption": "Update to Delta 1.3 to relive all your favorite Nintendo DS games.",
"tintColor": "8A28F7",
"imageURL": "https://user-images.githubusercontent.com/705880/115617602-6ce2b600-a2a6-11eb-984e-2197a30c71e2.png",
"appID": "com.rileytestut.Delta",
"date": "2021-04-21",
"notify": true
},
{
"title": "#StandWithUkraine",
"identifier": "support-ukraine",
"caption": "Find out how you can help support those impacted by the Russian invasion.",
"tintColor": "003e80",
"imageURL": "https://user-images.githubusercontent.com/705880/156053447-a158cac7-df5f-4497-8025-15c3c2e10b48.png",
"url": "https://linktr.ee/razomforukraine",
"date": "2022-03-01",
"notify": false
},
{
"title": "The Biggest AltServer Update Yet!",
"identifier": "altserver-1-5",
"caption": "Update to AltServer 1.5 to use AltJIT and other exciting new features.",
"tintColor": "018084",
"imageURL": "https://user-images.githubusercontent.com/705880/166509576-744be578-6868-4b7d-b4fd-b9418c084327.png",
"url": "https://faq.altstore.io/release-notes/altserver",
"date": "2022-05-03",
"notify": true
},
{
"title": "More Apps in AltStore!",
"identifier": "trusted-sources",
"caption": "Update to AltStore 1.5 to easily download some of our favorite apps.",
"tintColor": "00CAB3",
"imageURL": "https://user-images.githubusercontent.com/705880/167026375-ddcb004f-7160-405c-b3e3-87a6795d2f43.png",
"url": "https://faq.altstore.io/release-notes/altstore",
"date": "2022-05-05",
"notify": true
},
{
"title": "New to AltStore?",
"identifier": "updated-faq",
"caption": "Check out our updated guide to learn how to sideload apps!",
"tintColor": "018084",
"url": "https://faq.altstore.io",
"date": "2022-07-28",
"notify": false
}
],
"userInfo": {
"patreonAccessToken": "uqoDoTxH8dY1ImE8tK76wxrzKk67gjyjBAcK8sD3RLU"
}
}

View File

@@ -3,18 +3,18 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>application-identifier</key> <key>application-identifier</key>
<string>XYZ0123456.com.SideStore.SideStore</string> <string>A72ZC8AJ5X.com.SideStore.AltStore</string>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.siri</key> <key>com.apple.developer.siri</key>
<true/> <true/>
<key>com.apple.developer.team-identifier</key> <key>com.apple.developer.team-identifier</key>
<string>XYZ0123456</string> <string>A72ZC8AJ5X</string>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.SideStore.SideStore</string> <string>group.com.SideStore.AltStore</string>
</array> </array>
<key>get-task-allow</key> <key>get-task-allow</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>

View File

@@ -11,7 +11,7 @@ import AltStoreCore
import EmotionalDamage import EmotionalDamage
@available(iOS 13, *) @available(iOS 13, *)
final class SceneDelegate: UIResponder, UIWindowSceneDelegate class SceneDelegate: UIResponder, UIWindowSceneDelegate
{ {
var window: UIWindow? var window: UIWindow?

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Root</string>
<key>ApplicationGroupContainerIdentifier</key>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Anisette Server</string>
<key>Key</key>
<string>customAnisetteURL</string>
<key>DefaultValue</key>
<string>http://191.101.206.188:6969</string>
<key>Titles</key>
<array>
<string>Macley (US)</string>
<string>Macley (DE)</string>
<string>DrPudding</string>
<string>jkcoxson (AltServer)</string>
<string>jkcoxson (Provision)</string>
<string>Sideloadly</string>
<string>Nick</string>
<string>Jawshoeadan</string>
<string>crystall1nedev</string>
</array>
<key>Values</key>
<array>
<string>http://us1.sternserv.tech</string>
<string>http://de1.sternserv.tech</string>
<string>https://sign.rheaa.xyz</string>
<string>http://jkcoxson.com:2095</string>
<string>http://jkcoxson.com:2052</string>
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
<string>http://45.33.29.114</string>
<string>https://anisette.jawshoeadan.me</string>
<string>https://anisette.crystall1ne.software/</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>Danger Zone</string>
<key>FooterText</key>
<string>If you disable the toggle then app will use the server you input into the &quot;Anisette URL&quot; box rather than one selected from the above selector.</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Use preferred servers</string>
<key>Key</key>
<string>textServer</string>
<key>DefaultValue</key>
<true/>
<key>FooterText</key>
<string>chicken</string>
</dict>
<dict>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>Title</key>
<string>Anisette URL</string>
<key>Key</key>
<string>textInputAnisetteURL</string>
<key>AutocapitalizationType</key>
<string>None</string>
<key>AutocorrectionType</key>
<string>No</string>
<key>KeyboardType</key>
<string>URL</string>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

View File

@@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="AboutHeader" id="xq2-Pl-zaG" customClass="AboutPatreonHeaderView" customModule="SideStore" customModuleProvider="target"> <collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="AboutHeader" id="xq2-Pl-zaG" customClass="AboutPatreonHeaderView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="390" height="682"/> <rect key="frame" x="0.0" y="0.0" width="375" height="445"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="25" translatesAutoresizingMaskIntoConstraints="NO" id="XiA-Jf-XMp"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="25" translatesAutoresizingMaskIntoConstraints="NO" id="XiA-Jf-XMp">
<rect key="frame" x="16" y="2" width="358" height="630"/> <rect key="frame" x="16" y="2" width="343" height="393"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="5Ol-zN-wYv"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="5Ol-zN-wYv">
<rect key="frame" x="0.0" y="0.0" width="358" height="426"/> <rect key="frame" x="0.0" y="0.0" width="343" height="317"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="f7H-EV-7Sx"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="f7H-EV-7Sx">
<rect key="frame" x="0.0" y="0.0" width="358" height="55"/> <rect key="frame" x="0.0" y="0.0" width="343" height="55"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="SideStore" translatesAutoresizingMaskIntoConstraints="NO" id="pn6-Ic-MJm"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="SideStore" translatesAutoresizingMaskIntoConstraints="NO" id="pn6-Ic-MJm">
<rect key="frame" x="0.0" y="0.0" width="55" height="55"/> <rect key="frame" x="0.0" y="0.0" width="55" height="55"/>
@@ -31,7 +31,7 @@
</constraints> </constraints>
</imageView> </imageView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="si2-MA-3RH"> <stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="si2-MA-3RH">
<rect key="frame" x="65" y="0.0" width="293" height="55"/> <rect key="frame" x="65" y="0.0" width="278" height="55"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="hkS-oz-wiT"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="hkS-oz-wiT">
<rect key="frame" x="0.0" y="0.0" width="83" height="55"/> <rect key="frame" x="0.0" y="0.0" width="83" height="55"/>
@@ -51,7 +51,7 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="TFB-qo-cbh"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="TFB-qo-cbh">
<rect key="frame" x="210" y="0.0" width="83" height="55"/> <rect key="frame" x="195" y="0.0" width="83" height="55"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Zpb-k3-y7l"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Zpb-k3-y7l">
<rect key="frame" x="0.0" y="0.0" width="83" height="50"/> <rect key="frame" x="0.0" y="0.0" width="83" height="50"/>
@@ -75,13 +75,11 @@
</constraints> </constraints>
</stackView> </stackView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FeG-e5-LJl"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FeG-e5-LJl">
<rect key="frame" x="0.0" y="65" width="358" height="361"/> <rect key="frame" x="0.0" y="65" width="343" height="252"/>
<color key="backgroundColor" white="1" alpha="0.13" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="1" alpha="0.13" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<string key="text">Thank you for using SideStore! <string key="text">Hello, thank you for using SideStore!
Subscribing to the patreon supports us and makes sure we can continue developing SideStore for you. If you would subscribe to the patreon that would support us and make sure we can continue developing SideStore for you.
Following us on social media allows us to give quick updates and spread the word about sideloading!
-SideTeam</string> -SideTeam</string>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -91,10 +89,10 @@ Following us on social media allows us to give quick updates and spread the word
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="13" translatesAutoresizingMaskIntoConstraints="NO" id="QS9-vO-bj8"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="13" translatesAutoresizingMaskIntoConstraints="NO" id="QS9-vO-bj8">
<rect key="frame" x="0.0" y="451" width="358" height="179"/> <rect key="frame" x="0.0" y="342" width="343" height="51"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yEi-L6-kQ8"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yEi-L6-kQ8">
<rect key="frame" x="0.0" y="0.0" width="358" height="51"/> <rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="l4o-vb-cMy"/> <constraint firstAttribute="height" constant="51" id="l4o-vb-cMy"/>
@@ -104,28 +102,6 @@ Following us on social media allows us to give quick updates and spread the word
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state> </state>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hov-Ce-LaM" userLabel="Twitter Button">
<rect key="frame" x="0.0" y="64" width="358" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="m0M-GX-KKG"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<state key="normal" title="Follow us on Twitter!">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VdY-7Q-amF" userLabel="Twitter Button">
<rect key="frame" x="0.0" y="128" width="358" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="kDo-b8-6tZ"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<state key="normal" title="Follow us on Instagram!">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
</button>
</subviews> </subviews>
</stackView> </stackView>
</subviews> </subviews>
@@ -138,21 +114,19 @@ Following us on social media allows us to give quick updates and spread the word
<constraint firstItem="XiA-Jf-XMp" firstAttribute="top" secondItem="xq2-Pl-zaG" secondAttribute="top" constant="2" id="j8p-JX-Dcz"/> <constraint firstItem="XiA-Jf-XMp" firstAttribute="top" secondItem="xq2-Pl-zaG" secondAttribute="top" constant="2" id="j8p-JX-Dcz"/>
</constraints> </constraints>
<connections> <connections>
<outlet property="instagramButton" destination="VdY-7Q-amF" id="5kj-9x-k4F"/>
<outlet property="rileyImageView" destination="pn6-Ic-MJm" id="60i-Q0-ojz"/> <outlet property="rileyImageView" destination="pn6-Ic-MJm" id="60i-Q0-ojz"/>
<outlet property="rileyLabel" destination="DTd-Yu-HXr" id="O0y-JB-gWp"/> <outlet property="rileyLabel" destination="DTd-Yu-HXr" id="O0y-JB-gWp"/>
<outlet property="shaneLabel" destination="Zpb-k3-y7l" id="aQN-6B-s5T"/> <outlet property="shaneLabel" destination="Zpb-k3-y7l" id="aQN-6B-s5T"/>
<outlet property="supportButton" destination="yEi-L6-kQ8" id="Dzo-vd-SnD"/> <outlet property="supportButton" destination="yEi-L6-kQ8" id="Dzo-vd-SnD"/>
<outlet property="textView" destination="FeG-e5-LJl" id="K0M-lF-I6u"/> <outlet property="textView" destination="FeG-e5-LJl" id="K0M-lF-I6u"/>
<outlet property="twitterButton" destination="hov-Ce-LaM" id="gib-Lt-qtY"/>
</connections> </connections>
<point key="canvasLocation" x="147.82608695652175" y="58.258928571428569"/> <point key="canvasLocation" x="138" y="138"/>
</collectionReusableView> </collectionReusableView>
</objects> </objects>
<resources> <resources>
<image name="SideStore" width="1024" height="1024"/> <image name="SideStore" width="180" height="180"/>
<namedColor name="SettingsHighlighted"> <namedColor name="SettingsHighlighted">
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.23529411764705882" green="0.0" blue="0.40392156862745099" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
</resources> </resources>
</document> </document>

View File

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

View File

@@ -1,179 +0,0 @@
//
// AnisetteServerList.swift
// SideStore
//
// Created by ny on 6/18/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import UIKit
import SwiftUI
import AltStoreCore
typealias SUIButton = SwiftUI.Button
// MARK: - AnisetteServerData
struct AnisetteServerData: Codable {
let servers: [Server]
}
// MARK: - Server
struct Server: Codable {
var name: String
var address: String
}
struct AniServer: Codable {
var name: String
var url: URL
}
class AnisetteViewModel: ObservableObject {
@Published var selected: String = ""
@Published var source: String = "https://servers.sidestore.io/servers.json"
@Published var servers: [Server] = []
func getListOfServers() {
URLSession.shared.dataTask(with: URL(string: source)!) { data, response, error in
if let error = error {
return
}
if let data = data {
do {
let servers = try Foundation.JSONDecoder().decode(AnisetteServerData.self, from: data)
DispatchQueue.main.async {
self.servers = servers.servers.map { Server(name: $0.name, address: $0.address) }
}
} catch {
}
}
}
.resume()
for server in servers {
print(server)
print(server.name.count)
print(server.name)
}
}
}
struct AnisetteServers: View {
@Environment(\.presentationMode) var presentationMode
@StateObject var viewModel: AnisetteViewModel = AnisetteViewModel()
@State var selected: String? = nil
var errorCallback: () -> ()
var body: some View {
NavigationView {
ZStack {
Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all)
.onAppear {
viewModel.getListOfServers()
}
VStack {
if #available(iOS 16.0, *) {
SwiftUI.List($viewModel.servers, id: \.address, selection: $selected) { server in
HStack {
VStack(alignment: .leading) {
Text("\(server.name.wrappedValue)")
.font(.headline)
.underline(true, color: .white)
Text("\(server.address.wrappedValue)")
.fontWeight(.thin)
}
if selected != nil {
if server.address.wrappedValue == selected {
Spacer()
Image(systemName: "checkmark")
.onAppear {
UserDefaults.standard.menuAnisetteURL = server.address.wrappedValue
print(UserDefaults.synchronize(.standard)())
print(UserDefaults.standard.menuAnisetteURL)
print(server.address.wrappedValue)
}
}
}
}
.backgroundStyle((selected == nil) ? Color(UIColor(named: "SettingsHighlighted")!) : Color(UIColor(named: "SettingsBackground")!))
.listRowSeparatorTint(.white)
.listRowBackground((selected == nil) ? Color(UIColor(named: "SettingsHighlighted")!).ignoresSafeArea(.all) : Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all))
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.listRowBackground(Color(UIColor(named: "SettingsBackground")!).ignoresSafeArea(.all))
} else {
List(selection: $selected) {
ForEach($viewModel.servers, id: \.name) { server in
VStack {
HStack {
Text("\(server.name.wrappedValue)")
.foregroundColor(.white)
.frame(alignment: .center)
Text("\(server.address.wrappedValue)")
.foregroundColor(.white)
.frame(alignment: .center)
}
}
Spacer()
}
}
.listStyle(.plain)
// Fallback on earlier versions
}
if #available(iOS 15.0, *) {
TextField("Anisette Server List", text: $viewModel.source)
.padding(.leading, 5)
.padding(.vertical, 10)
.frame(alignment: .center)
.textFieldStyle(.plain)
.border(.white, width: 1)
.onSubmit {
UserDefaults.standard.menuAnisetteList = viewModel.source
viewModel.getListOfServers()
}
SUIButton(action: {
viewModel.getListOfServers()
}, label: {
Text("Refresh Servers")
})
.padding(.bottom, 20)
SUIButton(role: .destructive, action: {
#if !DEBUG
if Keychain.shared.adiPb != nil {
Keychain.shared.adiPb = nil
}
#endif
print("Cleared adi.pb from keychain")
errorCallback()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Reset adi.pb")
// if (selected != nil) {
// Text("\(selected!.uuidString)")
// }
})
.padding(.bottom, 20)
} else {
// Fallback on earlier versions
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Anisette Servers")
.onAppear {
if UserDefaults.standard.menuAnisetteList != "" {
viewModel.source = UserDefaults.standard.menuAnisetteList
} else {
viewModel.source = "https://servers.sidestore.io/servers.json"
}
print(UserDefaults.standard.menuAnisetteURL)
print(UserDefaults.standard.menuAnisetteList)
}
}
}

View File

@@ -1,53 +0,0 @@
//
// ErrorDetailsViewController.swift
// AltStore
//
// Created by Riley Testut on 10/5/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
class ErrorDetailsViewController: UIViewController
{
var loggedError: LoggedError?
@IBOutlet private var textView: UITextView!
override func viewDidLoad()
{
super.viewDidLoad()
if let error = self.loggedError?.error
{
self.title = error.localizedErrorCode
let font = self.textView.font ?? UIFont.preferredFont(forTextStyle: .body)
let detailedDescription = error.formattedDetailedDescription(with: font)
self.textView.attributedText = detailedDescription
}
else
{
self.title = NSLocalizedString("Error Details", comment: "")
}
self.navigationController?.navigationBar.tintColor = .altPrimary
if #available(iOS 15, *), let sheetController = self.navigationController?.sheetPresentationController
{
sheetController.detents = [.medium(), .large()]
sheetController.selectedDetentIdentifier = .medium
sheetController.prefersGrabberVisible = true
}
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
self.textView.textContainerInset.left = self.view.layoutMargins.left
self.textView.textContainerInset.right = self.view.layoutMargins.right
}
}

View File

@@ -8,18 +8,8 @@
import UIKit import UIKit
@objc(ErrorLogMenuButton)
private final class ErrorLogMenuButton: UIButton {
@available(iOS 14.0, *)
override func menuAttachmentPoint(for configuration: UIContextMenuConfiguration) -> CGPoint {
var point = super.menuAttachmentPoint(for: configuration)
point.y = self.bounds.midY
return point
}
}
@objc(ErrorLogTableViewCell) @objc(ErrorLogTableViewCell)
final class ErrorLogTableViewCell: UITableViewCell class ErrorLogTableViewCell: UITableViewCell
{ {
@IBOutlet var appIconImageView: AppIconImageView! @IBOutlet var appIconImageView: AppIconImageView!

View File

@@ -14,20 +14,11 @@ import Roxas
import Nuke import Nuke
import QuickLook class ErrorLogViewController: UITableViewController
final class ErrorLogViewController: UITableViewController
{ {
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
private var expandedErrorIDs = Set<NSManagedObjectID>() private var expandedErrorIDs = Set<NSManagedObjectID>()
private var isScrolling = false {
didSet {
guard self.isScrolling != oldValue else { return }
self.updateButtonInteractivity()
}
}
private lazy var timeFormatter: DateFormatter = { private lazy var timeFormatter: DateFormatter = {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none dateFormatter.dateStyle = .none
@@ -46,15 +37,6 @@ final class ErrorLogViewController: UITableViewController
self.tableView.dataSource = self.dataSource self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource self.tableView.prefetchDataSource = self.dataSource
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
let navigationController = segue.destination as! UINavigationController
let errorDetailsViewController = navigationController.viewControllers.first as! ErrorDetailsViewController
errorDetailsViewController.loggedError = loggedError
}
} }
private extension ErrorLogViewController private extension ErrorLogViewController
@@ -76,8 +58,14 @@ private extension ErrorLogViewController
let cell = cell as! ErrorLogTableViewCell let cell = cell as! ErrorLogTableViewCell
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date) cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "") cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
cell.errorCodeLabel.text = loggedError.error.localizedErrorCode
switch loggedError.domain
{
case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code))
case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code))
default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode
}
let nsError = loggedError.error as NSError let nsError = loggedError.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
cell.errorDescriptionTextView.text = errorDescription cell.errorDescriptionTextView.text = errorDescription
@@ -103,19 +91,12 @@ private extension ErrorLogViewController
}, },
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
self?.searchFAQ(for: loggedError) self?.searchFAQ(for: loggedError)
},
UIAction(title: NSLocalizedString("View More Details", comment: ""), image: UIImage(systemName: "ellipsis.circle")) { [weak self] _ in
} }
]) ])
cell.menuButton.menu = menu cell.menuButton.menu = menu
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
cell.selectionStyle = .none
} else {
cell.menuButton.isUserInteractionEnabled = false
} }
// Include errorDescriptionTextView's text in cell summary. // Include errorDescriptionTextView's text in cell summary.
cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ") cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ")
@@ -195,15 +176,6 @@ 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)
}
@IBAction func clearLoggedErrors(_ sender: UIBarButtonItem) @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) let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet)
@@ -249,27 +221,22 @@ private extension ErrorLogViewController
func searchFAQ(for loggedError: LoggedError) func searchFAQ(for loggedError: LoggedError)
{ {
let baseURL = URL(string: "https://faq.altstore.io/getting-started/error-codes")! let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
let query = [loggedError.domain, "\(loggedError.error.displayCode)"].joined(separator: "+") let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+")
components.queryItems = [URLQueryItem(name: "q", value: query)] components.queryItems = [URLQueryItem(name: "q", value: query)]
let safariViewController = SFSafariViewController(url: components.url ?? baseURL) let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
safariViewController.preferredControlTintColor = .altPrimary safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true) self.present(safariViewController, animated: true)
} }
func viewMoreDetails(for loggedError: LoggedError) {
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
}
} }
extension ErrorLogViewController extension ErrorLogViewController
{ {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{ {
guard #unavailable(iOS 14) else { return }
let loggedError = self.dataSource.item(at: indexPath) let loggedError = self.dataSource.item(at: indexPath)
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
@@ -332,43 +299,3 @@ extension ErrorLogViewController
} }
} }
} }
extension ErrorLogViewController: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
return fileURL as QLPreviewItem
}
}
extension ErrorLogViewController
{
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
{
self.isScrolling = true
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
{
self.isScrolling = false
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
guard !decelerate else { return }
self.isScrolling = false
}
private func updateButtonInteractivity()
{
guard #available(iOS 14, *) else { return }
for case let cell as ErrorLogTableViewCell in self.tableView.visibleCells
{
cell.menuButton.showsMenuAsPrimaryAction = self.isScrolling ? false : true
}
}
}

View File

@@ -19,7 +19,7 @@ extension InsetGroupTableViewCell
} }
} }
final class InsetGroupTableViewCell: UITableViewCell class InsetGroupTableViewCell: UITableViewCell
{ {
#if !TARGET_INTERFACE_BUILDER #if !TARGET_INTERFACE_BUILDER
@IBInspectable var style: Style = .single { @IBInspectable var style: Style = .single {

View File

@@ -8,7 +8,7 @@
import UIKit import UIKit
final class LicensesViewController: UIViewController class LicensesViewController: UIViewController
{ {
private var _didAppear = false private var _didAppear = false

Some files were not shown because too many files have changed in this diff Show More