Compare commits

..

23 Commits

Author SHA1 Message Date
Spidy123222
cbce8725c7 Merge branch 'develop' into users/junepark678/altsign-fixes 2025-01-20 22:27:31 -08:00
June
8cfbbf8768 Merge remote-tracking branch 'origin/develop' into users/junepark678/altsign-fixes 2024-12-28 10:26:57 +09:00
June
b3357049b0 use new altsign 2024-12-28 10:23:48 +09:00
June
a7f01956e8 update altsign 2024-12-28 10:12:34 +09:00
June
1cc0b11aa6 Fix build errors 2024-12-28 10:02:24 +09:00
June
9a598f701a fix conflicts 2024-12-28 09:52:09 +09:00
June
af26262b6b fixate AltSign 2024-12-28 09:52:09 +09:00
Magesh K
9ece114f7a [Fix]: operate on UI controls in main thread 2024-12-28 09:52:09 +09:00
Magesh K
ed0f83d0cb [README]: updated min iOS to 15 and added info about cocoapods requirement for development 2024-12-28 09:52:09 +09:00
Magesh K
2a8e1d8888 Reapply "[cleanup]: remove left overs from last cleanup"
This reverts commit 68470b61c5.
2024-12-28 09:52:09 +09:00
Magesh K
61077fa5b1 [Pods-fix]: using "Folders" feature of xcode to organize files bumped the project objectVersion from 60 to 71(Xcode 16) which is still unsupported by latest cocoapods 2024-12-28 09:52:09 +09:00
Magesh K
7fd9639351 [xcconfigs]: Move the missed configs and fix path for pods directory 2024-12-28 09:52:09 +09:00
Magesh K
eb06d05d63 Revert "[cleanup]: remove left overs from last cleanup"
This reverts commit 6c5cf2ef06.
2024-12-28 09:52:09 +09:00
Magesh K
853de98326 [cleanup]: remove left overs from last cleanup 2024-12-28 09:52:09 +09:00
Magesh K
bb9760f31d [AltSign]: updated to use latest AltSign from SideStore/AltSign master 2024-12-28 09:52:09 +09:00
Magesh K
60411cabb4 [cleanup]: declutter by moving the xcconfig into their own directory 2024-12-28 09:52:09 +09:00
Magesh K
e07c4aac37 -[AltSign]: updated submodule to latest 2024-12-28 09:52:09 +09:00
Magesh K
a9bef300a0 [AltSign]: updated to use latest AltSign from SideStore/AltSign master 2024-12-28 09:52:09 +09:00
Magesh K
2024e0ad32 [xcconfig]: corrected the variable from DEBUG_BUNDLE_ID_SUFFIX to BUNDLE_ID_SUFFIX 2024-12-28 09:52:09 +09:00
Spidy123222
3527a4b8bc Try adding backwards compatibility in trusted
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2024-12-28 09:52:09 +09:00
June
41c7c161d6 hopefully fix build errors 2024-12-28 09:33:12 +09:00
June
c3a199c2d0 fixate AltSign 2024-12-27 13:50:17 +09:00
June
ea00f2904b Add increasedMemoryLimit by default 2024-12-27 13:49:41 +09:00
226 changed files with 13564 additions and 13366 deletions

View File

@@ -1,63 +0,0 @@
import requests
import sys
import os
# Your GitHub Personal Access Token
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
# Repository details
REPO_OWNER = "SideStore"
REPO_NAME = "SideStore"
API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/actions/caches"
# Common headers for GitHub API calls
HEADERS = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_TOKEN}"
}
def list_caches():
response = requests.get(API_URL, headers=HEADERS)
if response.status_code != 200:
print(f"Failed to list caches. HTTP {response.status_code}")
print("Response:", response.text)
sys.exit(1)
data = response.json()
return data.get("actions_caches", [])
def delete_cache(cache_id):
delete_url = f"{API_URL}/{cache_id}"
response = requests.delete(delete_url, headers=HEADERS)
return response.status_code
def main():
caches = list_caches()
if not caches:
print("No caches found.")
return
print("Found caches:")
for cache in caches:
print(f"ID: {cache.get('id')}, Key: {cache.get('key')}")
print("\nDeleting caches...")
for cache in caches:
cache_id = cache.get("id")
status = delete_cache(cache_id)
if status == 204:
print(f"Successfully deleted cache with ID: {cache_id}")
else:
print(f"Failed to delete cache with ID: {cache_id}. HTTP status code: {status}")
print("All caches processed.")
if __name__ == "__main__":
main()
### How to use
'''
just export the GITHUB_TOKEN and then run this script via `python3 cache.py' to delete the caches
'''

View File

@@ -2,27 +2,283 @@ name: Alpha SideStore build
on:
push:
branches:
- develop-alpha
# cancel duplicate run if from same branch
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
# - alpha
- rebase-2.0-wip
jobs:
Reusable-build:
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Alpha"
bundle_id: "com.SideStore.SideStore"
# bundle_id_suffix: ".Alpha"
is_beta: true
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
is_shared_build_num: false
release_tag: "alpha"
release_name: "Alpha"
upstream_tag: "nightly"
upstream_name: "Nightly"
secrets:
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
build:
name: Build and upload SideStore Alpha releases
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-14'
version: '16.1'
runs-on: ${{ matrix.os }}
steps:
- name: Set current build as ALPHA
run: echo "IS_ALPHA=1" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Install xcbeautify
run: brew install xcbeautify
- name: Cache .alpha-build-num
uses: actions/cache@v4
with:
path: .alpha-build-num
key: alpha-build-num
- name: Get version
id: version-marketing
run: echo "VERSION_IPA=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_ENV
- name: Increase alpha build number and set as version
run: bash .github/workflows/increase-alpha-build-num.sh
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: Restore Pods from Cache (Exact match)
id: pods-restore
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-${{ hashFiles('Podfile') }}
# restore-keys: | # commented out to strictly check cache for this particular podfile
# pods-cache-
- name: Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
id: pods-restore-recent
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-
- name: Install CocoaPods
# if: ${{ steps.pods-restore.outputs.cache-hit != 'true'}}
id: pods-install
run: |
pod install
- name: Save Pods to Cache
id: save-pods
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-${{ hashFiles('Podfile') }}
- name: List Files and derived data
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Pods <<<<<<<<<<"
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Create dSYMs zip
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs/*
- name: Upload to alpha release
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: "Alpha"
tag: "alpha"
prerelease: true
files: SideStore.ipa SideStore.dSYMs.zip
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ alpha build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
Alpha 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!**
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore Stable](https://github.com/${{ github.repository }}/releases?q=stable).
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
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@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./SideStore.xcarchive/dSYMs/*
# Check if PUBLISH_ALPHA_UPDATES secret is set to non-zero
- name: Check if PUBLISH_ALPHA_UPDATES is set
id: check_publish
run: |
if [[ "${{ secrets.PUBLISH_ALPHA_UPDATES }}" != "__YES__" ]]; then
echo "PUBLISH_ALPHA_UPDATES is not set. Skipping deployment."
exit 1 # Exit with 1 to indicate no deployment
else
echo "PUBLISH_ALPHA_UPDATES is set. Proceeding with deployment."
exit 0 # Exit with 0 to indicate deployment should proceed
fi
continue-on-error: true # Continue even if exit code is 1
- name: Get short commit hash
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
# SHORT_COMMIT="${{ github.sha }}"
SHORT_COMMIT=${GITHUB_SHA:0:7}
echo "Short commit hash: $SHORT_COMMIT"
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_ENV
- name: Get formatted date
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "Formatted date: $FORMATTED_DATE"
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
- name: Get size of IPA in bytes (macOS/Linux)
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
if [[ "$(uname)" == "Darwin" ]]; then
# macOS
IPA_SIZE=$(stat -f %z SideStore-${{ steps.version.outputs.version }}.ipa)
else
# Linux
IPA_SIZE=$(stat -c %s SideStore-${{ steps.version.outputs.version }}.ipa)
fi
echo "IPA size in bytes: $IPA_SIZE"
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
- name: Compute SHA-256 of IPA
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
SHA256_HASH=$(shasum -a 256 SideStore-${{ steps.version.outputs.version }}.ipa | awk '{ print $1 }')
echo "SHA-256 Hash: $SHA256_HASH"
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
- name: Set environment variables dynamically
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
echo "BETA=true" >> $GITHUB_ENV
echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
echo "LOCALIZED_DESCRIPTION=This is alpha release for revision: ${{ github.sha }}" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/alpha/SideStore.ipa" >> $GITHUB_ENV
- name: Checkout SideStore/apps-v2.json
if: ${{ steps.check_publish.outcome == 'success' }}
uses: actions/checkout@v4
with:
# Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }}
repository: 'SideStore/apps-v2.json'
ref: 'main'
# token: ${{ github.token }}
token: ${{ secrets.APPS_DEPLOY_KEY }}
path: 'SideStore/apps-v2.json'
- name: Publish to SideStore/apps-v2.json
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
# Copy and execute the update script
pushd SideStore/apps-v2.json/
# Configure Git user (committer details)
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
# Make the update script executable and run it
python3 ../../update_apps.py "./_includes/source.json"
# Commit changes and push using SSH
git add ./_includes/source.json
git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
git status
git push origin HEAD:main
popd

View File

@@ -0,0 +1,28 @@
#!/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/$/-alpha.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > .alpha-build-num
}
if [ ! -f ".alpha-build-num" ]; then
write
exit 0
fi
LAST_DATE=`cat .alpha-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
LAST_BUILD_NUM=`cat .alpha-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

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
# Ensure we are in root directory
cd "$(dirname "$0")/../.."
DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1
# Use RELEASE_CHANNEL from the environment variable or default to "beta"
RELEASE_CHANNEL=${RELEASE_CHANNEL:-"beta"}
write() {
sed -e "/MARKETING_VERSION = .*/s/$/-$RELEASE_CHANNEL.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > build_number.txt
}
if [ ! -f "build_number.txt" ]; then
write
exit 0
fi
LAST_DATE=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
LAST_BUILD_NUM=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
# if [[ "$DATE" != "$LAST_DATE" ]]; then
# write
# else
# BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
# write
# fi
# Build number is always incremental
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
write

View File

@@ -0,0 +1,28 @@
#!/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

@@ -1,82 +1,317 @@
name: Nightly SideStore Build
name: Nightly SideStore build
on:
push:
branches:
- develop
schedule:
- cron: '0 0 * * *' # Runs every night at midnight UTC
workflow_dispatch: # Allows manual trigger
# cancel duplicate run if from same branch
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
check-changes:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
build:
name: Build and upload SideStore Nightly releases
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-14'
version: '16.1'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
- name: Set current build as BETA
run: |
echo "IS_BETA=1" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=beta" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Ensure full history
submodules: recursive
- name: Get last successful workflow run
id: get_last_success
run: |
LAST_SUCCESS=$(gh run list --workflow "Nightly SideStore Build" --json createdAt,conclusion \
--jq '[.[] | select(.conclusion=="success")][0].createdAt' || echo "")
echo "Last successful run: $LAST_SUCCESS"
echo "last_success=$LAST_SUCCESS" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: brew install ldid
- name: Check for new commits since last successful build
id: check
- name: Install xcbeautify
run: brew install xcbeautify
- name: Cache .nightly-build-num
uses: actions/cache@v4
with:
path: .nightly-build-num
key: nightly-build-num
- name: Get version
id: version-marketing
run: echo "VERSION_IPA=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_ENV
- name: Increase nightly build number and set as version
run: bash .github/workflows/increase-nightly-build-num.sh
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: Restore Pods from Cache (Exact match)
id: pods-restore
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-${{ hashFiles('Podfile') }}
# restore-keys: | # commented out to strictly check cache for this particular podfile
# pods-cache-
- name: Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
id: pods-restore-recent
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-
- name: Install CocoaPods
# if: ${{ steps.pods-restore.outputs.cache-hit != 'true'}}
id: pods-install
run: |
if [ -n "$LAST_SUCCESS" ]; then
NEW_COMMITS=$(git rev-list --count --since="$LAST_SUCCESS" origin/develop)
COMMIT_LOG=$(git log --since="$LAST_SUCCESS" --pretty=format:"%h %s" origin/develop)
else
NEW_COMMITS=1
COMMIT_LOG=$(git log -n 10 --pretty=format:"%h %s" origin/develop) # Show last 10 commits if no history
pod install
- name: Save Pods to Cache
id: save-pods
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-${{ hashFiles('Podfile') }}
- name: List Files and derived data
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Pods <<<<<<<<<<"
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make build 2>&1 | tee build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign | tee -a build.log
- name: Convert to IPA
run: make ipa | tee -a build.log
- name: Encrypt build.log generated from SideStore build for upload
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
if [ ! -f build.log ]; then
echo "Warning: build.log is missing, creating a dummy log..."
echo "Error: build.log was missing, This is a dummy placeholder file..." > build.log
fi
echo "Has changes: $NEW_COMMITS"
echo "New commits since last successful build:"
echo "$COMMIT_LOG"
if [ "$NEW_COMMITS" -gt 0 ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
zip -e -P "$BUILD_LOG_ZIP_PASSWORD" encrypted-build_log.zip build.log
- name: List Files after SideStore build
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Create dSYMs zip
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs/*
- name: Upload to nightly release
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: "Nightly"
tag: "nightly"
prerelease: true
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build_log.zip
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
Nightly builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. 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 Stable](https://github.com/${{ github.repository }}/releases?q=stable).
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
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@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./SideStore.xcarchive/dSYMs/*
- name: Upload encrypted-build_log.zip
uses: actions/upload-artifact@v4
with:
name: encrypted-build_log.zip
path: encrypted-build_log.zip
# Check if PUBLISH_BETA_UPDATES secret is set to non-zero
- name: Check if PUBLISH_BETA_UPDATES is set
id: check_publish
run: |
if [[ "${{ secrets.PUBLISH_BETA_UPDATES }}" != "__YES__" ]]; then
echo "PUBLISH_BETA_UPDATES is not set. Skipping deployment."
exit 1 # Exit with 1 to indicate no deployment
else
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "PUBLISH_BETA_UPDATES is set. Proceeding with deployment."
exit 0 # Exit with 0 to indicate deployment should proceed
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LAST_SUCCESS: ${{ env.last_success }}
continue-on-error: true # Continue even if exit code is 1
Reusable-build:
if: |
always() &&
(github.event_name == 'push' ||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
needs: check-changes
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Nightly"
bundle_id: "com.SideStore.SideStore"
# bundle_id_suffix: ".Nightly"
is_beta: true
publish: ${{ vars.PUBLISH_NIGHTLY_UPDATES == 'true' }}
is_shared_build_num: false
release_tag: "nightly"
release_name: "Nightly"
upstream_tag: "0.5.10"
upstream_name: "Stable"
secrets:
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
- name: Get short commit hash
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
# SHORT_COMMIT="${{ github.sha }}"
SHORT_COMMIT=${GITHUB_SHA:0:7}
echo "Short commit hash: $SHORT_COMMIT"
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_ENV
- name: Get formatted date
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "Formatted date: $FORMATTED_DATE"
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
- name: Get size of IPA in bytes (macOS/Linux)
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
if [[ "$(uname)" == "Darwin" ]]; then
# macOS
IPA_SIZE=$(stat -f %z SideStore-${{ steps.version.outputs.version }}.ipa)
else
# Linux
IPA_SIZE=$(stat -c %s SideStore-${{ steps.version.outputs.version }}.ipa)
fi
echo "IPA size in bytes: $IPA_SIZE"
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
- name: Compute SHA-256 of IPA
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
SHA256_HASH=$(shasum -a 256 SideStore-${{ steps.version.outputs.version }}.ipa | awk '{ print $1 }')
echo "SHA-256 Hash: $SHA256_HASH"
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
- name: Set environment variables dynamically
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
echo "LOCALIZED_DESCRIPTION=This is nightly release for revision: ${{ github.sha }}" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/nightly/SideStore.ipa" >> $GITHUB_ENV
- name: Checkout SideStore/apps-v2.json
if: ${{ steps.check_publish.outcome == 'success' }}
uses: actions/checkout@v4
with:
# Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }}
repository: 'SideStore/apps-v2.json'
ref: 'main' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
# ref: 'nightly' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
# token: ${{ github.token }}
token: ${{ secrets.APPS_DEPLOY_KEY }}
path: 'SideStore/apps-v2.json'
- name: Publish to SideStore/apps-v2.json
if: ${{ steps.check_publish.outcome == 'success' }}
run: |
# Copy and execute the update script
pushd SideStore/apps-v2.json/
# Configure Git user (committer details)
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
# update the source.json
python3 ../../update_apps.py "./_includes/source.json"
# Commit changes and push using SSH
git add ./_includes/source.json
git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
git status
git push origin HEAD:main
popd

View File

@@ -1,13 +1,10 @@
name: Pull Request SideStore build
on:
pull_request:
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
types: [opened, synchronize, reopened, ready_for_review]
jobs:
build:
name: Build and upload SideStore
if: ${{ github.event.pull_request.draft == false }}
strategy:
fail-fast: false
matrix:
@@ -54,12 +51,57 @@ jobs:
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: Restore Pods from Cache (Exact match)
id: pods-restore
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-${{ hashFiles('Podfile') }}
# restore-keys: | # commented out to strictly check cache for this particular podfile
# pods-cache-
- name: Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
id: pods-restore-recent
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-
- name: Install CocoaPods
# if: ${{ steps.pods-restore.outputs.cache-hit != 'true'}}
id: pods-install
run: |
pod install
- name: Save Pods to Cache
id: save-pods
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-${{ hashFiles('Podfile') }}
- name: List Files and derived data
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Pods <<<<<<<<<<"
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""

View File

@@ -1,105 +0,0 @@
name: Reusable SideStore Build
on:
workflow_call:
inputs:
is_beta:
required: false
default: false
type: boolean
publish:
required: false
default: false
type: boolean
is_shared_build_num:
required: false
default: true
type: boolean
release_name:
required: true
type: string
release_tag:
required: true
type: string
upstream_tag:
required: true
type: string
upstream_name:
required: true
type: string
bundle_id:
default: com.SideStore.SideStore
required: true
type: string
bundle_id_suffix:
default: ''
required: false
type: string
secrets:
# GITHUB_TOKEN:
# required: true
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
# so we serialize on the entire workflow
concurrency:
group: serialize-workflow
jobs:
shared:
uses: ./.github/workflows/sidestore-shared.yml
secrets: inherit
build:
needs: shared
uses: ./.github/workflows/sidestore-build.yml
with:
is_beta: ${{ inputs.is_beta }}
is_shared_build_num: ${{ inputs.is_shared_build_num }}
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
bundle_id: ${{ inputs.bundle_id }}
bundle_id_suffix: ${{ inputs.bundle_id_suffix }}
secrets: inherit
# tests-build:
# if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
# needs: shared
# uses: ./.github/workflows/sidestore-tests-build.yml
# with:
# release_tag: ${{ inputs.release_tag }}
# short_commit: ${{ needs.shared.outputs.short-commit }}
# secrets: inherit
# tests-run:
# if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
# needs: [shared, tests-build]
# uses: ./.github/workflows/sidestore-tests-run.yml
# with:
# release_tag: ${{ inputs.release_tag }}
# short_commit: ${{ needs.shared.outputs.short-commit }}
# secrets: inherit
deploy:
# needs: [shared, build, tests-build, tests-run] # Keep tests-run in needs
needs: [shared, build] # Keep tests-run in needs
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
uses: ./.github/workflows/sidestore-deploy.yml
with:
is_beta: ${{ inputs.is_beta }}
publish: ${{ inputs.publish }}
release_name: ${{ inputs.release_name }}
release_tag: ${{ inputs.release_tag }}
upstream_tag: ${{ inputs.upstream_tag }}
upstream_name: ${{ inputs.upstream_name }}
version: ${{ needs.build.outputs.version }}
short_commit: ${{ needs.shared.outputs.short-commit }}
release_channel: ${{ needs.build.outputs.release-channel }}
marketing_version: ${{ needs.build.outputs.marketing-version }}
bundle_id: ${{ inputs.bundle_id }}
secrets: inherit

View File

@@ -1,358 +0,0 @@
name: SideStore Build
on:
workflow_call:
inputs:
is_beta:
type: boolean
is_shared_build_num:
type: boolean
release_tag:
type: string
bundle_id:
type: string
bundle_id_suffix:
type: string
short_commit:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
outputs:
version:
value: ${{ jobs.build.outputs.version }}
marketing-version:
value: ${{ jobs.build.outputs.marketing-version }}
release-channel:
value: ${{ jobs.build.outputs.release-channel }}
jobs:
build:
name: Build SideStore - ${{ inputs.release_tag }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
outputs:
version: ${{ steps.version.outputs.version }}
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
steps:
- name: Set beta status
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
shell: bash
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Set ref based on is_shared_build_num
if: ${{ inputs.is_beta }}
id: set_ref
run: |
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
echo "ref=main" >> $GITHUB_ENV
else
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
fi
shell: bash
- name: Checkout SideStore/beta-build-num repo
if: ${{ inputs.is_beta }}
uses: actions/checkout@v4
with:
repository: 'SideStore/beta-build-num'
ref: ${{ env.ref }}
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/beta-build-num'
- name: Copy build_number.txt to repo root
if: ${{ inputs.is_beta }}
run: |
cp SideStore/beta-build-num/build_number.txt .
echo "cat build_number.txt"
cat build_number.txt
shell: bash
- name: Echo Build.xcconfig
run: |
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
- name: Set Release Channel info for build number bumper
id: release-channel
run: |
RELEASE_CHANNEL="${{ inputs.release_tag }}"
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
shell: bash
- name: Increase build number for beta builds
if: ${{ inputs.is_beta }}
run: |
bash .github/workflows/increase-beta-build-num.sh
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
id: version
run: |
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
shell: bash
- name: Set MARKETING_VERSION
if: ${{ inputs.is_beta }}
id: marketing-version
run: |
# Extract version number (e.g., "0.6.0")
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
# Extract date (YYYYMMDD) (e.g., "20250205")
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
# Extract build number (e.g., "2")
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
# Combine them into the final output
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$MARKETING_VERSION"
shell: bash
- name: Echo Updated Build.xcconfig, build_number.txt
if: ${{ inputs.is_beta }}
run: |
cat Build.xcconfig
cat build_number.txt
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-
# - name: (Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
- name: (Build) Clean previous build artifacts
# using 'tee' to intercept stdout and log for detailed build-log
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Build) List Files and derived data
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Set BundleID Suffix for Sidestore build
run: |
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
shell: bash
- name: Build SideStore.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Fakesign app
run: make fakesign | tee -a build/logs/build.log
shell: bash
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
shell: bash
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt build-logs for upload
id: encrypt-build-log
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-build-logs.zip
id: attach-encrypted-build-log
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
uses: actions/upload-artifact@v4
with:
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
path: encrypted-build-logs.zip
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa
- name: Zip dSYMs
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Keep rolling the build numbers for each successful build
if: ${{ inputs.is_beta }}
run: |
pushd SideStore/beta-build-num/
echo "Configure Git user (committer details)"
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
echo "Adding files to commit"
git add --verbose build_number.txt
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
echo "Pushing to remote repo"
git push --verbose
popd
shell: bash
- name: Get last successful commit
id: get_last_commit
run: |
# Try to get the last successful workflow run commit
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
- name: Create release notes
run: |
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
FROM_COMMIT=$LAST_SUCCESS_SHA
# Check if we got a valid SHA
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
echo "No successful run found, using initial commit of branch"
# Get the first commit of the branch (initial commit)
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
fi
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
# cat release-notes.md
shell: bash
- name: Upload release-notes.md
uses: actions/upload-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
path: release-notes.md
- name: Upload update_release_notes.py
uses: actions/upload-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
path: update_release_notes.py
- name: Upload update_apps.py
uses: actions/upload-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
path: update_apps.py

View File

@@ -1,281 +0,0 @@
name: SideStore Deploy
on:
workflow_call:
inputs:
is_beta:
type: boolean
publish:
type: boolean
release_name:
type: string
release_tag:
type: string
upstream_tag:
type: string
upstream_name:
type: string
version:
type: string
short_commit:
type: string
marketing_version:
type: string
release_channel:
type: string
bundle_id:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
# GITHUB_TOKEN:
# required: true
jobs:
deploy:
name: Deploy SideStore - ${{ inputs.release_tag }}
runs-on: macos-15
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}.ipa
- name: Download dSYM artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}-dSYMs.zip
- name: Download encrypted-build-logs artifact
uses: actions/download-artifact@v4
with:
name: encrypted-build-logs-${{ inputs.version }}.zip
- name: Download encrypted-tests-build-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
- name: Download encrypted-tests-run-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
- name: Download tests-recording artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: tests-recording-${{ inputs.short_commit }}.mp4
- name: Download test-results artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
- name: Download release-notes.md
uses: actions/download-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
- name: Download update_release_notes.py
uses: actions/download-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
- name: Download update_apps.py
uses: actions/download-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
- name: Read release notes
id: release_notes
run: |
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
- name: List files before upload
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
echo ""
shell: bash
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: List files to upload
id: list_uploads
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
echo ""
FILES="SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip"
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_BUILD }}" == "1" ]]; then
FILES="$FILES encrypted-tests-build-logs.zip"
fi
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_RUN }}" == "1" ]]; then
FILES="$FILES encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4"
fi
echo "Final upload list:"
for f in $FILES; do
if [[ -f "$f" ]]; then
echo " ✓ $f"
else
echo " - $f (missing)"
fi
done
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: Set Upstream Recommendation
id: upstream_recommendation
run: |
UPSTREAM_NAME=$(echo "${{ inputs.upstream_name }}" | tr '[:upper:]' '[:lower:]')
if [[ "$UPSTREAM_NAME" != "nightly" ]]; then
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }})." >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "content=" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: ${{ inputs.release_name }}
tag: ${{ inputs.release_tag }}
prerelease: ${{ inputs.is_beta }}
files: ${{ steps.list_uploads.outputs.files }}
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
${{ steps.upstream_recommendation.outputs.content }}
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ inputs.version }}`
${{ steps.release_notes.outputs.content }}
- name: Get formatted date
run: |
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "Formatted date: $FORMATTED_DATE"
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
shell: bash
- name: Get size of IPA in bytes (macOS/Linux)
run: |
if [[ "$(uname)" == "Darwin" ]]; then
# macOS
IPA_SIZE=$(stat -f %z SideStore.ipa)
else
# Linux
IPA_SIZE=$(stat -c %s SideStore.ipa)
fi
echo "IPA size in bytes: $IPA_SIZE"
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
shell: bash
- name: Compute SHA-256 of IPA
run: |
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
echo "SHA-256 Hash: $SHA256_HASH"
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
shell: bash
- name: Set Release Info variables
run: |
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
# Format localized description
get_description() {
cat <<EOF
This is release for:
- version: "${{ inputs.version }}"
- revision: "${{ inputs.short_commit }}"
- timestamp: "${{ steps.date.outputs.date }}"
Release Notes:
${{ steps.release_notes.outputs.content }}
EOF
}
LOCALIZED_DESCRIPTION=$(get_description)
echo "$LOCALIZED_DESCRIPTION"
# multiline strings
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
shell: bash
- name: Check if Publish updates is set
id: check_publish
run: |
echo "Publish updates to source.json = ${{ inputs.publish }}"
shell: bash
- name: Checkout SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
uses: actions/checkout@v4
with:
repository: 'SideStore/apps-v2.json'
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/apps-v2.json'
# for stable builds, let the user manually edit the source.json
- name: Publish to SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
id: publish-release
shell: bash
run: |
# Copy and execute the update script
pushd SideStore/apps-v2.json/
# Configure Git user (committer details)
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
# update the source.json
python3 ../../update_apps.py "./_includes/source.json"
# Commit changes and push using SSH
git add --verbose ./_includes/source.json
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
git push --verbose
popd

View File

@@ -1,24 +0,0 @@
name: SideStore Shared
on:
workflow_call:
outputs:
short-commit:
value: ${{ jobs.shared.outputs.short-commit }}
jobs:
shared:
name: Shared Steps
strategy:
fail-fast: false
runs-on: 'macos-15'
steps:
- name: Set short commit hash
id: commit-id
run: |
# SHORT_COMMIT="${{ github.sha }}"
SHORT_COMMIT=${GITHUB_SHA:0:7}
echo "Short commit hash: $SHORT_COMMIT"
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_OUTPUT
outputs:
short-commit: ${{ steps.commit-id.outputs.SHORT_COMMIT }}

View File

@@ -1,165 +0,0 @@
name: SideStore Tests Build
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-build:
name: Tests-Build SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies - xcbeautify
run: |
brew install xcbeautify
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '26.0'
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# # tests shouldn't restore cache unless it is same build
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
# delete-used-deriveddata-cache: true
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-
- name: Clean Derived Data (if required)
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
make clean
xcodebuild clean
shell: bash
- name: (Tests-Build) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Build) List Files and derived data
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore Tests
# using 'tee' to intercept stdout and log for detailed build-log
shell: bash
run: |
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: (Tests-Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) List Files and Build artifacts
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
path: tests-build-deriveddata.txt
- name: Encrypt tests-build-logs for upload
id: encrypt-test-log
if: always()
shell: bash
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
- name: Upload encrypted-tests-build-logs.zip
id: attach-encrypted-test-log
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
path: encrypted-tests-build-logs.zip

View File

@@ -1,196 +0,0 @@
name: SideStore Tests Run
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-run:
name: Tests-Run SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Boot Simulator async(nohup) for testing
run: |
mkdir -p build/logs
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '26.0'
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# # This comes from
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Run) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Run) List Files and derived data
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
path: tests-run-deriveddata.txt
# we expect simulator to have been booted by now, so exit otherwise
- name: Simulator Boot Check
run: |
mkdir -p build/logs
make -B sim-boot-check | tee -a build/logs/tests-run.log
exit ${PIPESTATUS[0]}
shell: bash
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
run: |
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
RECORD_PID=$!
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
shell: bash
- name: Run SideStore Tests
# using 'tee' to intercept stdout and log for detailed build-log
run: |
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
shell: bash
- name: Stop Recording tests
if: ${{ always() && env.RECORD_PID != '' }}
run: |
kill -INT ${{ env.RECORD_PID }}
shell: bash
- name: (Tests-Run) List Files and Build artifacts
if: always()
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt tests-run-logs for upload
id: encrypt-test-log
if: always()
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-tests-run-logs.zip
id: attach-encrypted-test-log
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
path: encrypted-tests-run-logs.zip
- name: Print tests-recording.log contents (if exists)
if: ${{ always() && env.RECORD_PID != '' }}
run: |
if [ -f tests-recording.log ]; then
echo "tests-recording.log found. Its contents:"
cat tests-recording.log
else
echo "tests-recording.log not found."
fi
shell: bash
- name: Check for tests-recording.mp4 presence
id: check-recording
if: ${{ always() && env.RECORD_PID != '' }}
run: |
if [ -f tests-recording.mp4 ]; then
echo "::set-output name=found::true"
echo "tests-recording.mp4 found."
else
echo "tests-recording.mp4 not found, skipping upload."
echo "::set-output name=found::false"
fi
shell: bash
- name: Upload tests-recording.mp4
id: upload-recording
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
uses: actions/upload-artifact@v4
with:
name: tests-recording-${{ inputs.short_commit }}.mp4
path: tests-recording.mp4
- name: Zip test-results
run: zip -r -9 ./test-results.zip ./build/tests
shell: bash
- name: Upload Test Artifacts
uses: actions/upload-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
path: test-results.zip

View File

@@ -7,236 +7,97 @@ on:
jobs:
build:
name: Build SideStore - stable (on tag push)
name: Build and upload SideStore
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
- os: 'macos-14'
version: '15.4'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Echo Build.xcconfig
run: |
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
- name: Install dependencies
run: brew install ldid
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Echo Updated Build.xcconfig
run: |
cat Build.xcconfig
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
- name: Get version
id: version
run: |
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$version"
shell: bash
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
run: |
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
echo 'Version mismatch: $tag != $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
exit 1
fi
echo 'Version matches: $tag == $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
shell: bash
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-
- name: (Build) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Build) List Files and derived data
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Build SideStore
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign | tee -a build/logs/build.log
shell: bash
run: make fakesign
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
shell: bash
run: make ipa
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new stable release
uses: softprops/action-gh-release@v1
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: true
files: SideStore.ipa
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
- TODO
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt build-logs for upload
id: encrypt-build-log
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-build-logs.zip
id: attach-encrypted-build-log
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
uses: actions/upload-artifact@v4
with:
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
path: encrypted-build-logs.zip
- 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@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa
- name: Zip dSYMs
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
release: ${{ github.ref_name }} # name
tag: ${{ github.ref_name }}
# stick with what the user pushed, do not use latest commit or anything,
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
# in this case we do not want the tag to be auto-updated to latest
updateTag: false
prerelease: false
files: >
SideStore.ipa
SideStore.dSYMs.zip
encrypted-build-logs.zip
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
- TODO
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

8
.gitignore vendored
View File

@@ -63,10 +63,4 @@ SideStore/.skip-prebuilt-fetch-em_proxy
# Never check-in this package.resolved file
# coz SPM then resolves packages using the stale entries in this file
*.xcodeproj/**/Package.resolved
*.xcworkspace/**/Package.resolved
# some more commandline build artifacts
test-recording.mp4
test-recording.log
altstore-sources.md
local-build.sh
*.xcworkspace/**/Package.resolved

6
.gitmodules vendored
View File

@@ -30,7 +30,7 @@
url = https://github.com/rileytestut/Roxas.git
[submodule "Dependencies/libimobiledevice"]
path = Dependencies/libimobiledevice
url = https://github.com/SideStore/libimobiledevice
url = https://github.com/libimobiledevice/libimobiledevice
[submodule "Dependencies/libusbmuxd"]
path = Dependencies/libusbmuxd
url = https://github.com/libimobiledevice/libusbmuxd.git
@@ -51,8 +51,8 @@
url = https://github.com/SideStore/minimuxer
branch = master
[submodule "SideStore/em_proxy"]
path = SideStore/em_proxy
url = https://github.com/SideStore/em_proxy
path = SideStore/em_proxy
url = https://github.com/SideStore/em_proxy
branch = master
[submodule "SideStore/libfragmentzip"]
path = SideStore/libfragmentzip

View File

@@ -4,7 +4,7 @@
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(GROUP_ID)</string>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
</array>
</dict>
</plist>

View File

@@ -35,6 +35,8 @@
</array>
</dict>
</array>
<key>BuildRevision</key>
<string>$(BUILD_REVISION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>

File diff suppressed because it is too large Load Diff

View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:SideStore/Tests/DataStructureTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A81A8CC42D68BA610086C96F"
BuildableName = "DataStructureTests.xctest"
BlueprintName = "DataStructureTests"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -28,27 +28,18 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:SideStore/Tests/SideStoreTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<!-- shouldAutocreateTestPlan = "YES"> -->
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A8E2DB202D684CBD009E5D31"
BuildableName = "UITests.xctest"
BlueprintName = "UITests"
BlueprintIdentifier = "D586D39728EF58B0000E101F"
BuildableName = "AltTests.xctest"
BlueprintName = "AltTests"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
<SelectedTests>
<Test
Identifier = "UITests/testExample()">
</Test>
</SelectedTests>
</TestableReference>
</Testables>
</TestAction>

View File

@@ -10,4 +10,7 @@
<FileRef
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -2,14 +2,24 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<!-- <key>com.apple.security.files.user-selected.read-write</key>
<array>
<string></string>
</array>
<key>com.apple.developer.applesignin</key>
<array>
<string></string>
</array> -->
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
<true/>
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
<true/>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,115 @@
//
// AnalyticsManager.swift
// AltStore
//
// Created by Riley Testut on 3/31/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AppCenter
import AppCenterAnalytics
import AppCenterCrashes
#if DEBUG
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
#elseif RELEASE
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
#else
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
#endif
extension AnalyticsManager
{
enum EventProperty: String
{
case name
case bundleIdentifier
case developerName
case version
case buildVersion
case size
case tintColor
case sourceIdentifier
case sourceURL
case patreonURL
case pledgeAmount
case pledgeCurrency
}
enum Event
{
case installedApp(InstalledApp)
case updatedApp(InstalledApp)
case refreshedApp(InstalledApp)
var name: String {
switch self
{
case .installedApp: return "installed_app"
case .updatedApp: return "updated_app"
case .refreshedApp: return "refreshed_app"
}
}
var properties: [EventProperty: String] {
let properties: [EventProperty: String?]
switch self
{
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
let appBundleURL = InstalledApp.fileURL(for: app)
let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
properties = [
.name: app.name,
.bundleIdentifier: app.bundleIdentifier,
.developerName: app.storeApp?.developerName,
.version: app.version,
.buildVersion: app.buildVersion,
.size: appBundleSize?.description,
.tintColor: app.storeApp?.tintColor?.hexString,
.sourceIdentifier: app.storeApp?.sourceIdentifier,
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString,
.patreonURL: app.storeApp?.source?.patreonURL?.absoluteString,
.pledgeAmount: app.storeApp?.pledgeAmount?.description,
.pledgeCurrency: app.storeApp?.pledgeCurrency
]
}
return properties.compactMapValues { $0 }
}
}
}
final class AnalyticsManager
{
static let shared = AnalyticsManager()
private init()
{
}
}
extension AnalyticsManager
{
func start()
{
AppCenter.start(withAppSecret: appCenterAppSecret, services: [
Analytics.self,
Crashes.self
])
}
func trackEvent(_ event: Event)
{
let properties = event.properties.reduce(into: [:]) { (properties, item) in
properties[item.key.rawValue] = item.value
}
Analytics.trackEvent(event.name, withProperties: properties)
}
}

View File

@@ -43,10 +43,8 @@ final class AppContentViewController: UITableViewController
}()
@IBOutlet private var subtitleLabel: UILabel!
// @IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
@IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
@IBOutlet private var versionLabel: UILabel!
@IBOutlet private var versionDateLabel: UILabel!
@IBOutlet private var sizeLabel: UILabel!
@@ -57,32 +55,35 @@ final class AppContentViewController: UITableViewController
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.contentInset.bottom = 20
self.subtitleLabel.text = self.app.subtitle
let desc = self.app.localizedDescription
self.descriptionTextView.text = desc
self.descriptionTextView.text = self.app.localizedDescription
if let version = self.app.latestAvailableVersion {
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
if let version = self.app.latestAvailableVersion
{
self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
} else {
self.versionDescriptionTextView.text = "nil"
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
}
else
{
self.versionDescriptionTextView.text = nil
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
}
self.descriptionTextView.maximumNumberOfLines = 5
self.versionDescriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.maximumNumberOfLines = 3
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
}
override func viewDidLayoutSubviews()
@@ -161,12 +162,8 @@ private extension AppContentViewController
switch sender
{
case self.descriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
default: return
}
@@ -189,7 +186,7 @@ extension AppContentViewController
switch Row.allCases[indexPath.row]
{
case .screenshots:
guard !self.app.allScreenshots.isEmpty else { return 0.0 }
guard !self.app.screenshots.isEmpty else { return 0.0 }
return UITableView.automaticDimension
case .permissions:

View File

@@ -67,11 +67,6 @@ final class AppViewController: UIViewController
self.navigationBarTitleView.sizeToFit()
self.navigationItem.titleView = self.navigationBarTitleView
// spacing in storyboard wasn't working, so had to do programatically
if let stackView = self.navigationBarTitleView as? UIStackView {
stackView.spacing = 8
}
self.contentViewControllerShadowView = UIView()
self.contentViewControllerShadowView.backgroundColor = .white
self.contentViewControllerShadowView.layer.cornerRadius = 38
@@ -392,8 +387,7 @@ private extension AppViewController
{
var buttonAction: AppBannerView.AppAction?
// if let installedApp = self.app.installedApp, let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
if let installedApp = self.app.installedApp, installedApp.hasUpdate
if let installedApp = self.app.installedApp, let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
{
// Explicitly set button action to .update if there is an update available, even if it's not supported.
buttonAction = .update
@@ -543,8 +537,7 @@ extension AppViewController
{
if let installedApp = self.app.installedApp
{
// if let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
if let latestVersion = self.app.latestAvailableVersion, installedApp.hasUpdate
if let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
{
self.updateApp(installedApp, to: latestVersion)
}

View File

@@ -195,7 +195,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
// NOTE: double dequeue of cell has been discontinued
// TODO: Using harcoded value until this is fixed
return CGSize(width: collectionView.bounds.width, height: 200)
return CGSize(width: collectionView.bounds.width, height: 260)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize

View File

@@ -27,12 +27,10 @@ extension AppDelegate
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
static let exportCertificateNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ExportCertificateNotification")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL"
static let exportCertificateCallbackTemplateKey = "callback"
}
@UIApplicationMain
@@ -70,17 +68,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// Register default settings before doing anything else.
UserDefaults.registerDefaults()
// Recreate Database if requested
// NOTE: Userdefaults are local to the SideStore.app sandbox and are not shared
if UserDefaults.standard.recreateDatabaseOnNextStart{
// reset the state
UserDefaults.standard.recreateDatabaseOnNextStart = false
// re-create database
DatabaseManager.recreateDatabase()
}
DatabaseManager.shared.start { (error) in
if let error = error
@@ -93,14 +81,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
self.setTintColor()
AnalyticsManager.shared.start()
self.setTintColor()
self.prepareImageCache()
// TODO: @mahee96: find if we need to start em_proxy as in altstore?
if UserDefaults.standard.enableEMPforWireguard {
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
}
// start_em_proxy(bind_addr: Consts.Proxy.serverURL)
SecureValueTransformer.register()
@@ -112,7 +99,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
#if DEBUG && targetEnvironment(simulator)
#if DEBUG && (targetEnvironment(simulator) || BETA)
UserDefaults.standard.isDebugModeEnabled = true
#endif
@@ -125,9 +112,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
{
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
// TODO: @mahee96: find if we need to stop em_proxy as in altstore?
if UserDefaults.standard.enableEMPforWireguard {
stop_em_proxy()
}
// stop_em_proxy()
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
@@ -144,9 +129,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillEnterForeground(_ application: UIApplication)
{
AppManager.shared.update()
if UserDefaults.standard.enableEMPforWireguard {
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
PatreonAPI.shared.refreshPatreonAccount()
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
@@ -297,26 +282,6 @@ private extension AppDelegate
return true
case "pairing":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["urlName"]?.removingPercentEncoding else { return false }
DispatchQueue.main.async {
exportPairingFile(callbackTemplate)
}
return true
case "certificate":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["callback_template"]?.removingPercentEncoding else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.exportCertificateNotification, object: nil, userInfo: [AppDelegate.exportCertificateCallbackTemplateKey: callbackTemplate])
}
return true
default: return false
}
}
@@ -459,8 +424,6 @@ private extension AppDelegate
try context.save()
let updatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest()
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<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">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -14,7 +14,7 @@
<objects>
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" 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"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/>
@@ -42,13 +42,13 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
</view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
@@ -57,13 +57,13 @@
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
<rect key="frame" x="0.0" y="0.0" width="343" height="41"/>
<rect key="frame" x="0.0" y="0.0" width="333.5" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
<rect key="frame" x="0.0" y="47" width="306.5" height="20.5"/>
<rect key="frame" x="0.0" y="47" width="308.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -160,7 +160,7 @@
</stackView>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
@@ -179,7 +179,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="498.5" width="343" height="96.5"/>
<rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
@@ -198,10 +198,6 @@
</stackView>
</subviews>
<constraints>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
</constraints>
</view>
@@ -219,15 +215,19 @@
<constraints>
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
</constraints>
</view>
@@ -264,7 +264,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="64" width="375" height="544"/>
<rect key="frame" x="0.0" y="44" width="375" height="564"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
@@ -298,7 +298,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="161" width="343" height="95.5"/>
<rect key="frame" x="16" y="168" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -310,7 +310,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="16" width="264" height="64"/>
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -318,7 +318,7 @@
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable LocalDevVPN and use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -329,7 +329,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="287.5" width="343" height="95.5"/>
<rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -341,7 +341,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
<rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -360,7 +360,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="413.5" width="343" height="95.5"/>
<rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -372,7 +372,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
<rect key="frame" x="79" y="17" width="264" height="62"/>
<rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -381,7 +381,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
<rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -431,7 +431,7 @@
</objects>
<point key="canvasLocation" x="1353" y="736"/>
</scene>
<!--Refresh SideStore-->
<!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX">
<objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
@@ -485,7 +485,7 @@
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Refresh SideStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>

View File

@@ -287,7 +287,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="20" width="335" height="34"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
@@ -353,7 +353,7 @@
</stackView>
</subviews>
</stackView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
@@ -532,7 +532,6 @@
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" name="Primary"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
@@ -562,7 +561,6 @@
<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"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
@@ -915,7 +913,6 @@
<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"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>

View File

@@ -538,8 +538,7 @@ private extension BrowseViewController
let app = self.dataSource.item(at: indexPath)
// if let installedApp = app.installedApp, !installedApp.isUpdateAvailable
if let installedApp = app.installedApp, !installedApp.hasUpdate
if let installedApp = app.installedApp, !installedApp.isUpdateAvailable
{
self.open(installedApp)
}
@@ -564,8 +563,7 @@ private extension BrowseViewController
}
Task<Void, Never>(priority: .userInitiated) { @MainActor in
// if let installedApp = app.installedApp, installedApp.isUpdateAvailable
if let installedApp = app.installedApp, installedApp.hasUpdate
if let installedApp = app.installedApp, installedApp.isUpdateAvailable
{
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
}

View File

@@ -482,8 +482,7 @@ private extension FeaturedViewController
let storeApp = self.dataSource.item(at: indexPath)
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
{
self.open(installedApp)
}
@@ -501,8 +500,7 @@ private extension FeaturedViewController
return
}
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
{
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
}

View File

@@ -138,12 +138,11 @@ extension AppBannerView
init(app: AppProtocol)
{
self.name = app.name
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
self.developerName = storeApp.developerName
if let track = storeApp.latestSupportedVersion?.channel,
ReleaseTracks.betaTracks.contains(track)
if storeApp.isBeta
{
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
self.isBeta = true
@@ -234,8 +233,7 @@ extension AppBannerView
{
// App is installed
// if installedApp.isUpdateAvailable
if installedApp.hasUpdate
if installedApp.isUpdateAvailable
{
buttonAction = .update
}

View File

@@ -13,18 +13,21 @@ final class CollapsingTextView: UITextView
var isCollapsed = true {
didSet {
guard self.isCollapsed != oldValue else { return }
self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var maximumNumberOfLines = 2 {
didSet {
self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var lineSpacing: Double = 2 {
didSet {
self.shouldResetLayout = true
if #available(iOS 16, *)
{
@@ -39,6 +42,7 @@ final class CollapsingTextView: UITextView
override var text: String! {
didSet {
self.shouldResetLayout = true
guard #available(iOS 16, *) else { return }
self.updateText()
@@ -47,6 +51,9 @@ final class CollapsingTextView: UITextView
let moreButton = UIButton(type: .system)
private var shouldResetLayout: Bool = false
private var previousSize: CGSize?
override init(frame: CGRect, textContainer: NSTextContainer?)
{
super.init(frame: frame, textContainer: textContainer)
@@ -108,39 +115,45 @@ final class CollapsingTextView: UITextView
height: font.lineHeight)
self.moreButton.frame = moreButtonFrame
if self.isCollapsed
if self.shouldResetLayout || self.previousSize != self.bounds.size
{
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
if self.isCollapsed
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
}
else
{
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
}
else
{
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
self.textContainer.maximumNumberOfLines = 0
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
}
else
{
self.textContainer.maximumNumberOfLines = 0
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
self.invalidateIntrinsicContentSize()
}
self.invalidateIntrinsicContentSize()
self.shouldResetLayout = false
self.previousSize = self.bounds.size
}
}

View File

@@ -65,23 +65,13 @@ class ToastView: RSTToastView
self.opensErrorLog = opensLog
}
enum InfoMode: String {
case fullError
case localizedDescription
}
convenience init(error: Error){
self.init(error: error, mode: .localizedDescription)
}
convenience init(error: Error, mode: InfoMode)
convenience init(error: Error)
{
let error = error as NSError
let mode = mode == .fullError ? ErrorProcessing.InfoMode.fullError : ErrorProcessing.InfoMode.localizedDescription
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let detailText = ErrorProcessing(mode).getDescription(error: error)
let detailText = ErrorProcessing(.fullError).getDescription(error: error)
self.init(text: text, detailText: detailText)
}

View File

@@ -81,6 +81,8 @@
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>BuildRevision</key>
<string>$(BUILD_REVISION)</string>
<key>INIntentsSupported</key>
<array>
<string>RefreshAllIntent</string>

View File

@@ -12,73 +12,84 @@ import EmotionalDamage
import minimuxer
import WidgetKit
import AltSign
import AltStoreCore
import UniformTypeIdentifiers
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{
private var didFinishLaunching = false
private var retries = 0
private var maxRetries = 3
private var splashView: SplashView!
private var destinationViewController: TabBarController?
private var startTime: Date!
override func viewDidLoad() {
super.viewDidLoad()
splashView = SplashView(frame: view.bounds, appName: "SideStore")
destinationViewController = storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
view.addSubview(splashView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard !didFinishLaunching else { return }
Task {
startTime = Date()
await runLaunchSequence()
doPostLaunch()
private var destinationViewController: TabBarController!
override var launchConditions: [RSTLaunchCondition] {
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
DatabaseManager.shared.start(completionHandler: completionHandler)
}
return [isDatabaseStarted]
}
private func runLaunchSequence() async {
guard retries < maxRetries else { return }
retries += 1
await Task.detached {
if !DatabaseManager.shared.isStarted {
await withCheckedContinuation { continuation in
DatabaseManager.shared.start { error in
if let error {
Task { await self.handleLaunchError(error, retryCallback: self.runLaunchSequence) }
} else {
Task { await self.finishLaunching() }
override var childForStatusBarStyle: UIViewController? {
return self.children.first
}
override var childForStatusBarHidden: UIViewController? {
return self.children.first
}
override func viewDidLoad()
{
defer {
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
}
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
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")
}
continuation.resume(returning: ())
}
}
} else {
await self.finishLaunching()
}
}.value
}
private func doPostLaunch() {
SideJITManager.shared.checkAndPromptIfNeeded(presentingVC: self)
}
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
DispatchQueue.global().async { SideJITManager.shared.askForNetwork() }
DispatchQueue.global().async {
self.askfornetwork()
}
print("SideJITServer Enabled")
}
#if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
detectAndImportAccountFile()
if UserDefaults.standard.enableEMPforWireguard {
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
}
guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.")
return
@@ -86,189 +97,281 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
start_minimuxer_threads(pf)
#endif
}
func start_minimuxer_threads(_ pairing_file: String) {
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
try minimuxer.startWithLogger(pairing_file, documentsDirectory, loggingEnabled)
} 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")")
func askfornetwork() {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
var SJSURL = address
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
start_auto_mounter(documentsDirectory)
// 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? {
let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
print("Loaded ALTPairingFile from \(documentsPath.path)")
return contents
} else if
let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty,
!UserDefaults.standard.isPairingReset {
print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
print("Loaded ALTPairingFile from Info.plist")
return plistString
} else {
// Show an alert explaining the pairing file
// Create new Alert
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
// Try to load it from a file picker
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
types.append(.xml)
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil)
UserDefaults.standard.isPairingReset = false
})
//Add "help" button to take user to wiki
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
let wikiURL: String = "https://docs.sidestore.io/docs/getting-started/pairing-file"
if let url = URL(string: wikiURL) {
UIApplication.shared.open(url)
}
sleep(2)
exit(0)
}
//Add buttons to dialog message
dialogMessage.addAction(wikiOption)
dialogMessage.addAction(ok)
func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) }
// Present Alert to
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
}
}
func displayError(_ msg: String) {
print(msg)
let alert = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
self.present(alert, animated: true)
}
// Create a new alert
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
// Present alert to user
self.present(dialogMessage, animated: true, completion: nil)
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let url = urls[0]
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
defer {
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
}
do {
let data = try Data(contentsOf: url)
guard let pairingString = String(data: data, encoding: .utf8) else {
// Read to a string
let data1 = try Data(contentsOf: urls[0])
let pairing_string = String(bytes: data1, encoding: .utf8)
if pairing_string == nil {
displayError("Unable to read pairing file")
return
}
try pairingString.write(to: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName), atomically: true, encoding: .utf8)
start_minimuxer_threads(pairingString)
// Save to a file for next launch
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!)
} catch {
displayError("Unable to read pairing file")
}
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
controller.dismiss(animated: true, completion: nil)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
}
func importAccountAtFile(_ file: URL, remove: Bool = false) {
_ = file.startAccessingSecurityScopedResource()
defer { file.stopAccessingSecurityScopedResource() }
guard let accountD = try? Data(contentsOf: file) else {
return Logger.main.notice("Could not parse data from file \(file)")
func start_minimuxer_threads(_ pairing_file: String) {
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
// enable minimuxer console logging only if enabled in settings
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled)
} catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
}
guard let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountD) else {
return Logger.main.notice("Could not parse data from file \(file)")
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
}
print("We want to import this account probably: \(account)")
if remove {
try? FileManager.default.removeItem(at: file)
else {
start_auto_mounter(documentsDirectory)
}
Keychain.shared.appleIDEmailAddress = account.email
Keychain.shared.appleIDPassword = account.password
Keychain.shared.adiPb = account.adiPB
Keychain.shared.identifier = account.local_user
if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) {
Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")!
Keychain.shared.signingCertificatePassword = account.certpass
let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!")
return toastView.show(in: self)
} else {
let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!")
return toastView.show(in: self)
}
}
func detectAndImportAccountFile() {
let accountFileURL = FileManager.default.documentsDirectory.appendingPathComponent("Account.sideconf")
#if !DEBUG
importAccountAtFile(accountFileURL, remove: true)
#else
importAccountAtFile(accountFileURL)
#endif
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
}
}
extension LaunchViewController {
@MainActor
func handleLaunchError(_ error: Error, retryCallback: (() async -> Void)? = nil) {
do { throw error } catch let error as NSError {
extension LaunchViewController
{
override func handleLaunchError(_ error: Error)
{
do
{
throw error
}
catch let error as NSError
{
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let desc: String
if #available(iOS 14.5, *) {
desc = ([error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }).joined(separator: "\n\n")
} else {
desc = error.debugDescription
let errorDescription: String
if #available(iOS 14.5, *)
{
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
errorDescription = errorMessages.joined(separator: "\n\n")
}
let alert = UIAlertController(title: title, message: desc, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default) { _ in
Task { await retryCallback?() }
})
present(alert, animated: true)
else
{
errorDescription = error.debugDescription
}
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
self.handleLaunchConditions()
}))
self.present(alertController, animated: true, completion: nil)
}
}
@MainActor
func finishLaunching() async {
guard !didFinishLaunching else { return }
didFinishLaunching = true
override func finishLaunching()
{
super.finishLaunching()
guard !self.didFinishLaunching else { return }
AppManager.shared.update()
AppManager.shared.updatePatronsIfNeeded()
PatreonAPI.shared.refreshPatreonAccount()
AppManager.shared.updateAllSources { result in
guard case .failure(let error) = result else { return }
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
print("Failed to update sources on launch. \(errorDesc)")
var mode: ToastView.InfoMode = .fullError
if String(describing: error).contains("The Internet connection appears to be offline"){
mode = .localizedDescription // dont make noise!
}
let toastView = ToastView(error: error, mode: mode)
let toastView = ToastView(error: error)
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self.destinationViewController!.selectedViewController ?? self.destinationViewController!)
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
}
updateKnownSources()
self.updateKnownSources()
// Ask widgets to be refreshed
WidgetCenter.shared.reloadAllTimelines()
didFinishLaunching = true
let destinationVC = destinationViewController!
// Add view controller as child (rather than presenting modally)
// so tint adjustment + card presentations works correctly.
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
self.destinationViewController.view.alpha = 0.0
self.addChild(self.destinationViewController)
self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
self.destinationViewController.didMove(toParent: self)
let elapsed = abs(startTime.timeIntervalSinceNow)
let remaining = elapsed >= 1 ? 0 : 1 - elapsed
try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
destinationVC.loadViewIfNeeded()
addChild(destinationVC)
destinationVC.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(destinationVC.view)
destinationVC.didMove(toParent: self)
// Pin edges BEFORE animation
NSLayoutConstraint.activate([
destinationVC.view.topAnchor.constraint(equalTo: view.topAnchor),
destinationVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
destinationVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
destinationVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
// Set initial alpha for fade-in
destinationVC.view.alpha = 0
UIView.transition(with: view, duration: 0.3, options: .transitionCrossDissolve) { [self] in
self.splashView.alpha = 0
destinationVC.view.alpha = 1
} completion: { _ in
self.splashView.removeFromSuperview()
self.destinationViewController = destinationVC
UIView.animate(withDuration: 0.2) {
self.destinationViewController.view.alpha = 1.0
}
self.didFinishLaunching = true
}
}
func updateKnownSources() {
private extension LaunchViewController
{
func updateKnownSources()
{
AppManager.shared.updateKnownSources { result in
switch result {
switch result
{
case .failure(let error): print("[ALTLog] Failed to update known sources:", error)
case .success((_, let blockedSources)):
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier })
let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL })
let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", #keyPath(Source.identifier), blockedSourceIDs, #keyPath(Source.sourceURL), blockedSourceURLs)
let sourceErrors = Source.all(satisfying: predicate, in: context).map { source in
let blocked = blockedSources.first { $0.identifier == source.identifier }
return SourceError.blocked(source, bundleIDs: blocked?.bundleIDs, existingSource: source)
let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@",
#keyPath(Source.identifier), blockedSourceIDs,
#keyPath(Source.sourceURL), blockedSourceURLs)
let sourceErrors = Source.all(satisfying: predicate, in: context).map { (source) in
let blockedSource = blockedSources.first { $0.identifier == source.identifier }
return SourceError.blocked(source, bundleIDs: blockedSource?.bundleIDs, existingSource: source)
}
guard !sourceErrors.isEmpty else { return }
Task {
for error in sourceErrors {
for error in sourceErrors
{
let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name)
let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
await self.presentAlert(title: title, message: message)
}
}
@@ -277,142 +380,3 @@ extension LaunchViewController {
}
}
}
// MARK: - SplashView
final class SplashView: UIView {
let iconView = UIImageView()
let titleLabel = UILabel()
init(frame: CGRect, appName: String) {
super.init(frame: frame)
backgroundColor = .systemBackground
setupIcon()
setupTitle(appName: appName)
}
required init?(coder: NSCoder) { fatalError() }
private func setupIcon() {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.layer.shadowColor = UIColor.black.cgColor
container.layer.shadowOpacity = 0.25
container.layer.shadowOffset = CGSize(width: 0, height: 4)
container.layer.shadowRadius = 8
addSubview(container)
iconView.image = UIImage(named: "AppIcon") ?? UIImage(named: "AppIcon60x60") ?? UIImage(systemName: "app.fill")
iconView.contentMode = .scaleAspectFit
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.layer.cornerRadius = 24
iconView.clipsToBounds = true
container.addSubview(iconView)
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: centerXAnchor),
container.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20),
container.widthAnchor.constraint(equalToConstant: 120),
container.heightAnchor.constraint(equalToConstant: 120),
iconView.topAnchor.constraint(equalTo: container.topAnchor),
iconView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
iconView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
])
}
private func setupTitle(appName: String) {
titleLabel.text = appName
titleLabel.font = .systemFont(ofSize: 24, weight: .bold)
titleLabel.textColor = .label
titleLabel.textAlignment = .center
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
])
}
}
// MARK: - PairingFileManager
final class PairingFileManager {
static let shared = PairingFileManager()
func fetchPairingFile(presentingVC: UIViewController) -> String? {
let fm = FileManager.default
let filename = pairingFileName
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
if fm.fileExists(atPath: documentsPath.path),
let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
return contents
}
if let url = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
fm.fileExists(atPath: url.path),
let data = fm.contents(atPath: url.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty, !UserDefaults.standard.isPairingReset { return contents }
if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String,
!plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset { return plistString }
presentPairingFileAlert(on: presentingVC)
return nil
}
private func presentPairingFileAlert(on vc: UIViewController) {
let alert = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Help", style: .default) { _ in
if let url = URL(string: "https://docs.sidestore.io/docs/advanced/pairing-file") { UIApplication.shared.open(url) }
sleep(2); exit(0)
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
var types = UTType.types(tag: "plist", tagClass: .filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: .filenameExtension, conformingTo: .data))
types.append(.xml)
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types)
picker.delegate = vc as? UIDocumentPickerDelegate
picker.shouldShowFileExtensions = true
vc.present(picker, animated: true)
UserDefaults.standard.isPairingReset = false
})
vc.present(alert, animated: true)
}
}
// MARK: - SideJITManager
final class SideJITManager {
static let shared = SideJITManager()
func checkAndPromptIfNeeded(presentingVC: UIViewController) {
guard #available(iOS 17, *), !UserDefaults.standard.sidejitenable else { return }
DispatchQueue.global().async {
self.isSideJITServerDetected { result in
DispatchQueue.main.async {
switch result {
case .success():
let alert = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in UserDefaults.standard.sidejitenable = true })
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
presentingVC.present(alert, animated: true)
case .failure(_): print("Cannot find sideJITServer")
}
}
}
}
}
func askForNetwork() {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
URLSession.shared.dataTask(with: URL(string: "\(SJSURL)/re/")!) { data, resp, err in
print("data: \(String(describing: data)), response: \(String(describing: resp)), error: \(String(describing: err))")
}.resume()
}
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
guard let url = URL(string: SJSURL) else { return }
URLSession.shared.dataTask(with: url) { _, _, error in
if let error = error { completion(.failure(error)); return }
completion(.success(()))
}.resume()
}
}

View File

@@ -8,6 +8,7 @@
import Foundation
import UIKit
import SwiftUI
import UserNotifications
import MobileCoreServices
import Intents
@@ -22,6 +23,7 @@ import Roxas
extension AppManager
{
static let didFetchSourceNotification = Notification.Name("io.sidestore.AppManager.didFetchSource")
static let didUpdatePatronsNotification = Notification.Name("io.sidestore.AppManager.didUpdatePatrons")
static let didAddSourceNotification = Notification.Name("io.sidestore.AppManager.didAddSource")
static let didRemoveSourceNotification = Notification.Name("io.sidestore.AppManager.didRemoveSource")
static let willInstallAppFromNewSourceNotification = Notification.Name("io.sidestore.AppManager.willInstallAppFromNewSource")
@@ -589,6 +591,34 @@ extension AppManager
return updateKnownSourcesOperation
}
func updatePatronsIfNeeded()
{
guard self.operationQueue.operations.allSatisfy({ !($0 is UpdatePatronsOperation) }) else {
// There's already an UpdatePatronsOperation running.
return
}
self.updatePatronsResult = nil
let updatePatronsOperation = UpdatePatronsOperation()
updatePatronsOperation.resultHandler = { (result) in
do
{
try result.get()
self.updatePatronsResult = .success(())
}
catch
{
print("Error updating Friend Zone Patrons:", error)
self.updatePatronsResult = .failure(error)
}
NotificationCenter.default.post(name: AppManager.didUpdatePatronsNotification, object: self)
}
self.run([updatePatronsOperation], context: nil)
}
func updateAllSources(completion: @escaping (Result<Void, Error>) -> Void)
{
self.updateSourcesResult = nil
@@ -604,9 +634,6 @@ extension AppManager
do
{
let (_, context) = try result.get()
// print("\n\n\n\(context.insertedObjects)\n\n\n")
// print("\n\n\n\(context.updatedObjects)\n\n\n")
// print("\n\n\n\(context.deletedObjects)\n\n\n")
try context.save()
DispatchQueue.main.async {
@@ -675,37 +702,9 @@ extension AppManager
}
}
let operation = AppOperation.install(app)
self.perform([operation], presentingViewController: presentingViewController, group: group)
Task{
var app: AppProtocol = app
// ---- Preflight bundle ID resolution ----
if UserDefaults.standard.customizeAppId, // only show prompt when enabled by user
let presentingViewController {
let originalBundleID = app.bundleIdentifier
let resolution = await self.resolveBundleID(
initial: originalBundleID,
presentingViewController: presentingViewController
)
switch resolution {
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
group.progress.cancel()
case .resolved(let newBundleID):
app = AnyApp(
name: app.name,
bundleIdentifier: newBundleID,
url: app.url,
storeApp: app.storeApp
)
}
}
await self.perform([.install(app)], presentingViewController: presentingViewController, group: group)
}
return group
}
@@ -730,11 +729,10 @@ extension AppManager
}
}
assert(appVersion as AnyObject !== installedApp) // Make sure we never accidentally "update" to already installed app.
let operation = AppOperation.update(appVersion)
assert(operation.app as AnyObject !== installedApp) // Make sure we never accidentally "update" to already installed app.
Task{
await self.perform([.update(appVersion)], presentingViewController: presentingViewController, group: group)
}
self.perform([operation], presentingViewController: presentingViewController, group: group)
return group.progress
}
@@ -744,20 +742,16 @@ extension AppManager
{
let group = group ?? RefreshGroup()
Task{
await self.perform(installedApps.map { .refresh($0) }, presentingViewController: presentingViewController, group: group)
}
return group
let operations = installedApps.map { AppOperation.refresh($0) }
return self.perform(operations, presentingViewController: presentingViewController, group: group)
}
func activate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
{
let group = RefreshGroup()
Task{
await self.perform([.activate(installedApp)], presentingViewController: presentingViewController, group: group)
}
let operation = AppOperation.activate(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
group.completionHandler = { (results) in
do
@@ -815,9 +809,8 @@ extension AppManager
}
}
Task{
await self.perform([.deactivate(installedApp)], presentingViewController: presentingViewController, group: group)
}
let operation = AppOperation.deactivate(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
}
}
@@ -841,9 +834,8 @@ extension AppManager
}
}
Task{
await self.perform([.backup(installedApp)], presentingViewController: presentingViewController, group: group)
}
let operation = AppOperation.backup(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
}
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
@@ -868,9 +860,8 @@ extension AppManager
}
}
Task{
await self.perform([.restore(installedApp)], presentingViewController: presentingViewController, group: group)
}
let operation = AppOperation.restore(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
}
func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
@@ -1097,7 +1088,7 @@ private extension AppManager
}
@discardableResult
private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) async -> RefreshGroup
private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) -> RefreshGroup
{
let operations = operations.filter { self.progress(for: $0) == nil || self.progress(for: $0)?.isCancelled == true }
@@ -1159,10 +1150,38 @@ private extension AppManager
case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough
case .refresh(let app):
let refreshProgress = self._refresh(app, operation: operation, group: group) { (result) in
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
// Check if backup app is installed in place of real app.
// let altBackupUti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
// if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
// altBackupUti != nil || // why would altbackup requires reinstall? it shouldn't cause we are just renewing profiles
// app.needsResign || // why would an app require resign during refresh? it shouldn't!
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
// => mahee96: jkcoxson confirmed misagent manages profiles independently without requiring lockdownd or installd intervention, so sidestore profile renewal shouldn't require reinstall
// app.bundleIdentifier == StoreApp.altstoreAppID
// {
// Resign app instead of just refreshing profiles because either:
// * Refreshing using different certificate // when can this happen?, lets assume, refreshing with different certificate, why not just ask user to re-install manually? (probably we need re-install button)
// * Backup app is still installed // but why? I mean the AltBackup was put in place for a reason? ie during refresh just renew appIDs don't care about the app itself.
// * App explicitly needs resigning // when can this happen?
// * Device is jailbroken and using AltDaemon on iOS 14.0 or later (b/c refreshing with provisioning profiles is broken)
// let installProgress = self._install(app, operation: operation, group: group) { (result) in
// self.finish(operation, result: result, group: group, progress: progress)
// }
// progress?.addChild(installProgress, withPendingUnitCount: 80)
// }
// else
// {
// Refreshing with same certificate as last time, and backup app isn't still installed,
// so we can just refresh provisioning profiles.
let refreshProgress = self._refresh(app, operation: operation, group: group) { (result) in
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
// }
case .activate(let app):
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
self.finish(operation, result: result, group: group, progress: progress)
@@ -1209,13 +1228,13 @@ private extension AppManager
}
else
{
// Disable the idleTimeout
DispatchQueue.main.schedule {
if !UIApplication.shared.isIdleTimerDisabled { // accept only once if concurrent
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
}
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
}
performAppOperations()
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = false
}
}
return group
@@ -1232,10 +1251,21 @@ private extension AppManager
{
let progress = Progress.discreteProgress(totalUnitCount: 100)
let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
let context = context ?? InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
assert(context.authenticatedContext === group.context)
context.beginInstallationHandler = { (installedApp) in
switch appOperation
{
case .update where installedApp.bundleIdentifier == StoreApp.altstoreAppID:
// AltStore will quit before installation finishes,
// so assume if we get this far the update will finish successfully.
let event = AnalyticsManager.Event.updatedApp(installedApp)
AnalyticsManager.shared.trackEvent(event)
default: break
}
group.beginInstallationHandler?(installedApp)
}
@@ -1256,6 +1286,20 @@ private extension AppManager
}
}
var verifyPledgeOperation: VerifyAppPledgeOperation?
if let storeApp = app.storeApp
{
verifyPledgeOperation = VerifyAppPledgeOperation(storeApp: storeApp, presentingViewController: context.presentingViewController)
verifyPledgeOperation?.resultHandler = { result in
switch result
{
case .failure(let error):
context.error = error
case .success: break
}
}
}
/* Download */
let downloadedAppURL = context.temporaryDirectory.appendingPathComponent("Cached.app")
let downloadOperation = DownloadAppOperation(app: downloadingApp, destinationURL: downloadedAppURL, context: context)
@@ -1267,8 +1311,7 @@ private extension AppManager
if cacheApp
{
let updatedApp = AnyApp(from: app, bundleId: context.bundleIdentifier)
try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: updatedApp), shouldReplace: true)
try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: app), shouldReplace: true)
}
}
catch
@@ -1278,9 +1321,15 @@ private extension AppManager
}
progress.addChild(downloadOperation.progress, withPendingUnitCount: 25)
if let verifyPledgeOperation
{
downloadOperation.addDependency(verifyPledgeOperation)
}
/* Verify App */
let permissionsMode = UserDefaults.shared.permissionCheckingDisabled ? .none : permissionReviewMode
let verifyOperation = VerifyAppOperation(permissionsMode: permissionsMode, context: context, customBundleId: app.bundleIdentifier)
let verifyOperation = VerifyAppOperation(permissionsMode: permissionsMode, context: context)
verifyOperation.resultHandler = { (result) in
do
{
@@ -1433,7 +1482,7 @@ private extension AppManager
let patchAppURL = URL(string: patchAppLink)
else { throw OperationError.invalidApp }
let patchApp = AnyApp(name: app.name, bundleIdentifier: context.bundleIdentifier, url: patchAppURL, storeApp: nil)
let patchApp = AnyApp(name: app.name, bundleIdentifier: app.bundleIdentifier, url: patchAppURL, storeApp: nil)
DispatchQueue.main.async {
let storyboard = UIStoryboard(name: "PatchApp", bundle: nil)
@@ -1455,7 +1504,7 @@ private extension AppManager
presentingViewController?.dismiss(animated: true, completion: nil)
}
}
presentingViewController.present(navigationController, animated: true, completion: nil)
presentingViewController.present(navigationController, animated: true, completion: nil)
}
}
catch
@@ -1467,24 +1516,6 @@ private extension AppManager
patchAppOperation.addDependency(deactivateAppsOperation)
let modifyAppExBundleIdOperation = RSTAsyncBlockOperation { operation in
if !context.useMainProfile {
operation.finish()
return
}
if let app = context.app, let profile = context.provisioningProfiles?[context.bundleIdentifier] {
var appexBundleIds: [String: String] = [:]
for appex in app.appExtensions {
appexBundleIds[appex.bundleIdentifier] = appex.bundleIdentifier.replacingOccurrences(of: app.bundleIdentifier, with: profile.bundleIdentifier)
}
context.appexBundleIds = appexBundleIds
}
operation.finish()
}
modifyAppExBundleIdOperation.addDependency(fetchProvisioningProfilesOperation)
/* Resign */
let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in
@@ -1499,7 +1530,6 @@ private extension AppManager
}
}
resignAppOperation.addDependency(patchAppOperation)
resignAppOperation.addDependency(modifyAppExBundleIdOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
@@ -1545,6 +1575,7 @@ private extension AppManager
// Operations picked for request
var operations = [
verifyPledgeOperation,
downloadOperation,
verifyOperation,
removeAppExtensionsOperation,
@@ -1552,7 +1583,6 @@ private extension AppManager
patchAppOperation,
refreshAnisetteDataOperation,
fetchProvisioningProfilesOperation,
modifyAppExBundleIdOperation,
resignAppOperation,
sendAppOperation,
installOperation
@@ -1632,8 +1662,8 @@ private extension AppManager
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.fileURL)
context.useMainProfile = app.useMainProfile
// Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path
// Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path
//App-Extensions: Ensure DB data and disk state must match
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)
@@ -2065,16 +2095,6 @@ private extension AppManager
func finish(_ operation: AppOperation, result: Result<InstalledApp, Error>, group: RefreshGroup, progress: Progress?)
{
// Remove disableIdleTimeout
// TODO: This should disable for the last finish() request not the first though for batches
// probably if we are in batch mode, we can count expected no of finishes() to arrive
// and schedule disabling only on last request by matching it with count.
DispatchQueue.main.schedule {
if UIApplication.shared.isIdleTimerDisabled { // accept only once if concurrent
UIApplication.shared.isIdleTimerDisabled = false
}
}
// Must remove before saving installedApp.
if let currentProgress = self.progress(for: operation), currentProgress == progress
{
@@ -2092,6 +2112,27 @@ private extension AppManager
self.scheduleExpirationWarningLocalNotification(for: installedApp)
}
let event: AnalyticsManager.Event?
switch operation
{
case .install: event = .installedApp(installedApp)
case .refresh: event = .refreshedApp(installedApp)
case .update where installedApp.bundleIdentifier == StoreApp.altstoreAppID:
// AltStore quits before update finishes, so we've preemptively logged this update event.
// In case AltStore doesn't quit, such as when update has a different bundle identifier,
// make sure we don't log this update event a second time.
event = nil
case .update: event = .updatedApp(installedApp)
case .activate, .deactivate, .backup, .restore: event = nil
}
if let event = event
{
AnalyticsManager.shared.trackEvent(event)
}
// Ask widgets to be refreshed
WidgetCenter.shared.reloadAllTimelines()
@@ -2176,7 +2217,7 @@ private extension AppManager
switch operation
{
case _ where requiresSerialQueue: fallthrough
case is InstallAppOperation, is RefreshAppOperation, is BackupAppOperation:
case is InstallAppOperation, is RefreshAppOperation, is BackupAppOperation, is VerifyAppPledgeOperation:
if let installAltStoreOperation = operation as? InstallAppOperation, installAltStoreOperation.context.bundleIdentifier == StoreApp.altstoreAppID
{
// Add dependencies on previous serial operations in `context` to ensure re-installing AltStore goes last.
@@ -2228,126 +2269,3 @@ private extension AppManager
}
}
}
private enum BundleIDAlertKeys {
static var okAction: UInt8 = 0
}
private func _isValidBundleID(_ value: String) -> Bool {
let pattern = #"^[A-Za-z][A-Za-z0-9\-]*(\.[A-Za-z0-9\-]+)+$"#
return value.range(of: pattern, options: .regularExpression) != nil
}
private extension UIResponder {
@objc func _validateBundleIDText(_ sender: UITextField) {
let isValid = sender.text.map(_isValidBundleID) ?? false
sender.backgroundColor =
isValid || sender.text?.isEmpty == true
? .clear
: UIColor.systemRed.withAlphaComponent(0.2)
if
let alert = sender.superview?.superview as? UIAlertController,
let okAction = objc_getAssociatedObject(alert, &BundleIDAlertKeys.okAction) as? UIAlertAction
{
okAction.isEnabled = isValid
}
}
}
private extension AppManager {
func _presentBundleIDOverrideDialog(
bundleIdentifier: String,
presentingViewController: UIViewController,
completion: @escaping (BundleIDResolution) -> Void
) {
let alert = self._makeBundleIDOverrideAlert(
initialBundleID: bundleIdentifier,
completion: completion
)
presentingViewController.present(alert, animated: true)
}
func _makeBundleIDOverrideAlert(
initialBundleID: String,
completion: @escaping (BundleIDResolution) -> Void
) -> UIAlertController {
let titleText = NSLocalizedString("AppID Customization", comment: "")
let messageText = NSLocalizedString("Customize the AppID if required and press 'Confirm' to proceed.", comment: "")
let alert = UIAlertController(
title: titleText,
message: messageText,
preferredStyle: .alert
)
var okAction: UIAlertAction!
alert.addTextField { textField in
textField.text = initialBundleID
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.addTarget(
nil,
action: #selector(UIResponder._validateBundleIDText(_:)),
for: .editingChanged
)
}
okAction = UIAlertAction(title: NSLocalizedString("Confirm", comment: ""), style: .default) { _ in
completion(.resolved(alert.textFields?.first?.text ?? initialBundleID))
}
okAction.isEnabled = _isValidBundleID(initialBundleID)
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in
completion(.cancelled)
}
alert.addAction(cancelAction)
alert.addAction(okAction)
objc_setAssociatedObject(
alert,
&BundleIDAlertKeys.okAction,
okAction,
.OBJC_ASSOCIATION_ASSIGN
)
return alert
}
}
// ---- Part 1: Add async resolver ----
private extension AppManager {
enum BundleIDResolution {
case resolved(String)
case cancelled
}
@MainActor
func resolveBundleID(
initial: String,
presentingViewController: UIViewController
) async -> BundleIDResolution {
await withCheckedContinuation { continuation in
let alert = self._makeBundleIDOverrideAlert(
initialBundleID: initial
) { result in
continuation.resume(returning: result)
}
presentingViewController.present(alert, animated: true)
}
}
}

View File

@@ -16,7 +16,6 @@ import AltStoreCore
import AltSign
import Roxas
import minimuxer
import SemanticVersion
import Nuke
@@ -166,11 +165,9 @@ class MyAppsViewController: UICollectionViewController, PeekPopPreviewing
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
var minimuxerStatus: Bool {
// added isMinimuxerStatusCheckEnabled to forcefully ignore minimuxer status if status check is disabled in settings
guard !UserDefaults.standard.isMinimuxerStatusCheckEnabled || minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No Wi-Fi or VPN!")).show(in: self)
guard minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
return false
}
return true
@@ -237,30 +234,18 @@ private extension MyAppsViewController
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.maximumNumberOfLines = 2
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.configure(for: app, action: .update)
var versionText = latestSupportedVersion.localizedVersion
// If the app is SideStore itself, remove the build number to save space
if app.bundleIdentifier == Bundle.Info.appbundleIdentifier,
let version = SemanticVersion(latestSupportedVersion.version)
{
// leave out the build so that it doesnt take up much space
versionText = SemanticVersion(version.major, version.minor, version.patch, version.preRelease).description
}
cell.bannerView.subtitleLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), versionText)
cell.bannerView.subtitleLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), latestSupportedVersion.localizedVersion)
let appName: String
if ReleaseTracks.betaTracks.contains(latestSupportedVersion.channel)
if app.isBeta
{
appName = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
}
@@ -284,9 +269,12 @@ private extension MyAppsViewController
cell.mode = .collapsed
}
cell.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
cell.setNeedsLayout()
// Below lines are necessary to avoid "more" button layout issues.
cell.versionDescriptionTextView.setNeedsLayout()
cell.layoutIfNeeded()
}
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
@@ -367,12 +355,8 @@ private extension MyAppsViewController
formatter.maximumUnitCount = 1
var timeInterval: String? = "expired"
let expirationDate = installedApp.expirationDate
let isExpired = currentDate > expirationDate
if(!isExpired) {
timeInterval = formatter.string(from: currentDate, to: expirationDate)
}
let timeInterval = formatter.string(from: currentDate, to: installedApp.expirationDate)
cell.bannerView.button.setTitle(timeInterval?.uppercased(), for: .normal)
cell.bannerView.button.isIndicatingActivity = false
@@ -380,7 +364,7 @@ private extension MyAppsViewController
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.buttonLabel.isHidden = isExpired
cell.bannerView.buttonLabel.isHidden = false
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
cell.bannerView.button.removeTarget(self, action: nil, for: .primaryActionTriggered)
@@ -544,6 +528,11 @@ private extension MyAppsViewController
{
print("[ALTLog] Failed to fetch updates:", error)
}
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isAltStorePatron, PatreonAPI.shared.isAuthenticated
{
self.dataSource.predicate = nil
}
}
}
@@ -719,29 +708,22 @@ private extension MyAppsViewController
let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell
// Toggle the state
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
{
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
// Set collapsed mode on the cell
cell?.mode = .collapsed
}
else
{
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
// Set expanded mode on the cell
cell?.mode = .expanded
}
// Clear cached size so it's recalculated
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
// Animate the change smoothly with a duration
UIView.animate(withDuration: 0.25) {
self.collectionView.performBatchUpdates({
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
self.collectionView.performBatchUpdates({
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
@IBAction func refreshApp(_ sender: UIButton)
@@ -1062,6 +1044,57 @@ private extension MyAppsViewController
cell.bannerView.iconImageView.isIndicatingActivity = false
}
func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result<Void, Error>) -> Void)
{
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
func removeAppExtensions() throws
{
for appExtension in application.appExtensions
{
try FileManager.default.removeItem(at: appExtension.fileURL)
}
let scInfoURL = application.fileURL.appendingPathComponent("SC_Info")
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
if let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL),
let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String]
{
let replacementPaths = sinfReplicationPaths.filter { !$0.starts(with: "PlugIns/") } // Filter out app extension paths.
manifestPlist["SinfReplicationPaths"] = replacementPaths
try manifestPlist.write(to: manifestPlistURL)
}
}
let firstSentence: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
}
else
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
}
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
completion(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
completion(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
let result = Result { try removeAppExtensions() }
completion(result)
})
self.present(alertController, animated: true, completion: nil)
}
@objc func showHiddenUpdatesAlert(_ sender: UIButton)
{
guard !self.unsupportedUpdates.isEmpty else { return }
@@ -1482,6 +1515,15 @@ private extension MyAppsViewController
guard minimuxerStatus else { return }
}
if #available(iOS 17, *), !sidejitenabled {
let error = OperationError.tooNewError as NSError
let localizedError = error.withLocalizedTitle("No iOS 17 On Device JIT!")
ToastView(error: localizedError, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
switch result {
@@ -1560,7 +1602,6 @@ private extension MyAppsViewController
}
catch let error as AppManager.FetchSourcesError
{
print(error)
try await error.managedObjectContext?.performAsync {
try error.managedObjectContext?.save()
}
@@ -1592,7 +1633,6 @@ private extension MyAppsViewController
}
catch let error as NSError
{
print(error)
let toastView = ToastView(error: error.withLocalizedTitle(NSLocalizedString("Unable to Check for Updates", comment: "")))
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self)

View File

@@ -21,19 +21,12 @@ extension UpdateCollectionViewCell
{
var mode: Mode = .expanded {
didSet {
switch self.mode {
case .collapsed:
self.versionDescriptionTextView.isCollapsed = true
case .expanded:
self.versionDescriptionTextView.isCollapsed = false
}
self.setNeedsLayout()
self.update()
}
}
@IBOutlet var bannerView: AppBannerView!
// @IBOutlet var versionDescriptionTextView: CollapsingTextView!
@IBOutlet var versionDescriptionTextView: CollapsingMarkdownView!
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
@IBOutlet private var blurView: UIVisualEffectView!
@@ -92,16 +85,16 @@ extension UpdateCollectionViewCell
}
}
// override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
// {
// // Ensure cell is laid out so it will report correct size.
// self.versionDescriptionTextView.setNeedsLayout()
// self.versionDescriptionTextView.layoutIfNeeded()
//
// let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
//
// return size
// }
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{
// Ensure cell is laid out so it will report correct size.
self.versionDescriptionTextView.setNeedsLayout()
self.versionDescriptionTextView.layoutIfNeeded()
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
return size
}
}
private extension UpdateCollectionViewCell

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -11,7 +11,7 @@
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="SideStore" customModuleProvider="target">
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
@@ -30,7 +30,7 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
@@ -39,7 +39,7 @@
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
<rect key="frame" x="0.0" y="50" width="343" height="75"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="15" y="0.0" width="313" height="26"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -91,7 +91,7 @@
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="secondaryLabelColor">
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -319,8 +319,7 @@ private extension NewsViewController
let app = self.dataSource.item(at: indexPath)
guard let storeApp = app.storeApp else { return }
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
{
self.open(installedApp)
}
@@ -339,8 +338,7 @@ private extension NewsViewController
}
Task<Void, Never>(priority: .userInitiated) { @MainActor in
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
{
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
}

View File

@@ -84,87 +84,71 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
self.finish(.failure(error))
return
}
Task {
// try to use cached session
if
let certificate = Keychain.shared.certificate,
let session = Keychain.shared.session,
let team = Keychain.shared.team
{
if session.anisetteData.date.timeIntervalSinceNow < -40.0 {
let anisetteData = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAnisetteData, any Error>) in
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
fetchAnisetteDataOperation.resultHandler = { (result) in
c.resume(with: result)
}
self.operationQueue.addOperation(fetchAnisetteDataOperation)
}
session.anisetteData = anisetteData
}
self.context.team = team
self.context.session = session
self.context.certificate = certificate
self.finish(.success((team, certificate, session)))
return
}
// Sign In
self.signIn() { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
// new login
do {
let (account, session) = try await withUnsafeThrowingContinuation { c in
self.signIn() { (result) in
c.resume(with: result)
}
}
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success((let account, let session)):
self.context.session = session
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
let team = try await withUnsafeThrowingContinuation { c in
self.fetchTeam(for: account, session: session) { (result) in
c.resume(with: result)
// Fetch Team
self.fetchTeam(for: account, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let team):
self.context.team = team
self.progress.completedUnitCount += 1
// Fetch Certificate
self.fetchCertificate(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let certificate):
self.context.certificate = certificate
self.progress.completedUnitCount += 1
// Register Device
self.registerCurrentDevice(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
// Save account/team to disk.
self.save(team) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
// Must cache App IDs _after_ saving account/team to disk.
self.cacheAppIDs(team: team, session: session) { (result) in
let result = result.map { _ in (team, certificate, session) }
self.finish(result)
}
}
}
}
}
}
}
}
}
self.context.team = team
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
let certificate = try await withUnsafeThrowingContinuation { c in
self.fetchCertificate(for: team, session: session) { (result) in
c.resume(with: result)
}
}
self.context.certificate = certificate
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
let _ = try await withUnsafeThrowingContinuation { c in
self.registerCurrentDevice(for: team, session: session) { (result) in
c.resume(with: result)
}
}
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
try await withUnsafeThrowingContinuation { c in
self.save(team) { (result) in
c.resume(with: result)
}
}
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
try await withUnsafeThrowingContinuation { c in
self.cacheAppIDs(team: team, session: session) { (result) in
c.resume(with: result)
}
}
Keychain.shared.team = team
Keychain.shared.certificate = certificate
Keychain.shared.session = session
self.finish(.success((team, certificate, session)))
} catch {
self.finish(.failure(error))
}
}
}
@@ -375,29 +359,6 @@ private extension AuthenticationOperation
}
}
if let adsid = Keychain.shared.appleIDAdsid, let xcodeToken = Keychain.shared.appleIDXcodeToken {
Logger.sideload.notice("Authenticating Apple ID with tokens...")
let semaphore = DispatchSemaphore(value: 0)
var shouldContinue = true
Task {
defer {
semaphore.signal()
}
do {
let (account, session) = try await self.authenticateWithToken(adsid: adsid, xcodeToken: xcodeToken)
completionHandler(.success((account, session)))
shouldContinue = false
} catch {
Logger.sideload.notice("Authentication failed with token. Fall back to email and password login: \(error)")
}
}
semaphore.wait()
if !shouldContinue {
return
}
}
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{
Logger.sideload.notice("Authenticating Apple ID...")
@@ -423,25 +384,6 @@ private extension AuthenticationOperation
}
}
func authenticateWithToken(adsid: String, xcodeToken: String) async throws -> (ALTAccount, ALTAppleAPISession) {
let anisetteData = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAnisetteData, any Error>) in
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
fetchAnisetteDataOperation.resultHandler = { (result) in
c.resume(with: result)
}
self.operationQueue.addOperation(fetchAnisetteDataOperation)
}
let session = ALTAppleAPISession(dsid: adsid, authToken: xcodeToken, anisetteData: anisetteData)
let account = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAccount, any Error>) in
ALTAppleAPI.shared.fetchAccount2(session: session) { result in
c.resume(with: result)
}
}
return (account, session)
}
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{
self.appleIDEmailAddress = appleID
@@ -502,8 +444,6 @@ private extension AuthenticationOperation
verificationHandler: verificationHandler) { (account, session, error) in
if let account = account, let session = session
{
Keychain.shared.appleIDAdsid = session.dsid
Keychain.shared.appleIDXcodeToken = session.authToken
completionHandler(.success((account, session)))
}
else
@@ -807,30 +747,3 @@ extension AuthenticationOperation
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
}
}
extension ALTAppleAPI {
func fetchAccount2(session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAccount, Error>) -> Void)
{
let url = URL(string: "viewDeveloper.action", relativeTo: self.baseURL)!
self.sendRequest(with: url, additionalParameters: nil, session: session, team: nil) { (responseDictionary, requestError) in
do
{
guard let responseDictionary = responseDictionary else { throw requestError ?? ALTAppleAPIError.unknown() }
guard let account = try self.processResponse(responseDictionary, parseHandler: { () -> Any? in
guard let dictionary = responseDictionary["developer"] as? [String: Any] else { return nil }
let account = ALTAccount(responseDictionary: dictionary)
return account
}, resultCodeHandler: nil) as? ALTAccount else {
throw ALTAppleAPIError.unknown()
}
completionHandler(.success(account))
} catch {
completionHandler(.failure(error))
}
}
}
}

View File

@@ -99,10 +99,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
self.finish(.failure(RefreshError(.noInstalledApps)))
return
}
if UserDefaults.standard.enableEMPforWireguard {
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 {

View File

@@ -38,7 +38,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
self.context = context
self.appName = app.name
self.bundleIdentifier = context.bundleIdentifier
self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = destinationURL
@@ -77,7 +77,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
guard let latestVersion = storeApp.latestAvailableVersion else {
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
throw OperationError.unknown(failureReason: failureReason)
}
}
// Attempt to download latest _available_ version, and fall back to older versions if necessary.
appVersion = latestVersion
@@ -99,8 +99,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
if let installedApp = storeApp.installedApp
{
// guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
guard installedApp.hasUpdate else { return self.finish(.failure(error)) }
guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
}
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
@@ -224,6 +223,12 @@ private extension DownloadAppOperation
fileURL = sourceURL
self.progress.completedUnitCount += 3
}
else if let host = sourceURL.host, host.lowercased().hasSuffix("patreon.com") && sourceURL.path.lowercased() == "/file"
{
// Patreon app
fileURL = try await downloadPatreonApp(from: sourceURL)
self.printWithTid("downloadPatreonApp: completed at \(fileURL.path)")
}
else
{
// Regular app
@@ -317,6 +322,107 @@ private extension DownloadAppOperation
self.printWithTid("download started: \(downloadURL)")
}
}
func downloadPatreonApp(from patreonURL: URL) async throws -> URL
{
guard !UserDefaults.shared.skipPatreonDownloads else {
// Skip all hacks, take user straight to Patreon post.
return try await downloadFromPatreonPost()
}
do
{
// User is pledged to this app, attempt to download.
let fileURL = try await downloadFile(from: patreonURL)
return fileURL
}
catch URLError.noPermissionsToReadFile
{
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
// Attempt to sign-in again in case our Patreon session has expired.
try await withCheckedThrowingContinuation { continuation in
PatreonAPI.shared.authenticate(presentingViewController: presentingViewController) { result in
do
{
let account = try result.get()
try account.managedObjectContext?.save()
continuation.resume()
}
catch
{
continuation.resume(throwing: error)
}
}
}
do
{
// Success, so try to download once more now that we're definitely authenticated.
let fileURL = try await downloadFile(from: patreonURL)
return fileURL
}
catch URLError.noPermissionsToReadFile
{
// We know authentication succeeded, so failure must mean user isn't patron/on the correct tier,
// or that our hacky workaround for downloading Patreon attachments has failed.
// Either way, taking them directly to the post serves as a decent fallback.
return try await downloadFromPatreonPost()
}
}
func downloadFromPatreonPost() async throws -> URL
{
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
let downloadURL: URL
if let components = URLComponents(url: patreonURL, resolvingAgainstBaseURL: false),
let postItem = components.queryItems?.first(where: { $0.name == "h" }),
let postID = postItem.value,
let patreonPostURL = URL(string: "https://www.patreon.com/posts/" + postID)
{
downloadURL = patreonPostURL
}
else
{
downloadURL = patreonURL
}
return try await downloadFromPatreon(downloadURL, presentingViewController: presentingViewController)
}
}
@MainActor
func downloadFromPatreon(_ patreonURL: URL, presentingViewController: UIViewController) async throws -> URL
{
let webViewController = WebViewController(url: patreonURL)
webViewController.delegate = self
webViewController.webView.navigationDelegate = self
let navigationController = UINavigationController(rootViewController: webViewController)
presentingViewController.present(navigationController, animated: true)
let downloadURL: URL
do
{
defer {
navigationController.dismiss(animated: true)
}
downloadURL = try await withCheckedThrowingContinuation { continuation in
self.downloadPatreonAppContinuation = continuation
}
}
let fileURL = try await downloadFile(from: downloadURL)
return fileURL
}
}
}

View File

@@ -201,7 +201,7 @@ struct OperationError: ALTLocalizedError {
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID. Please replace your pairing using iloader.", comment: "")
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .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: "")
@@ -220,16 +220,16 @@ struct OperationError: ALTLocalizedError {
case .openAppFailed:
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
case .noWiFi: return NSLocalizedString("You do not appear to be connected to Wi-Fi and/or LocalDevVPN!\nSideStore cannot install or refresh applications without Wi-Fi and LocalDevVPN. If both are connected, replace your pairing with iloader.", comment: "")
case .tooNewError: return NSLocalizedString("iOS 17.0-17.3.1 changed how JIT is enabled so SideStore cannot enable JIT without SideJITServer on these versions, sorry for any inconvenience.", comment: "")
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer. Please check that you are on the same Wi-Fi of and your Firewall has been set correctly on your server.", comment: "")
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice. Please make sure you have paired your iDevice by running 'SideJITServer -y', or try refreshing SideJITServer from Settings.", comment: "")
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP. Please make sure that you are on the same Wi-Fi as SideJITServer", comment: "")
case .refreshsidejit: return NSLocalizedString("Unable to find app; Please try refreshing SideJITServer from Settings.", comment: "")
case .anisetteV1Error: return NSLocalizedString("An error occurred while getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
case .provisioningError: return NSLocalizedString("An error occurred while provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .anisetteV3Error: return NSLocalizedString("An error occurred while 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 the cache: %@", comment: "")
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
case .refreshAppFailed:
@@ -238,10 +238,10 @@ struct OperationError: ALTLocalizedError {
case .invalidParameters:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message)
case .invalidOperationContext:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
return String(format: NSLocalizedString("Invalid Operation Context%@", comment: ""), message)
return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message)
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
@@ -260,7 +260,7 @@ struct OperationError: ALTLocalizedError {
var recoverySuggestion: String? {
switch self.code
{
case .noWiFi: return NSLocalizedString("Make sure LocalDevVPN is connected and that you are connected to any Wi-Fi network!", comment: "")
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
case .serverNotFound: return NSLocalizedString("Make sure you're on the same Wi-Fi network as a computer running AltServer, or try connecting this device to your computer via USB.", comment: "")
case .maximumAppIDLimitReached:
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
@@ -308,9 +308,9 @@ extension MinimuxerError: LocalizedError {
case .NoDevice:
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
case .NoConnection:
return NSLocalizedString("Unable to connect to the device, make sure LocalDevVPN is enabled and you're connected to Wi-Fi. This could mean an invalid pairing.", comment: "")
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi. This could mean an invalid pairing.", comment: "")
case .PairingFile:
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use iloader to replace it.", comment: "")
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 .CreateDebug:
return self.createService(name: "debug")
@@ -338,7 +338,7 @@ extension MinimuxerError: LocalizedError {
case .CreateAfc:
return self.createService(name: "AFC")
case .RwAfc:
return NSLocalizedString("AFC was unable to manage files on the device. Ensure Wi-Fi and LocalDevVPN are connected. If they both are, replace your pairing using iloader.", comment: "")
return NSLocalizedString("AFC was unable to manage files on the device. This usually means an invalid pairing.", comment: "")
case .InstallApp(let message):
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
case .UninstallApp:
@@ -350,38 +350,6 @@ extension MinimuxerError: LocalizedError {
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .ProfileRemove:
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .CreateLockdown:
return NSLocalizedString("Unable to connect to lockdown", comment: "")
case .CreateCoreDevice:
return NSLocalizedString("Unable to connect to core device proxy", comment: "")
case .CreateSoftwareTunnel:
return NSLocalizedString("Unable to create software tunnel", comment: "")
case .CreateRemoteServer:
return NSLocalizedString("Unable to connect to remote server", comment: "")
case .CreateProcessControl:
return NSLocalizedString("Unable to connect to process control", comment: "")
case .GetLockdownValue:
return NSLocalizedString("Unable to get value from lockdown", comment: "")
case .Connect:
return NSLocalizedString("Unable to connect to TCP port", comment: "")
case .Close:
return NSLocalizedString("Unable to close TCP port", comment: "")
case .XpcHandshake:
return NSLocalizedString("Unable to get services from XPC", comment: "")
case .NoService:
return NSLocalizedString("Device did not contain service", comment: "")
case .InvalidProductVersion:
return NSLocalizedString("Service version was in an unexpected format", comment: "")
case .CreateFolder:
return NSLocalizedString("Unable to create DDI folder", comment: "")
case .DownloadImage:
return NSLocalizedString("Unable to download DDI", comment: "")
case .ImageLookup:
return NSLocalizedString("Unable to lookup DDI images", comment: "")
case .ImageRead:
return NSLocalizedString("Unable to read images to memory", comment: "")
case .Mount:
return NSLocalizedString("Mount failed", comment: "")
}
}

View File

@@ -45,14 +45,8 @@ extension VerificationError
VerificationError(code: .mismatchedHash, app: app, hash: hash, expectedHash: expectedHash)
}
static func mismatchedVersion(version: String,
expectedVersion: String,
app: AppProtocol) -> VerificationError
{
VerificationError(code: .mismatchedVersion, app: app,
version: version,
expectedVersion: expectedVersion
)
static func mismatchedVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
VerificationError(code: .mismatchedVersion, app: app, version: version, expectedVersion: expectedVersion)
}
static func mismatchedBuildVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
@@ -161,11 +155,11 @@ struct VerificationError: ALTLocalizedError
case .mismatchedVersion:
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
return String(format: NSLocalizedString("The downloaded version of %@ does not match the version specified by the source.\nExpected version: %@\nFound version: %@", comment: ""), appName, expectedVersion ?? "nil", version ?? "nil")
return String(format: NSLocalizedString("The downloaded version of %@ does not match the version specified by the source.", comment: ""), appName)
case .mismatchedBuildVersion:
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
return String(format: NSLocalizedString("The downloaded version of %@ does not match the build number specified by the source.\nExpected version: %@\nFound version: %@", comment: ""), appName, expectedVersion ?? "nil", version ?? "nil")
return String(format: NSLocalizedString("The downloaded version of %@ does not match the build number specified by the source.", comment: ""), appName)
case .undeclaredPermissions:
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")

View File

@@ -198,7 +198,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.timeZone = TimeZone.init(secondsFromGMT: 0)
formatter.timeZone = TimeZone.current
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
let dateString = formatter.string(from: Date())
formattedJSON["date"] = dateString

View File

@@ -54,8 +54,7 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
Logger.sideload.notice("Fetching provisioning profiles for app \(self.context.bundleIdentifier, privacy: .public)...")
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
let effectiveBundleId = self.context.bundleIdentifier
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
do
{
@@ -63,27 +62,25 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
let profile = try result.get()
var profiles = [effectiveBundleId: profile]
var profiles = [app.bundleIdentifier: profile]
var error: Error?
let dispatchGroup = DispatchGroup()
if !self.context.useMainProfile {
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
self.progress.completedUnitCount += 1
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
self.progress.completedUnitCount += 1
}
}
@@ -122,36 +119,6 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
return value
}
}
internal func fetchProvisioningProfile(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure:
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
// So instead, we just return the fetched profile from above.
completionHandler(.success(profile))
case .success:
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}
extension FetchProvisioningProfilesOperation
@@ -221,30 +188,19 @@ extension FetchProvisioningProfilesOperation
// Or, if the app _is_ installed but with a different team, we need to create a new
// bundle identifier anyway to prevent collisions with the previous team.
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
let effectiveParentBundleID = self.context.bundleIdentifier
let updatedParentBundleID: String
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore (and its extensions).
updatedParentBundleID = effectiveParentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
else
{
updatedParentBundleID = effectiveParentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
if let parentApp = parentApp,
app.bundleIdentifier.hasPrefix(parentBundleID + ".")
{
let suffix = String(app.bundleIdentifier.dropFirst(parentBundleID.count))
bundleID = updatedParentBundleID + suffix
}
else
{
bundleID = updatedParentBundleID
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
bundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
let preferredName: String
@@ -267,7 +223,7 @@ extension FetchProvisioningProfilesOperation
//process
self.fetchProvisioningProfile(
for: appID, app: app, team: team, session: session, completionHandler: completionHandler
for: appID, team: team, session: session, completionHandler: completionHandler
)
}
}
@@ -372,6 +328,43 @@ extension FetchProvisioningProfilesOperation
}
}
}
internal func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure:
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
// So instead, we just return the fetched profile from above.
completionHandler(.success(profile))
case .success:
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
@@ -381,8 +374,8 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
}
// modify Operations are allowed for the app groups and other stuffs
override func fetchProvisioningProfile(for appID: ALTAppID,
app: ALTApplication,
func fetchProvisioningProfile(appID: ALTAppID,
for app: ALTApplication,
team: ALTTeam,
session: ALTAppleAPISession,
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
@@ -403,7 +396,7 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
case .success(let appID):
// Fetch Provisioning Profile
super.fetchProvisioningProfile(for: appID, app: app, team: team, session: session, completionHandler: completionHandler)
super.fetchProvisioningProfile(for: appID, team: team, session: session, completionHandler: completionHandler)
}
}
}
@@ -418,6 +411,11 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
entitlements[key] = value
}
for (key, value) in [ALTEntitlement.increasedMemoryLimit : true]
{
entitlements[key] = value
}
let requiredFeatures = entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
return (feature, value)
@@ -620,14 +618,3 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
}
}
}
// <TEST> : users were reporting that refresh (though seemed like it refreshed the app becomes no longer available)
// possibly, this is caused since refesh was not updating appFeatures and AppGroups in the new profile? not sure.
// for now we are reverting by keeping same operation that happens during fetch in install path to see if it fixes issue #893
// class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesInstallOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}

View File

@@ -11,7 +11,6 @@ import CoreData
import AltStoreCore
import Roxas
import SemanticVersion
@objc(FetchSourceOperation)
final class FetchSourceOperation: ResultOperation<Source>
@@ -177,6 +176,7 @@ final class FetchSourceOperation: ResultOperation<Source>
}
try self.verify(source, response: response)
try self.verifyPledges(for: source, in: childContext)
try childContext.save()
@@ -246,20 +246,66 @@ private extension FetchSourceOperation
#endif
}
let incomingSourceID = source.identifier
if let previousSourceID = self.$source.identifier,
incomingSourceID != previousSourceID
if let previousSourceID = self.$source.identifier
{
// if let version = BuildInfo().marketing_version,
// SemanticVersion(version)! <= SemanticVersion("0.6.1")!
// {
// // delete the source, so that incoming will be saved.
// self.source?.managedObjectContext?.delete(self.source!)
// }
// else
// {
throw SourceError.changedID(source.identifier, previousID: self.$source.identifier ?? "nil", source: source)
// }
guard source.identifier == previousSourceID else { throw SourceError.changedID(source.identifier, previousID: previousSourceID, source: source) }
}
}
func verifyPledges(for source: Source, in context: NSManagedObjectContext) throws
{
guard let patreonURL = source.patreonURL, let patreonAccount = DatabaseManager.shared.patreonAccount(in: context) else { return }
let normalizedPatreonURL = try patreonURL.normalized()
guard let pledge = patreonAccount.pledges.first(where: { pledge in
do
{
let normalizedCampaignURL = try pledge.campaignURL.normalized()
return normalizedCampaignURL == normalizedPatreonURL
}
catch
{
Logger.main.error("Failed to normalize Patreon URL \(pledge.campaignURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
return false
}
}) else { return }
// User is pledged to this source's Patreon, so check which apps they're pledged to.
// We only assign `isPledged = true` because false is already the default,
// and only one check needs to be true for isPledged to be true.
for app in source.apps where app.isPledgeRequired
{
if let requiredAppPledge = app.pledgeAmount
{
if pledge.amount >= requiredAppPledge
{
app.isPledged = true
continue
}
}
if let tierIDs = app._tierIDs
{
let tier = pledge.tiers.first { tierIDs.contains($0.identifier) }
if tier != nil
{
app.isPledged = true
continue
}
}
if let rewardID = app._rewardID
{
let reward = pledge.rewards.first { $0.identifier == rewardID }
if reward != nil
{
app.isPledged = true
continue
}
}
}
}

View File

@@ -72,8 +72,6 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
}
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber, storeBuildVersion: storeBuildVersion)
installedApp.useMainProfile = self.context.useMainProfile
installedApp.needsResign = false
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
@@ -98,22 +96,22 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
let resignedParentBundleID = resignedApp.bundleIdentifier
let resignedBundleID = appExtension.bundleIdentifier
let appExBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
print("`parentBundleID`: \(parentBundleID)")
print("`resignedParentBundleID`: \(resignedParentBundleID)")
print("`appExBundleID`: \(appExBundleID)")
print("`resignedAppExBundleID`: \(resignedBundleID)")
print("`resignedBundleID`: \(resignedBundleID)")
print("`originalBundleID`: \(originalBundleID)")
let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == appExBundleID })
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
{
installedExtension = appExtension
}
else
{
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: appExBundleID, context: backgroundContext)
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
}
installedExtension.update(resignedAppExtension: appExtension)

View File

@@ -66,8 +66,6 @@ class AppOperationContext
var app: ALTApplication?
var provisioningProfiles: [String: ALTProvisioningProfile]?
var appexBundleIds: [String: String]?
var useMainProfile = false
var isFinished = false

View File

@@ -55,23 +55,8 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
try FileManager.default.removeItem(at: appExtension.fileURL)
}
}
private func updateManifest() throws {
guard let app = context.app else {
return
}
let scInfoURL = app.fileURL.appendingPathComponent("SC_Info")
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
if let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL),
let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String]
{
let replacementPaths = sinfReplicationPaths.filter { !$0.starts(with: "PlugIns/") } // Filter out app extension paths.
manifestPlist["SinfReplicationPaths"] = replacementPaths
try manifestPlist.write(to: manifestPlistURL)
}
}
private func removeAppExtensions(from targetAppBundle: ALTApplication,
localAppExtensions: Set<ALTApplication>?,
@@ -100,7 +85,7 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
presentingViewController.present(alertController, animated: true){
// if for any reason the view wasn't presented, then just signal that as error
if presentingViewController.presentedViewController == nil && !alertController.isViewLoaded {
if presentingViewController.presentedViewController == nil {
let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." +
"\nDid you move to different screen or background after starting the operation?"
self.finish(.failure(
@@ -136,17 +121,12 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
self.finish(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions (Use Main Profile)", comment: ""), style: .default) { (action) in
self.context.useMainProfile = true
self.finish(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions (Register App ID for Each Extension)", comment: ""), style: .default) { (action) in
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
self.finish(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
do {
try Self.removeExtensions(from: targetAppBundle.appExtensions)
try self.updateManifest()
return self.finish(.success(()))
} catch {
return self.finish(.failure(error))

View File

@@ -55,8 +55,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let effectiveBundleId = self.context.bundleIdentifier
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles, appexBundleIds: context.appexBundleIds ?? [:]) { (result) in
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
guard let appBundleURL = self.process(result) else { return }
// Resign app bundle
@@ -66,13 +65,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
// Finish
do
{
let updatedApp = AnyApp(
name: app.name,
bundleIdentifier: effectiveBundleId,
url: app.fileURL,
storeApp: app.storeApp
)
let destinationURL = InstalledApp.refreshedIPAURL(for: updatedApp)
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
print("Successfully resigned app to \(destinationURL.absoluteString)")
@@ -114,26 +107,22 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
private extension ResignAppOperation
{
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], appexBundleIds: [String: String], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
let bundleIdentifier = context.bundleIdentifier
let bundleIdentifier = app.bundleIdentifier
let openURL = InstalledApp.openAppURL(for: app)
let fileURL = app.fileURL
func prepare(_ bundle: Bundle, bundleID identifier: String?, additionalInfoDictionaryValues: [String: Any] = [:]) throws
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
{
guard let identifier else { throw ALTError(.missingAppBundle) }
guard let profile = context.useMainProfile ? profiles.values.first : profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
guard let identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
guard let profile = profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
if let forcedBundleIdentifier = appexBundleIds[identifier] {
infoDictionary[kCFBundleIdentifierKey as String] = forcedBundleIdentifier
} else {
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
}
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.altBundleID] = identifier
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>"
infoDictionary.removeValue(forKey: "DTXcode")
@@ -204,7 +193,7 @@ private extension ResignAppOperation
if app.isAltStoreApp
{
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID }
guard Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) is 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.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
@@ -248,7 +237,7 @@ private extension ResignAppOperation
}
// Prepare app
try prepare(appBundle, bundleID: bundleIdentifier, 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])
@@ -265,8 +254,7 @@ private extension ResignAppOperation
#endif
guard let appExtension = Bundle(url: fileURL) else { throw ALTError(.missingAppBundle) }
let updatedAppExBundleId = appExtension.bundleIdentifier?.replacingOccurrences(of: app.bundleIdentifier, with: bundleIdentifier)
try prepare(appExtension, bundleID: updatedAppExBundleId)
try prepare(appExtension)
}
}

View File

@@ -38,13 +38,11 @@ final class VerifyAppOperation: ResultOperation<Void>
{
let permissionsMode: PermissionReviewMode
let context: InstallAppOperationContext
var customBundleId: String?
init(permissionsMode: PermissionReviewMode, context: InstallAppOperationContext, customBundleId: String? = nil)
init(permissionsMode: PermissionReviewMode, context: InstallAppOperationContext)
{
self.permissionsMode = permissionsMode
self.context = context
self.customBundleId = customBundleId
super.init()
}
@@ -67,8 +65,7 @@ final class VerifyAppOperation: ResultOperation<Void>
}
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
let bundleId = customBundleId ?? app.bundleIdentifier
guard bundleId == self.context.bundleIdentifier else {
guard app.bundleIdentifier == self.context.bundleIdentifier else {
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
}
}
@@ -85,16 +82,17 @@ final class VerifyAppOperation: ResultOperation<Void>
do
{
guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) }
// TODO: @mahee96: appVersion is instantiated source info as AppVersion incoming from source json
// app is the instantiated ipa downloaded from the specified in the source json in temp dir
//
// For alpha and beta/nightly releases, the CFBundleShortVersionString which is the
// $(MARKETING_VERSION) will be overriden with the commit id before invoking xcode build
//
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
// process missing permissions check only if the source is V2 or later
if let source = appVersion.app?.source,
source.isSourceAtLeastV2
{
try await self.verifyPermissions(of: app, match: appVersion)
}
try await self.verifyPermissions(of: app, match: appVersion)
self.finish(.success(()))
}
@@ -131,17 +129,24 @@ private extension VerifyAppOperation
{
let (version, buildVersion) = await $appVersion.perform { ($0.version, $0.buildVersion) }
// marketplace buildVersion validation
if let buildVersion
{
guard buildVersion == app.buildVersion else {
throw VerificationError.mismatchedBuildVersion(app.buildVersion, expectedVersion: buildVersion, app: app)
}
}
let downloadedIpaRevision = Bundle(url: app.fileURL)!.object(forInfoDictionaryKey: "BuildRevision") as? String ?? ""
let sourceJsonIpaRevision = appVersion.revision
// if not beta but version matches, then accept it, else compare revisions between source and downloaded
if version != app.version {
throw VerificationError.mismatchedVersion(version: app.version, expectedVersion: version, app: app)
throw VerificationError.mismatchedVersion(app.version, expectedVersion: version, app: app)
}
if (appVersion.isBeta && downloadedIpaRevision != sourceJsonIpaRevision) {
let sourceJsonIpaRevision = sourceJsonIpaRevision ?? "nil"
throw VerificationError.mismatchedVersion(app.version + " - " + downloadedIpaRevision,
expectedVersion: version + " - " + sourceJsonIpaRevision, app: app)
}
// if let buildVersion
// {
// // TODO: @mahee96: requires altsign-marketplace branch release or equivalent
// guard buildVersion == app.buildVersion else { throw VerificationError.mismatchedBuildVersion(app.buildVersion, expectedVersion: buildVersion, app: app) }
// }
}
func verifyPermissions(of app: ALTApplication, @AsyncManaged match appVersion: AppVersion) async throws

View File

@@ -0,0 +1,281 @@
//
// VerifyAppPledgeOperation.swift
// AltStore
//
// Created by Riley Testut on 12/6/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Combine
import AltStoreCore
class VerifyAppPledgeOperation: ResultOperation<Void>
{
@AsyncManaged
private(set) var storeApp: StoreApp
private let presentingViewController: UIViewController?
private var openPatreonPageContinuation: CheckedContinuation<Void, Never>?
private var cancellable: AnyCancellable?
init(storeApp: StoreApp, presentingViewController: UIViewController?)
{
self.storeApp = storeApp
self.presentingViewController = presentingViewController
}
override func main()
{
super.main()
// _Don't_ rethrow earlier errors, or else user will only be taken to Patreon post if connected to same Wi-Fi as AltServer.
// if let error = self.context.error
// {
// self.finish(.failure(error))
// return
// }
Task<Void, Never>.detached(priority: .medium) {
do
{
guard await self.$storeApp.isPledgeRequired else { return self.finish(.success(())) }
if let presentingViewController = self.presentingViewController
{
// Ask user to connect Patreon account if they are signed-in to Patreon inside WebViewController, but haven't yet signed in through AltStore settings.
// This is most likely because the user joined a Patreon campaign directly through WebViewController before connecting Patreon account in settings.
try await self.connectPatreonAccountIfNeeded(presentingViewController: presentingViewController)
}
do
{
try await self.verifyPledge()
}
catch let error as OperationError where error.code == .pledgeRequired || error.code == .pledgeInactive
{
guard
let presentingViewController = self.presentingViewController,
let source = await self.$storeApp.source,
let patreonURL = await self.$storeApp.perform({ _ in source.patreonURL })
else { throw error }
let components = URLComponents(url: patreonURL, resolvingAgainstBaseURL: false)
let lastPathComponent = components?.path.components(separatedBy: "/").last
let username = lastPathComponent ?? patreonURL.lastPathComponent
let checkoutURL: URL
if await self.$storeApp.prefersCustomPledge, let customPledgeURL = URL(string: "https://www.patreon.com/checkout/" + username + "?rid=0&custom=1")
{
checkoutURL = customPledgeURL
let action = await UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default)
try await presentingViewController.presentConfirmationAlert(title: NSLocalizedString("Custom Pledge", comment: ""),
message: NSLocalizedString("This app supports custom pledges. Pledge any amount on Patreon to receive access.", comment: ""),
primaryAction: action)
}
else if !username.isEmpty, let url = URL(string: "https://www.patreon.com/join/" + username)
{
// Prefer /join URL over campaign homepage.
// URL format from https://support.patreon.com/hc/en-us/articles/360044376211-Managing-members-with-custom-pledges
checkoutURL = url
}
else
{
checkoutURL = patreonURL
}
// Direct user to Patreon page if they're not already pledged.
await self.openPatreonPage(checkoutURL, presentingViewController: presentingViewController)
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
if let patreonAccount = await context.performAsync({ DatabaseManager.shared.patreonAccount(in: context) })
{
// Patreon account is connected, so we'll update it via API to see if pledges changed.
// If so, we'll re-fetch the source to update pledge statuses.
try await self.updatePledges(for: source, account: patreonAccount)
}
else
{
// Patreon account is not connected, so prompt user to connect it.
try await self.connectPatreonAccountIfNeeded(presentingViewController: presentingViewController)
}
do
{
try await self.verifyPledge()
}
catch
{
// Ignore error, but cancel remainder of operation.
throw CancellationError()
}
}
self.finish(.success(()))
}
catch
{
self.finish(.failure(error))
}
}
}
}
private extension VerifyAppPledgeOperation
{
func verifyPledge() async throws
{
let (appName, isPledged) = await self.$storeApp.perform { ($0.name, $0.isPledged) }
if !PatreonAPI.shared.isAuthenticated || !isPledged
{
let isInstalled = await self.$storeApp.installedApp != nil
if isInstalled
{
// Assume if there is an InstalledApp, the user had previously pledged to this app.
throw OperationError.pledgeInactive(appName: appName)
}
else
{
throw OperationError.pledgeRequired(appName: appName)
}
}
}
func connectPatreonAccountIfNeeded(presentingViewController: UIViewController) async throws
{
guard !PatreonAPI.shared.isAuthenticated, let authCookie = PatreonAPI.shared.authCookies.first(where: { $0.name.lowercased() == "session_id" }) else { return }
Logger.sideload.debug("Patreon Auth cookie: \(authCookie.name)=\(authCookie.value)")
let message = NSLocalizedString("You're signed into Patreon but haven't connected your account with SideStore.\n\nPlease connect your account to download Patreon-exclusive apps.", comment: "")
let action = await UIAlertAction(title: NSLocalizedString("Connect Patreon Account", comment: ""), style: .default)
do
{
_ = try await presentingViewController.presentConfirmationAlert(title: NSLocalizedString("Patreon Account Detected", comment: ""),
message: message, actions: [action])
}
catch
{
// Ignore and continue
return
}
try await withCheckedThrowingContinuation { continuation in
PatreonAPI.shared.authenticate(presentingViewController: presentingViewController) { result in
do
{
let account = try result.get()
try account.managedObjectContext?.save()
continuation.resume()
}
catch
{
continuation.resume(throwing: error)
}
}
}
if let source = await self.$storeApp.source
{
// Fetch source to update pledge status now that account is connected.
try await self.update(source)
}
}
func updatePledges(@AsyncManaged for source: Source, @AsyncManaged account: PatreonAccount) async throws
{
guard PatreonAPI.shared.isAuthenticated else { return }
let previousPledgeIDs = Set(await $account.perform { $0.pledges.map(\.identifier) })
let updatedPledgeIDs = try await withCheckedThrowingContinuation { continuation in
PatreonAPI.shared.fetchAccount { (result: Result<PatreonAccount, Swift.Error>) in
do
{
let account = try result.get()
let pledgeIDs = Set(account.pledges.map(\.identifier))
try account.managedObjectContext?.save()
continuation.resume(returning: pledgeIDs)
}
catch
{
Logger.sideload.error("Failed to update Patreon account. \(error.localizedDescription, privacy: .public)")
continuation.resume(throwing: error)
}
}
}
if updatedPledgeIDs != previousPledgeIDs
{
// Active pledges changed, so fetch source to update pledge status.
try await self.update(source)
}
}
func update(@AsyncManaged _ source: Source) async throws
{
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
_ = try await AppManager.shared.fetchSource(sourceURL: $source.sourceURL, managedObjectContext: context)
try await context.performAsync {
try context.save()
}
}
@MainActor
func openPatreonPage(_ patreonURL: URL, presentingViewController: UIViewController) async
{
let webViewController = WebViewController(url: patreonURL)
webViewController.delegate = self
let navigationController = UINavigationController(rootViewController: webViewController)
presentingViewController.present(navigationController, animated: true)
// Automatically dismiss if user completes checkout flow.
self.cancellable = webViewController.webView.publisher(for: \.url, options: [.new])
.compactMap { $0 }
.compactMap { URLComponents(url: $0, resolvingAgainstBaseURL: false) }
.compactMap { components in
let lastPathComponent = components.path.components(separatedBy: "/").last
return lastPathComponent?.lowercased()
}
.filter { $0 == "membership" }
.receive(on: RunLoop.main)
.sink { [weak self] url in
guard let continuation = self?.openPatreonPageContinuation else { return }
self?.openPatreonPageContinuation = nil
continuation.resume()
}
await withCheckedContinuation { continuation in
self.openPatreonPageContinuation = continuation
}
// Cache auth cookies just in case user signed in.
await PatreonAPI.shared.saveAuthCookies()
navigationController.dismiss(animated: true)
self.cancellable = nil
}
}
extension VerifyAppPledgeOperation: WebViewControllerDelegate
{
func webViewControllerDidFinish(_ webViewController: WebViewController)
{
guard let continuation = self.openPatreonPageContinuation else { return }
self.openPatreonPageContinuation = nil
continuation.resume()
}
}

View File

@@ -8,8 +8,6 @@
<key>name</key>
<string>Original</string>
<key>imageName</key>
<string>App</string>
<key>iconName</key>
<string>AppIcon</string>
</dict>
</array>
@@ -20,88 +18,66 @@
<string>Blue</string>
<key>imageName</key>
<string>Blue</string>
<key>iconName</key>
<string>BlueIcon</string>
</dict>
<dict>
<key>name</key>
<string>Dark</string>
<key>imageName</key>
<string>Dark</string>
<key>iconName</key>
<string>DarkIcon</string>
</dict>
<dict>
<key>name</key>
<string>Honeydew</string>
<key>imageName</key>
<string>Honeydew</string>
<key>iconName</key>
<string>HoneydewIcon</string>
</dict>
<dict>
<key>name</key>
<string>Pride</string>
<key>imageName</key>
<string>Pride</string>
<key>iconName</key>
<string>PrideIcon</string>
</dict>
<dict>
<key>name</key>
<string>Sandy</string>
<key>imageName</key>
<string>Sandy</string>
<key>iconName</key>
<string>SandyIcon</string>
</dict>
<dict>
<key>name</key>
<string>Sky</string>
<key>imageName</key>
<string>Sky</string>
<key>iconName</key>
<string>SkyIcon</string>
</dict>
<dict>
<key>name</key>
<string>Snow</string>
<key>imageName</key>
<string>Snow</string>
<key>iconName</key>
<string>SnowIcon</string>
</dict>
<dict>
<key>name</key>
<string>Starburst</string>
<key>imageName</key>
<string>Starburst</string>
<key>iconName</key>
<string>StarburstIcon</string>
</dict>
<dict>
<key>name</key>
<string>Storm</string>
<key>imageName</key>
<string>Storm</string>
<key>iconName</key>
<string>StormIcon</string>
</dict>
<dict>
<key>name</key>
<string>Vista</string>
<key>imageName</key>
<string>Vista</string>
<key>iconName</key>
<string>VistaIcon</string>
</dict>
<dict>
<key>name</key>
<string>Winter</string>
<key>imageName</key>
<string>Winter</string>
<key>iconName</key>
<string>WinterIcon</string>
</dict>
</array>
</dict>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

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

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

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

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 719 KiB

After

Width:  |  Height:  |  Size: 719 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 352 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 453 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 464 KiB

After

Width:  |  Height:  |  Size: 464 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

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

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