Compare commits
23 Commits
a6be43da53
...
users/june
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbce8725c7 | ||
|
|
8cfbbf8768 | ||
|
|
b3357049b0 | ||
|
|
a7f01956e8 | ||
|
|
1cc0b11aa6 | ||
|
|
9a598f701a | ||
|
|
af26262b6b | ||
|
|
9ece114f7a | ||
|
|
ed0f83d0cb | ||
|
|
2a8e1d8888 | ||
|
|
61077fa5b1 | ||
|
|
7fd9639351 | ||
|
|
eb06d05d63 | ||
|
|
853de98326 | ||
|
|
bb9760f31d | ||
|
|
60411cabb4 | ||
|
|
e07c4aac37 | ||
|
|
a9bef300a0 | ||
|
|
2024e0ad32 | ||
|
|
3527a4b8bc | ||
|
|
41c7c161d6 | ||
|
|
c3a199c2d0 | ||
|
|
ea00f2904b |
63
.github/maintenance/cache.py
vendored
@@ -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
|
|
||||||
'''
|
|
||||||
300
.github/workflows/alpha.yml
vendored
@@ -2,27 +2,283 @@ name: Alpha SideStore build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop-alpha
|
# - alpha
|
||||||
|
- rebase-2.0-wip
|
||||||
# cancel duplicate run if from same branch
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Reusable-build:
|
build:
|
||||||
uses: ./.github/workflows/reusable-sidestore-build.yml
|
name: Build and upload SideStore Alpha releases
|
||||||
with:
|
concurrency:
|
||||||
# bundle_id: "com.SideStore.SideStore.Alpha"
|
group: ${{ github.ref }}
|
||||||
bundle_id: "com.SideStore.SideStore"
|
cancel-in-progress: true
|
||||||
# bundle_id_suffix: ".Alpha"
|
strategy:
|
||||||
is_beta: true
|
fail-fast: false
|
||||||
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
|
matrix:
|
||||||
is_shared_build_num: false
|
include:
|
||||||
release_tag: "alpha"
|
- os: 'macos-14'
|
||||||
release_name: "Alpha"
|
version: '16.1'
|
||||||
upstream_tag: "nightly"
|
|
||||||
upstream_name: "Nightly"
|
runs-on: ${{ matrix.os }}
|
||||||
secrets:
|
steps:
|
||||||
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
|
||||||
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
- 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
|
||||||
|
|||||||
28
.github/workflows/increase-alpha-build-num.sh
vendored
Normal 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
|
||||||
|
|
||||||
34
.github/workflows/increase-beta-build-num.sh
vendored
@@ -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
|
|
||||||
28
.github/workflows/increase-nightly-build-num.sh
vendored
Normal 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
|
||||||
|
|
||||||
363
.github/workflows/nightly.yml
vendored
@@ -1,82 +1,317 @@
|
|||||||
name: Nightly SideStore Build
|
name: Nightly SideStore build
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- 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:
|
jobs:
|
||||||
check-changes:
|
build:
|
||||||
if: github.event_name == 'schedule'
|
name: Build and upload SideStore Nightly releases
|
||||||
runs-on: ubuntu-latest
|
concurrency:
|
||||||
outputs:
|
group: ${{ github.ref }}
|
||||||
has_changes: ${{ steps.check.outputs.has_changes }}
|
cancel-in-progress: true
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-14'
|
||||||
|
version: '16.1'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
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
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Ensure full history
|
submodules: recursive
|
||||||
|
|
||||||
- name: Get last successful workflow run
|
- name: Install dependencies
|
||||||
id: get_last_success
|
run: brew install ldid
|
||||||
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: Check for new commits since last successful build
|
- name: Install xcbeautify
|
||||||
id: check
|
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: |
|
run: |
|
||||||
if [ -n "$LAST_SUCCESS" ]; then
|
pod install
|
||||||
NEW_COMMITS=$(git rev-list --count --since="$LAST_SUCCESS" origin/develop)
|
|
||||||
COMMIT_LOG=$(git log --since="$LAST_SUCCESS" --pretty=format:"%h %s" origin/develop)
|
- name: Save Pods to Cache
|
||||||
else
|
id: save-pods
|
||||||
NEW_COMMITS=1
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
COMMIT_LOG=$(git log -n 10 --pretty=format:"%h %s" origin/develop) # Show last 10 commits if no history
|
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
|
fi
|
||||||
|
|
||||||
echo "Has changes: $NEW_COMMITS"
|
zip -e -P "$BUILD_LOG_ZIP_PASSWORD" encrypted-build_log.zip build.log
|
||||||
echo "New commits since last successful build:"
|
|
||||||
echo "$COMMIT_LOG"
|
- name: List Files after SideStore build
|
||||||
|
run: |
|
||||||
if [ "$NEW_COMMITS" -gt 0 ]; then
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
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
|
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
|
fi
|
||||||
env:
|
continue-on-error: true # Continue even if exit code is 1
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
LAST_SUCCESS: ${{ env.last_success }}
|
|
||||||
|
|
||||||
Reusable-build:
|
- name: Get short commit hash
|
||||||
if: |
|
if: ${{ steps.check_publish.outcome == 'success' }}
|
||||||
always() &&
|
run: |
|
||||||
(github.event_name == 'push' ||
|
# SHORT_COMMIT="${{ github.sha }}"
|
||||||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
|
SHORT_COMMIT=${GITHUB_SHA:0:7}
|
||||||
needs: check-changes
|
echo "Short commit hash: $SHORT_COMMIT"
|
||||||
uses: ./.github/workflows/reusable-sidestore-build.yml
|
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_ENV
|
||||||
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 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
|
||||||
|
|||||||
48
.github/workflows/pr.yml
vendored
@@ -1,13 +1,10 @@
|
|||||||
name: Pull Request SideStore build
|
name: Pull Request SideStore build
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and upload SideStore
|
name: Build and upload SideStore
|
||||||
if: ${{ github.event.pull_request.draft == false }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -54,12 +51,57 @@ jobs:
|
|||||||
swiftpm-cache-restore-keys: |
|
swiftpm-cache-restore-keys: |
|
||||||
xcode-cache-sourcedata-
|
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
|
- name: List Files and derived data
|
||||||
run: |
|
run: |
|
||||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
ls -la .
|
ls -la .
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||||
|
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
105
.github/workflows/reusable-sidestore-build.yml
vendored
@@ -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
|
|
||||||
358
.github/workflows/sidestore-build.yml
vendored
@@ -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
|
|
||||||
281
.github/workflows/sidestore-deploy.yml
vendored
@@ -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
|
|
||||||
24
.github/workflows/sidestore-shared.yml
vendored
@@ -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 }}
|
|
||||||
165
.github/workflows/sidestore-tests-build.yml
vendored
@@ -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
|
|
||||||
196
.github/workflows/sidestore-tests-run.yml
vendored
@@ -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
|
|
||||||
249
.github/workflows/stable.yml
vendored
@@ -7,236 +7,97 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build SideStore - stable (on tag push)
|
name: Build and upload SideStore
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-26'
|
- os: 'macos-14'
|
||||||
version: '26.0'
|
version: '15.4'
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Echo Build.xcconfig
|
- name: Install dependencies
|
||||||
run: |
|
run: brew install ldid
|
||||||
echo "cat Build.xcconfig"
|
|
||||||
cat Build.xcconfig
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
|
- name: Change version to tag
|
||||||
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||||
|
|
||||||
- name: Echo Updated Build.xcconfig
|
- name: Get version
|
||||||
run: |
|
|
||||||
cat Build.xcconfig
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Extract MARKETING_VERSION from Build.xcconfig
|
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
|
||||||
echo "version=$version" >> $GITHUB_OUTPUT
|
|
||||||
echo "version=$version"
|
|
||||||
|
|
||||||
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
|
- name: Echo version
|
||||||
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
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: Setup Xcode
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
- name: Cache Build
|
||||||
id: xcode-cache-restore
|
uses: irgaly/xcode-cache@v1
|
||||||
uses: actions/cache/restore@v3
|
|
||||||
with:
|
with:
|
||||||
path: |
|
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||||
~/Library/Developer/Xcode/DerivedData
|
restore-keys: xcode-cache-deriveddata-
|
||||||
~/Library/Caches/org.swift.swiftpm
|
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
||||||
key: xcode-cache-build-stable-${{ github.sha }}
|
swiftpm-cache-restore-keys: |
|
||||||
|
xcode-cache-sourcedata-
|
||||||
|
|
||||||
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
- name: Build SideStore
|
||||||
id: xcode-cache-restore-recent
|
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||||
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: Fakesign app
|
- name: Fakesign app
|
||||||
run: make fakesign | tee -a build/logs/build.log
|
run: make fakesign
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Convert to IPA
|
- name: Convert to IPA
|
||||||
run: make ipa | tee -a build/logs/build.log
|
run: make ipa
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: (Build) Save Xcode & SwiftPM Cache
|
- name: Get current date
|
||||||
id: cache-save
|
id: date
|
||||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
uses: actions/cache/save@v3
|
|
||||||
|
- 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:
|
with:
|
||||||
path: |
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
~/Library/Developer/Xcode/DerivedData
|
name: ${{ steps.version.outputs.version }}
|
||||||
~/Library/Caches/org.swift.swiftpm
|
tag_name: ${{ github.ref_name }}
|
||||||
key: xcode-cache-build-stable-${{ github.sha }}
|
draft: true
|
||||||
|
files: SideStore.ipa
|
||||||
- name: (Build) List Files and Build artifacts
|
body: |
|
||||||
run: |
|
<!-- 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. -->
|
||||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
## Changelog
|
||||||
ls -la .
|
|
||||||
echo ""
|
- 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 <<<<<<<<<<"
|
- name: Add version to IPA file name
|
||||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
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
|
- name: Upload SideStore.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
path: SideStore.ipa
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Zip dSYMs
|
|
||||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
- name: Upload *.dSYM Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: SideStore.dSYMs.zip
|
path: ./*.dSYM/
|
||||||
|
|
||||||
- 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 }}`
|
|
||||||
|
|||||||
8
.gitignore
vendored
@@ -63,10 +63,4 @@ SideStore/.skip-prebuilt-fetch-em_proxy
|
|||||||
# Never check-in this package.resolved file
|
# Never check-in this package.resolved file
|
||||||
# coz SPM then resolves packages using the stale entries in this file
|
# coz SPM then resolves packages using the stale entries in this file
|
||||||
*.xcodeproj/**/Package.resolved
|
*.xcodeproj/**/Package.resolved
|
||||||
*.xcworkspace/**/Package.resolved
|
*.xcworkspace/**/Package.resolved
|
||||||
|
|
||||||
# some more commandline build artifacts
|
|
||||||
test-recording.mp4
|
|
||||||
test-recording.log
|
|
||||||
altstore-sources.md
|
|
||||||
local-build.sh
|
|
||||||
6
.gitmodules
vendored
@@ -30,7 +30,7 @@
|
|||||||
url = https://github.com/rileytestut/Roxas.git
|
url = https://github.com/rileytestut/Roxas.git
|
||||||
[submodule "Dependencies/libimobiledevice"]
|
[submodule "Dependencies/libimobiledevice"]
|
||||||
path = Dependencies/libimobiledevice
|
path = Dependencies/libimobiledevice
|
||||||
url = https://github.com/SideStore/libimobiledevice
|
url = https://github.com/libimobiledevice/libimobiledevice
|
||||||
[submodule "Dependencies/libusbmuxd"]
|
[submodule "Dependencies/libusbmuxd"]
|
||||||
path = Dependencies/libusbmuxd
|
path = Dependencies/libusbmuxd
|
||||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
url = https://github.com/SideStore/minimuxer
|
url = https://github.com/SideStore/minimuxer
|
||||||
branch = master
|
branch = master
|
||||||
[submodule "SideStore/em_proxy"]
|
[submodule "SideStore/em_proxy"]
|
||||||
path = SideStore/em_proxy
|
path = SideStore/em_proxy
|
||||||
url = https://github.com/SideStore/em_proxy
|
url = https://github.com/SideStore/em_proxy
|
||||||
branch = master
|
branch = master
|
||||||
[submodule "SideStore/libfragmentzip"]
|
[submodule "SideStore/libfragmentzip"]
|
||||||
path = SideStore/libfragmentzip
|
path = SideStore/libfragmentzip
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(GROUP_ID)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>BuildRevision</key>
|
||||||
|
<string>$(BUILD_REVISION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -28,27 +28,18 @@
|
|||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<TestPlans>
|
<!-- shouldAutocreateTestPlan = "YES"> -->
|
||||||
<TestPlanReference
|
|
||||||
reference = "container:SideStore/Tests/SideStoreTests.xctestplan"
|
|
||||||
default = "YES">
|
|
||||||
</TestPlanReference>
|
|
||||||
</TestPlans>
|
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO">
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "A8E2DB202D684CBD009E5D31"
|
BlueprintIdentifier = "D586D39728EF58B0000E101F"
|
||||||
BuildableName = "UITests.xctest"
|
BuildableName = "AltTests.xctest"
|
||||||
BlueprintName = "UITests"
|
BlueprintName = "AltTests"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
<SelectedTests>
|
|
||||||
<Test
|
|
||||||
Identifier = "UITests/testExample()">
|
|
||||||
</Test>
|
|
||||||
</SelectedTests>
|
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
|
|||||||
3
AltStore.xcworkspace/contents.xcworkspacedata
generated
@@ -10,4 +10,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
|
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -2,14 +2,24 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>aps-environment</key>
|
<!-- <key>com.apple.security.files.user-selected.read-write</key>
|
||||||
<string>development</string>
|
<array>
|
||||||
|
<string></string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.applesignin</key>
|
||||||
|
<array>
|
||||||
|
<string></string>
|
||||||
|
</array> -->
|
||||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.siri</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
115
AltStore/Analytics/AnalyticsManager.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,10 +43,8 @@ final class AppContentViewController: UITableViewController
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
@IBOutlet private var subtitleLabel: UILabel!
|
@IBOutlet private var subtitleLabel: UILabel!
|
||||||
// @IBOutlet private var descriptionTextView: CollapsingTextView!
|
@IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||||
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
|
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||||
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
|
||||||
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
|
|
||||||
@IBOutlet private var versionLabel: UILabel!
|
@IBOutlet private var versionLabel: UILabel!
|
||||||
@IBOutlet private var versionDateLabel: UILabel!
|
@IBOutlet private var versionDateLabel: UILabel!
|
||||||
@IBOutlet private var sizeLabel: UILabel!
|
@IBOutlet private var sizeLabel: UILabel!
|
||||||
@@ -57,32 +55,35 @@ final class AppContentViewController: UITableViewController
|
|||||||
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
|
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
|
||||||
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
|
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.tableView.contentInset.bottom = 20
|
self.tableView.contentInset.bottom = 20
|
||||||
|
|
||||||
self.subtitleLabel.text = self.app.subtitle
|
self.subtitleLabel.text = self.app.subtitle
|
||||||
let desc = self.app.localizedDescription
|
self.descriptionTextView.text = self.app.localizedDescription
|
||||||
self.descriptionTextView.text = desc
|
|
||||||
|
|
||||||
if let version = self.app.latestAvailableVersion {
|
if let version = self.app.latestAvailableVersion
|
||||||
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
|
{
|
||||||
|
self.versionDescriptionTextView.text = version.localizedDescription
|
||||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
||||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
||||||
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
||||||
} else {
|
}
|
||||||
self.versionDescriptionTextView.text = "nil"
|
else
|
||||||
|
{
|
||||||
|
self.versionDescriptionTextView.text = nil
|
||||||
self.versionLabel.text = nil
|
self.versionLabel.text = nil
|
||||||
self.versionDateLabel.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.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.maximumNumberOfLines = 3
|
||||||
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews()
|
override func viewDidLayoutSubviews()
|
||||||
@@ -161,12 +162,8 @@ private extension AppContentViewController
|
|||||||
|
|
||||||
switch sender
|
switch sender
|
||||||
{
|
{
|
||||||
case self.descriptionTextView.toggleButton:
|
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||||
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||||
|
|
||||||
case self.versionDescriptionTextView.toggleButton:
|
|
||||||
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
|
||||||
|
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +186,7 @@ extension AppContentViewController
|
|||||||
switch Row.allCases[indexPath.row]
|
switch Row.allCases[indexPath.row]
|
||||||
{
|
{
|
||||||
case .screenshots:
|
case .screenshots:
|
||||||
guard !self.app.allScreenshots.isEmpty else { return 0.0 }
|
guard !self.app.screenshots.isEmpty else { return 0.0 }
|
||||||
return UITableView.automaticDimension
|
return UITableView.automaticDimension
|
||||||
|
|
||||||
case .permissions:
|
case .permissions:
|
||||||
|
|||||||
@@ -67,11 +67,6 @@ final class AppViewController: UIViewController
|
|||||||
self.navigationBarTitleView.sizeToFit()
|
self.navigationBarTitleView.sizeToFit()
|
||||||
self.navigationItem.titleView = self.navigationBarTitleView
|
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 = UIView()
|
||||||
self.contentViewControllerShadowView.backgroundColor = .white
|
self.contentViewControllerShadowView.backgroundColor = .white
|
||||||
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
||||||
@@ -392,8 +387,7 @@ private extension AppViewController
|
|||||||
{
|
{
|
||||||
var buttonAction: AppBannerView.AppAction?
|
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, let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||||
if let installedApp = self.app.installedApp, installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
// Explicitly set button action to .update if there is an update available, even if it's not supported.
|
// Explicitly set button action to .update if there is an update available, even if it's not supported.
|
||||||
buttonAction = .update
|
buttonAction = .update
|
||||||
@@ -543,8 +537,7 @@ extension AppViewController
|
|||||||
{
|
{
|
||||||
if let installedApp = self.app.installedApp
|
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.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||||
if let latestVersion = self.app.latestAvailableVersion, installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
self.updateApp(installedApp, to: latestVersion)
|
self.updateApp(installedApp, to: latestVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
|||||||
|
|
||||||
// NOTE: double dequeue of cell has been discontinued
|
// NOTE: double dequeue of cell has been discontinued
|
||||||
// TODO: Using harcoded value until this is fixed
|
// 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
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ extension AppDelegate
|
|||||||
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
||||||
|
|
||||||
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
||||||
static let exportCertificateNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ExportCertificateNotification")
|
|
||||||
|
|
||||||
static let importAppDeepLinkURLKey = "fileURL"
|
static let importAppDeepLinkURLKey = "fileURL"
|
||||||
static let appBackupResultKey = "result"
|
static let appBackupResultKey = "result"
|
||||||
static let addSourceDeepLinkURLKey = "sourceURL"
|
static let addSourceDeepLinkURLKey = "sourceURL"
|
||||||
static let exportCertificateCallbackTemplateKey = "callback"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@@ -70,17 +68,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
// Register default settings before doing anything else.
|
// Register default settings before doing anything else.
|
||||||
UserDefaults.registerDefaults()
|
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
|
DatabaseManager.shared.start { (error) in
|
||||||
if let error = error
|
if let error = error
|
||||||
@@ -93,14 +81,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setTintColor()
|
AnalyticsManager.shared.start()
|
||||||
|
|
||||||
self.setTintColor()
|
self.setTintColor()
|
||||||
self.prepareImageCache()
|
self.prepareImageCache()
|
||||||
|
|
||||||
// TODO: @mahee96: find if we need to start em_proxy as in altstore?
|
// 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()
|
SecureValueTransformer.register()
|
||||||
|
|
||||||
@@ -112,7 +99,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
||||||
|
|
||||||
#if DEBUG && targetEnvironment(simulator)
|
#if DEBUG && (targetEnvironment(simulator) || BETA)
|
||||||
UserDefaults.standard.isDebugModeEnabled = true
|
UserDefaults.standard.isDebugModeEnabled = true
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -125,9 +112,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
{
|
{
|
||||||
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
||||||
// TODO: @mahee96: find if we need to stop em_proxy as in altstore?
|
// 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 }
|
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
||||||
|
|
||||||
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
||||||
@@ -144,9 +129,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
func applicationWillEnterForeground(_ application: UIApplication)
|
func applicationWillEnterForeground(_ application: UIApplication)
|
||||||
{
|
{
|
||||||
AppManager.shared.update()
|
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
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||||
@@ -297,26 +282,6 @@ private extension AppDelegate
|
|||||||
|
|
||||||
return true
|
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
|
default: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,8 +424,6 @@ private extension AppDelegate
|
|||||||
|
|
||||||
try context.save()
|
try context.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let updatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest()
|
let updatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest()
|
||||||
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<objects>
|
<objects>
|
||||||
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
|
<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">
|
<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"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<color key="barTintColor" name="SettingsBackground"/>
|
<color key="barTintColor" name="SettingsBackground"/>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
|
<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>
|
</view>
|
||||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
|
<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>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
|
<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"/>
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
|
||||||
<subviews>
|
<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">
|
<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"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<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">
|
<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"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</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"/>
|
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
|
||||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
|
<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>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
||||||
@@ -198,10 +198,6 @@
|
|||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<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"/>
|
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
@@ -219,15 +215,19 @@
|
|||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
|
<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 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="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="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="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 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 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="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="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="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="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"/>
|
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
|
<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>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
|
<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"/>
|
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
|
||||||
@@ -298,7 +298,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
|
<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>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
|
<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>
|
<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">
|
<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"/>
|
<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"/>
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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"/>
|
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
|
<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>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
|
<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>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
|
<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>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
|
<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>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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">
|
<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"/>
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -431,7 +431,7 @@
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1353" y="736"/>
|
<point key="canvasLocation" x="1353" y="736"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Refresh SideStore-->
|
<!--Refresh AltStore-->
|
||||||
<scene sceneID="9Vh-dM-OqX">
|
<scene sceneID="9Vh-dM-OqX">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
|
<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"/>
|
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</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"/>
|
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
|
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
|
||||||
|
|||||||
@@ -287,7 +287,7 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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"/>
|
<rect key="frame" x="20" y="20" width="335" height="34"/>
|
||||||
<color key="backgroundColor" name="Background"/>
|
<color key="backgroundColor" name="Background"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</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"/>
|
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
|
||||||
<color key="backgroundColor" name="Background"/>
|
<color key="backgroundColor" name="Background"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||||
@@ -532,7 +532,6 @@
|
|||||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" name="Primary"/>
|
<color key="tintColor" name="Primary"/>
|
||||||
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
@@ -562,7 +561,6 @@
|
|||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
@@ -915,7 +913,6 @@
|
|||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|||||||
@@ -538,8 +538,7 @@ private extension BrowseViewController
|
|||||||
|
|
||||||
let app = self.dataSource.item(at: indexPath)
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
// if let installedApp = app.installedApp, !installedApp.isUpdateAvailable
|
if let installedApp = app.installedApp, !installedApp.isUpdateAvailable
|
||||||
if let installedApp = app.installedApp, !installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
self.open(installedApp)
|
self.open(installedApp)
|
||||||
}
|
}
|
||||||
@@ -564,8 +563,7 @@ private extension BrowseViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
Task<Void, Never>(priority: .userInitiated) { @MainActor in
|
Task<Void, Never>(priority: .userInitiated) { @MainActor in
|
||||||
// if let installedApp = app.installedApp, installedApp.isUpdateAvailable
|
if let installedApp = app.installedApp, installedApp.isUpdateAvailable
|
||||||
if let installedApp = app.installedApp, installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,8 +482,7 @@ private extension FeaturedViewController
|
|||||||
|
|
||||||
let storeApp = self.dataSource.item(at: indexPath)
|
let storeApp = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||||
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
self.open(installedApp)
|
self.open(installedApp)
|
||||||
}
|
}
|
||||||
@@ -501,8 +500,7 @@ private extension FeaturedViewController
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||||
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,12 +138,11 @@ extension AppBannerView
|
|||||||
init(app: AppProtocol)
|
init(app: AppProtocol)
|
||||||
{
|
{
|
||||||
self.name = app.name
|
self.name = app.name
|
||||||
|
|
||||||
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
|
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
|
||||||
self.developerName = storeApp.developerName
|
self.developerName = storeApp.developerName
|
||||||
|
|
||||||
if let track = storeApp.latestSupportedVersion?.channel,
|
if storeApp.isBeta
|
||||||
ReleaseTracks.betaTracks.contains(track)
|
|
||||||
{
|
{
|
||||||
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
||||||
self.isBeta = true
|
self.isBeta = true
|
||||||
@@ -234,8 +233,7 @@ extension AppBannerView
|
|||||||
{
|
{
|
||||||
// App is installed
|
// App is installed
|
||||||
|
|
||||||
// if installedApp.isUpdateAvailable
|
if installedApp.isUpdateAvailable
|
||||||
if installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
buttonAction = .update
|
buttonAction = .update
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,21 @@ final class CollapsingTextView: UITextView
|
|||||||
var isCollapsed = true {
|
var isCollapsed = true {
|
||||||
didSet {
|
didSet {
|
||||||
guard self.isCollapsed != oldValue else { return }
|
guard self.isCollapsed != oldValue else { return }
|
||||||
|
self.shouldResetLayout = true
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var maximumNumberOfLines = 2 {
|
var maximumNumberOfLines = 2 {
|
||||||
didSet {
|
didSet {
|
||||||
|
self.shouldResetLayout = true
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lineSpacing: Double = 2 {
|
var lineSpacing: Double = 2 {
|
||||||
didSet {
|
didSet {
|
||||||
|
self.shouldResetLayout = true
|
||||||
|
|
||||||
if #available(iOS 16, *)
|
if #available(iOS 16, *)
|
||||||
{
|
{
|
||||||
@@ -39,6 +42,7 @@ final class CollapsingTextView: UITextView
|
|||||||
|
|
||||||
override var text: String! {
|
override var text: String! {
|
||||||
didSet {
|
didSet {
|
||||||
|
self.shouldResetLayout = true
|
||||||
|
|
||||||
guard #available(iOS 16, *) else { return }
|
guard #available(iOS 16, *) else { return }
|
||||||
self.updateText()
|
self.updateText()
|
||||||
@@ -47,6 +51,9 @@ final class CollapsingTextView: UITextView
|
|||||||
|
|
||||||
let moreButton = UIButton(type: .system)
|
let moreButton = UIButton(type: .system)
|
||||||
|
|
||||||
|
private var shouldResetLayout: Bool = false
|
||||||
|
private var previousSize: CGSize?
|
||||||
|
|
||||||
override init(frame: CGRect, textContainer: NSTextContainer?)
|
override init(frame: CGRect, textContainer: NSTextContainer?)
|
||||||
{
|
{
|
||||||
super.init(frame: frame, textContainer: textContainer)
|
super.init(frame: frame, textContainer: textContainer)
|
||||||
@@ -108,39 +115,45 @@ final class CollapsingTextView: UITextView
|
|||||||
height: font.lineHeight)
|
height: font.lineHeight)
|
||||||
self.moreButton.frame = moreButtonFrame
|
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)
|
if self.isCollapsed
|
||||||
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
|
|
||||||
|
|
||||||
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
|
||||||
{
|
{
|
||||||
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
|
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||||
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.maximumNumberOfLines = self.maximumNumberOfLines
|
||||||
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
|
||||||
|
var exclusionFrame = moreButtonFrame
|
||||||
self.moreButton.isHidden = false
|
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
|
else
|
||||||
{
|
{
|
||||||
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
|
self.textContainer.maximumNumberOfLines = 0
|
||||||
self.textContainer.exclusionPaths = []
|
self.textContainer.exclusionPaths = []
|
||||||
|
|
||||||
self.moreButton.isHidden = true
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,23 +65,13 @@ class ToastView: RSTToastView
|
|||||||
self.opensErrorLog = opensLog
|
self.opensErrorLog = opensLog
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InfoMode: String {
|
convenience init(error: Error)
|
||||||
case fullError
|
|
||||||
case localizedDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(error: Error){
|
|
||||||
self.init(error: error, mode: .localizedDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(error: Error, mode: InfoMode)
|
|
||||||
{
|
{
|
||||||
let error = error as NSError
|
let error = error as NSError
|
||||||
let mode = mode == .fullError ? ErrorProcessing.InfoMode.fullError : ErrorProcessing.InfoMode.localizedDescription
|
|
||||||
|
|
||||||
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
|
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)
|
self.init(text: text, detailText: detailText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>BuildRevision</key>
|
||||||
|
<string>$(BUILD_REVISION)</string>
|
||||||
<key>INIntentsSupported</key>
|
<key>INIntentsSupported</key>
|
||||||
<array>
|
<array>
|
||||||
<string>RefreshAllIntent</string>
|
<string>RefreshAllIntent</string>
|
||||||
|
|||||||
@@ -12,73 +12,84 @@ import EmotionalDamage
|
|||||||
import minimuxer
|
import minimuxer
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
import AltSign
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||||
|
|
||||||
final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
|
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||||
|
{
|
||||||
private var didFinishLaunching = false
|
private var didFinishLaunching = false
|
||||||
private var retries = 0
|
|
||||||
private var maxRetries = 3
|
private var destinationViewController: TabBarController!
|
||||||
private var splashView: SplashView!
|
|
||||||
private var destinationViewController: TabBarController?
|
override var launchConditions: [RSTLaunchCondition] {
|
||||||
private var startTime: Date!
|
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
|
||||||
|
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [isDatabaseStarted]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func runLaunchSequence() async {
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
guard retries < maxRetries else { return }
|
return self.children.first
|
||||||
retries += 1
|
}
|
||||||
|
|
||||||
await Task.detached {
|
override var childForStatusBarHidden: UIViewController? {
|
||||||
if !DatabaseManager.shared.isStarted {
|
return self.children.first
|
||||||
await withCheckedContinuation { continuation in
|
}
|
||||||
DatabaseManager.shared.start { error in
|
|
||||||
if let error {
|
override func viewDidLoad()
|
||||||
Task { await self.handleLaunchError(error, retryCallback: self.runLaunchSequence) }
|
{
|
||||||
} else {
|
defer {
|
||||||
Task { await self.finishLaunching() }
|
// 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 {
|
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
|
||||||
DispatchQueue.global().async { SideJITManager.shared.askForNetwork() }
|
DispatchQueue.global().async {
|
||||||
|
self.askfornetwork()
|
||||||
|
}
|
||||||
print("SideJITServer Enabled")
|
print("SideJITServer Enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#if !targetEnvironment(simulator)
|
#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 {
|
guard let pf = fetchPairingFile() else {
|
||||||
displayError("Device pairing file not found.")
|
displayError("Device pairing file not found.")
|
||||||
return
|
return
|
||||||
@@ -86,189 +97,281 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
|
|||||||
start_minimuxer_threads(pf)
|
start_minimuxer_threads(pf)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func start_minimuxer_threads(_ pairing_file: String) {
|
func askfornetwork() {
|
||||||
target_minimuxer_address()
|
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
||||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
|
||||||
do {
|
var SJSURL = address
|
||||||
let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
|
|
||||||
try minimuxer.startWithLogger(pairing_file, documentsDirectory, loggingEnabled)
|
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
||||||
} catch {
|
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
||||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName))
|
|
||||||
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")")
|
|
||||||
}
|
}
|
||||||
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) {
|
func displayError(_ msg: String) {
|
||||||
print(msg)
|
print(msg)
|
||||||
let alert = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
|
// Create a new alert
|
||||||
self.present(alert, animated: true)
|
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]) {
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
let url = urls[0]
|
let url = urls[0]
|
||||||
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
|
||||||
defer {
|
|
||||||
if (isSecuredURL) {
|
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: url)
|
// Read to a string
|
||||||
guard let pairingString = String(data: data, encoding: .utf8) else {
|
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")
|
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 {
|
} catch {
|
||||||
displayError("Unable to read pairing file")
|
displayError("Unable to read pairing file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSecuredURL) {
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
controller.dismiss(animated: true, completion: nil)
|
controller.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func importAccountAtFile(_ file: URL, remove: Bool = false) {
|
func start_minimuxer_threads(_ pairing_file: String) {
|
||||||
_ = file.startAccessingSecurityScopedResource()
|
target_minimuxer_address()
|
||||||
defer { file.stopAccessingSecurityScopedResource() }
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
guard let accountD = try? Data(contentsOf: file) else {
|
do {
|
||||||
return Logger.main.notice("Could not parse data from file \(file)")
|
// 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 {
|
if #available(iOS 17, *) {
|
||||||
return Logger.main.notice("Could not parse data from file \(file)")
|
// 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)")
|
else {
|
||||||
if remove {
|
start_auto_mounter(documentsDirectory)
|
||||||
try? FileManager.default.removeItem(at: file)
|
|
||||||
}
|
}
|
||||||
Keychain.shared.appleIDEmailAddress = account.email
|
|
||||||
Keychain.shared.appleIDPassword = account.password
|
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||||
Keychain.shared.adiPb = account.adiPB
|
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LaunchViewController {
|
extension LaunchViewController
|
||||||
@MainActor
|
{
|
||||||
func handleLaunchError(_ error: Error, retryCallback: (() async -> Void)? = nil) {
|
override func handleLaunchError(_ error: Error)
|
||||||
do { throw error } catch let error as NSError {
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
catch let error as NSError
|
||||||
|
{
|
||||||
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
|
||||||
let desc: String
|
|
||||||
if #available(iOS 14.5, *) {
|
let errorDescription: String
|
||||||
desc = ([error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }).joined(separator: "\n\n")
|
|
||||||
} else {
|
if #available(iOS 14.5, *)
|
||||||
desc = error.debugDescription
|
{
|
||||||
|
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)
|
else
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default) { _ in
|
{
|
||||||
Task { await retryCallback?() }
|
errorDescription = error.debugDescription
|
||||||
})
|
}
|
||||||
present(alert, animated: true)
|
|
||||||
|
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
|
override func finishLaunching()
|
||||||
func finishLaunching() async {
|
{
|
||||||
guard !didFinishLaunching else { return }
|
super.finishLaunching()
|
||||||
didFinishLaunching = true
|
|
||||||
|
guard !self.didFinishLaunching else { return }
|
||||||
|
|
||||||
AppManager.shared.update()
|
AppManager.shared.update()
|
||||||
|
AppManager.shared.updatePatronsIfNeeded()
|
||||||
|
PatreonAPI.shared.refreshPatreonAccount()
|
||||||
|
|
||||||
AppManager.shared.updateAllSources { result in
|
AppManager.shared.updateAllSources { result in
|
||||||
guard case .failure(let error) = result else { return }
|
guard case .failure(let error) = result else { return }
|
||||||
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
|
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
|
||||||
|
|
||||||
|
|
||||||
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
|
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
|
||||||
print("Failed to update sources on launch. \(errorDesc)")
|
print("Failed to update sources on launch. \(errorDesc)")
|
||||||
|
|
||||||
var mode: ToastView.InfoMode = .fullError
|
let toastView = ToastView(error: error)
|
||||||
if String(describing: error).contains("The Internet connection appears to be offline"){
|
|
||||||
mode = .localizedDescription // dont make noise!
|
|
||||||
}
|
|
||||||
let toastView = ToastView(error: error, mode: mode)
|
|
||||||
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
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()
|
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)
|
UIView.animate(withDuration: 0.2) {
|
||||||
let remaining = elapsed >= 1 ? 0 : 1 - elapsed
|
self.destinationViewController.view.alpha = 1.0
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.didFinishLaunching = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateKnownSources() {
|
private extension LaunchViewController
|
||||||
|
{
|
||||||
|
func updateKnownSources()
|
||||||
|
{
|
||||||
AppManager.shared.updateKnownSources { result in
|
AppManager.shared.updateKnownSources { result in
|
||||||
switch result {
|
switch result
|
||||||
|
{
|
||||||
case .failure(let error): print("[ALTLog] Failed to update known sources:", error)
|
case .failure(let error): print("[ALTLog] Failed to update known sources:", error)
|
||||||
case .success((_, let blockedSources)):
|
case .success((_, let blockedSources)):
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||||
let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier })
|
let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier })
|
||||||
let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL })
|
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 predicate = NSPredicate(format: "%K IN %@ OR %K IN %@",
|
||||||
let blocked = blockedSources.first { $0.identifier == source.identifier }
|
#keyPath(Source.identifier), blockedSourceIDs,
|
||||||
return SourceError.blocked(source, bundleIDs: blocked?.bundleIDs, existingSource: source)
|
#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 }
|
guard !sourceErrors.isEmpty else { return }
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
for error in sourceErrors {
|
for error in sourceErrors
|
||||||
|
{
|
||||||
let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name)
|
let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name)
|
||||||
let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
||||||
|
|
||||||
await self.presentAlert(title: title, message: message)
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Intents
|
import Intents
|
||||||
@@ -22,6 +23,7 @@ import Roxas
|
|||||||
extension AppManager
|
extension AppManager
|
||||||
{
|
{
|
||||||
static let didFetchSourceNotification = Notification.Name("io.sidestore.AppManager.didFetchSource")
|
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 didAddSourceNotification = Notification.Name("io.sidestore.AppManager.didAddSource")
|
||||||
static let didRemoveSourceNotification = Notification.Name("io.sidestore.AppManager.didRemoveSource")
|
static let didRemoveSourceNotification = Notification.Name("io.sidestore.AppManager.didRemoveSource")
|
||||||
static let willInstallAppFromNewSourceNotification = Notification.Name("io.sidestore.AppManager.willInstallAppFromNewSource")
|
static let willInstallAppFromNewSourceNotification = Notification.Name("io.sidestore.AppManager.willInstallAppFromNewSource")
|
||||||
@@ -589,6 +591,34 @@ extension AppManager
|
|||||||
return updateKnownSourcesOperation
|
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)
|
func updateAllSources(completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
{
|
{
|
||||||
self.updateSourcesResult = nil
|
self.updateSourcesResult = nil
|
||||||
@@ -604,9 +634,6 @@ extension AppManager
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (_, context) = try result.get()
|
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()
|
try context.save()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
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
|
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{
|
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||||
await self.perform([.update(appVersion)], presentingViewController: presentingViewController, group: group)
|
|
||||||
}
|
|
||||||
|
|
||||||
return group.progress
|
return group.progress
|
||||||
}
|
}
|
||||||
@@ -744,20 +742,16 @@ extension AppManager
|
|||||||
{
|
{
|
||||||
let group = group ?? RefreshGroup()
|
let group = group ?? RefreshGroup()
|
||||||
|
|
||||||
Task{
|
let operations = installedApps.map { AppOperation.refresh($0) }
|
||||||
await self.perform(installedApps.map { .refresh($0) }, presentingViewController: presentingViewController, group: group)
|
return self.perform(operations, presentingViewController: presentingViewController, group: group)
|
||||||
}
|
|
||||||
|
|
||||||
return group
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func activate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
func activate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||||
{
|
{
|
||||||
let group = RefreshGroup()
|
let group = RefreshGroup()
|
||||||
|
|
||||||
Task{
|
let operation = AppOperation.activate(installedApp)
|
||||||
await self.perform([.activate(installedApp)], presentingViewController: presentingViewController, group: group)
|
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||||
}
|
|
||||||
|
|
||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
@@ -815,9 +809,8 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task{
|
let operation = AppOperation.deactivate(installedApp)
|
||||||
await self.perform([.deactivate(installedApp)], presentingViewController: presentingViewController, group: group)
|
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,9 +834,8 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task{
|
let operation = AppOperation.backup(installedApp)
|
||||||
await self.perform([.backup(installedApp)], presentingViewController: presentingViewController, group: group)
|
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||||
@@ -868,9 +860,8 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task{
|
let operation = AppOperation.restore(installedApp)
|
||||||
await self.perform([.restore(installedApp)], presentingViewController: presentingViewController, group: group)
|
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||||
@@ -1097,7 +1088,7 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@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 }
|
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 .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough
|
||||||
case .refresh(let app):
|
case .refresh(let app):
|
||||||
let refreshProgress = self._refresh(app, operation: operation, group: group) { (result) in
|
// Check if backup app is installed in place of real app.
|
||||||
self.finish(operation, result: result, group: group, progress: progress)
|
// let altBackupUti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||||
}
|
|
||||||
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
// 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):
|
case .activate(let app):
|
||||||
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
|
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
|
||||||
self.finish(operation, result: result, group: group, progress: progress)
|
self.finish(operation, result: result, group: group, progress: progress)
|
||||||
@@ -1209,13 +1228,13 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Disable the idleTimeout
|
|
||||||
DispatchQueue.main.schedule {
|
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()
|
performAppOperations()
|
||||||
|
DispatchQueue.main.schedule {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return group
|
return group
|
||||||
@@ -1232,10 +1251,21 @@ private extension AppManager
|
|||||||
{
|
{
|
||||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
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)
|
assert(context.authenticatedContext === group.context)
|
||||||
|
|
||||||
context.beginInstallationHandler = { (installedApp) in
|
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)
|
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 */
|
/* Download */
|
||||||
let downloadedAppURL = context.temporaryDirectory.appendingPathComponent("Cached.app")
|
let downloadedAppURL = context.temporaryDirectory.appendingPathComponent("Cached.app")
|
||||||
let downloadOperation = DownloadAppOperation(app: downloadingApp, destinationURL: downloadedAppURL, context: context)
|
let downloadOperation = DownloadAppOperation(app: downloadingApp, destinationURL: downloadedAppURL, context: context)
|
||||||
@@ -1267,8 +1311,7 @@ private extension AppManager
|
|||||||
|
|
||||||
if cacheApp
|
if cacheApp
|
||||||
{
|
{
|
||||||
let updatedApp = AnyApp(from: app, bundleId: context.bundleIdentifier)
|
try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: app), shouldReplace: true)
|
||||||
try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: updatedApp), shouldReplace: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -1278,9 +1321,15 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
progress.addChild(downloadOperation.progress, withPendingUnitCount: 25)
|
progress.addChild(downloadOperation.progress, withPendingUnitCount: 25)
|
||||||
|
|
||||||
|
if let verifyPledgeOperation
|
||||||
|
{
|
||||||
|
downloadOperation.addDependency(verifyPledgeOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Verify App */
|
/* Verify App */
|
||||||
let permissionsMode = UserDefaults.shared.permissionCheckingDisabled ? .none : permissionReviewMode
|
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
|
verifyOperation.resultHandler = { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -1433,7 +1482,7 @@ private extension AppManager
|
|||||||
let patchAppURL = URL(string: patchAppLink)
|
let patchAppURL = URL(string: patchAppLink)
|
||||||
else { throw OperationError.invalidApp }
|
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 {
|
DispatchQueue.main.async {
|
||||||
let storyboard = UIStoryboard(name: "PatchApp", bundle: nil)
|
let storyboard = UIStoryboard(name: "PatchApp", bundle: nil)
|
||||||
@@ -1455,7 +1504,7 @@ private extension AppManager
|
|||||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
presentingViewController.present(navigationController, animated: true, completion: nil)
|
presentingViewController.present(navigationController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -1467,24 +1516,6 @@ private extension AppManager
|
|||||||
patchAppOperation.addDependency(deactivateAppsOperation)
|
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 */
|
/* Resign */
|
||||||
let resignAppOperation = ResignAppOperation(context: context)
|
let resignAppOperation = ResignAppOperation(context: context)
|
||||||
resignAppOperation.resultHandler = { (result) in
|
resignAppOperation.resultHandler = { (result) in
|
||||||
@@ -1499,7 +1530,6 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
resignAppOperation.addDependency(patchAppOperation)
|
resignAppOperation.addDependency(patchAppOperation)
|
||||||
resignAppOperation.addDependency(modifyAppExBundleIdOperation)
|
|
||||||
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
||||||
|
|
||||||
|
|
||||||
@@ -1545,6 +1575,7 @@ private extension AppManager
|
|||||||
|
|
||||||
// Operations picked for request
|
// Operations picked for request
|
||||||
var operations = [
|
var operations = [
|
||||||
|
verifyPledgeOperation,
|
||||||
downloadOperation,
|
downloadOperation,
|
||||||
verifyOperation,
|
verifyOperation,
|
||||||
removeAppExtensionsOperation,
|
removeAppExtensionsOperation,
|
||||||
@@ -1552,7 +1583,6 @@ private extension AppManager
|
|||||||
patchAppOperation,
|
patchAppOperation,
|
||||||
refreshAnisetteDataOperation,
|
refreshAnisetteDataOperation,
|
||||||
fetchProvisioningProfilesOperation,
|
fetchProvisioningProfilesOperation,
|
||||||
modifyAppExBundleIdOperation,
|
|
||||||
resignAppOperation,
|
resignAppOperation,
|
||||||
sendAppOperation,
|
sendAppOperation,
|
||||||
installOperation
|
installOperation
|
||||||
@@ -1632,8 +1662,8 @@ private extension AppManager
|
|||||||
|
|
||||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||||
context.app = ALTApplication(fileURL: app.fileURL)
|
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
|
//App-Extensions: Ensure DB data and disk state must match
|
||||||
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
|
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
|
||||||
let diskAppEx: Set<ALTApplication> = Set(context.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?)
|
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.
|
// Must remove before saving installedApp.
|
||||||
if let currentProgress = self.progress(for: operation), currentProgress == progress
|
if let currentProgress = self.progress(for: operation), currentProgress == progress
|
||||||
{
|
{
|
||||||
@@ -2092,6 +2112,27 @@ private extension AppManager
|
|||||||
self.scheduleExpirationWarningLocalNotification(for: installedApp)
|
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
|
// Ask widgets to be refreshed
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
|
||||||
@@ -2176,7 +2217,7 @@ private extension AppManager
|
|||||||
switch operation
|
switch operation
|
||||||
{
|
{
|
||||||
case _ where requiresSerialQueue: fallthrough
|
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
|
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.
|
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import AltStoreCore
|
|||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
import minimuxer
|
import minimuxer
|
||||||
import SemanticVersion
|
|
||||||
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
|
||||||
@@ -166,11 +165,9 @@ class MyAppsViewController: UICollectionViewController, PeekPopPreviewing
|
|||||||
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
var minimuxerStatus: Bool {
|
var minimuxerStatus: Bool {
|
||||||
// added isMinimuxerStatusCheckEnabled to forcefully ignore minimuxer status if status check is disabled in settings
|
guard minimuxer.ready() else {
|
||||||
guard !UserDefaults.standard.isMinimuxerStatusCheckEnabled || minimuxer.ready() else {
|
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
|
||||||
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No Wi-Fi or VPN!")).show(in: self)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -237,30 +234,18 @@ private extension MyAppsViewController
|
|||||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
cell.tintColor = app.tintColor ?? .altPrimary
|
cell.tintColor = app.tintColor ?? .altPrimary
|
||||||
cell.versionDescriptionTextView.maximumNumberOfLines = 2
|
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
|
||||||
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
|
|
||||||
|
|
||||||
cell.bannerView.iconImageView.image = nil
|
cell.bannerView.iconImageView.image = nil
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
|
||||||
cell.bannerView.button.isIndicatingActivity = false
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
cell.bannerView.configure(for: app, action: .update)
|
cell.bannerView.configure(for: app, action: .update)
|
||||||
|
cell.bannerView.subtitleLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), latestSupportedVersion.localizedVersion)
|
||||||
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)
|
|
||||||
|
|
||||||
let appName: String
|
let appName: String
|
||||||
|
|
||||||
if ReleaseTracks.betaTracks.contains(latestSupportedVersion.channel)
|
if app.isBeta
|
||||||
{
|
{
|
||||||
appName = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
appName = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
||||||
}
|
}
|
||||||
@@ -284,9 +269,12 @@ private extension MyAppsViewController
|
|||||||
cell.mode = .collapsed
|
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()
|
cell.setNeedsLayout()
|
||||||
|
|
||||||
|
// Below lines are necessary to avoid "more" button layout issues.
|
||||||
|
cell.versionDescriptionTextView.setNeedsLayout()
|
||||||
cell.layoutIfNeeded()
|
cell.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
||||||
@@ -367,12 +355,8 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
formatter.maximumUnitCount = 1
|
formatter.maximumUnitCount = 1
|
||||||
|
|
||||||
var timeInterval: String? = "expired"
|
|
||||||
let expirationDate = installedApp.expirationDate
|
let timeInterval = formatter.string(from: currentDate, to: installedApp.expirationDate)
|
||||||
let isExpired = currentDate > expirationDate
|
|
||||||
if(!isExpired) {
|
|
||||||
timeInterval = formatter.string(from: currentDate, to: expirationDate)
|
|
||||||
}
|
|
||||||
cell.bannerView.button.setTitle(timeInterval?.uppercased(), for: .normal)
|
cell.bannerView.button.setTitle(timeInterval?.uppercased(), for: .normal)
|
||||||
|
|
||||||
cell.bannerView.button.isIndicatingActivity = false
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
@@ -380,7 +364,7 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
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.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||||
|
|
||||||
cell.bannerView.button.removeTarget(self, action: nil, for: .primaryActionTriggered)
|
cell.bannerView.button.removeTarget(self, action: nil, for: .primaryActionTriggered)
|
||||||
@@ -544,6 +528,11 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
print("[ALTLog] Failed to fetch updates:", error)
|
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
|
let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell
|
||||||
|
|
||||||
// Toggle the state
|
|
||||||
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
|
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
|
||||||
{
|
{
|
||||||
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
|
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
|
||||||
// Set collapsed mode on the cell
|
|
||||||
cell?.mode = .collapsed
|
cell?.mode = .collapsed
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
|
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
|
||||||
// Set expanded mode on the cell
|
|
||||||
cell?.mode = .expanded
|
cell?.mode = .expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cached size so it's recalculated
|
|
||||||
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
|
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
|
||||||
|
|
||||||
// Animate the change smoothly with a duration
|
self.collectionView.performBatchUpdates({
|
||||||
UIView.animate(withDuration: 0.25) {
|
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||||
self.collectionView.performBatchUpdates({
|
}, completion: nil)
|
||||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
|
||||||
}, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func refreshApp(_ sender: UIButton)
|
@IBAction func refreshApp(_ sender: UIButton)
|
||||||
@@ -1062,6 +1044,57 @@ private extension MyAppsViewController
|
|||||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
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)
|
@objc func showHiddenUpdatesAlert(_ sender: UIButton)
|
||||||
{
|
{
|
||||||
guard !self.unsupportedUpdates.isEmpty else { return }
|
guard !self.unsupportedUpdates.isEmpty else { return }
|
||||||
@@ -1482,6 +1515,15 @@ private extension MyAppsViewController
|
|||||||
guard minimuxerStatus else { return }
|
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
|
AppManager.shared.enableJIT(for: installedApp) { result in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
switch result {
|
switch result {
|
||||||
@@ -1560,7 +1602,6 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
catch let error as AppManager.FetchSourcesError
|
catch let error as AppManager.FetchSourcesError
|
||||||
{
|
{
|
||||||
print(error)
|
|
||||||
try await error.managedObjectContext?.performAsync {
|
try await error.managedObjectContext?.performAsync {
|
||||||
try error.managedObjectContext?.save()
|
try error.managedObjectContext?.save()
|
||||||
}
|
}
|
||||||
@@ -1592,7 +1633,6 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
catch let error as NSError
|
catch let error as NSError
|
||||||
{
|
{
|
||||||
print(error)
|
|
||||||
let toastView = ToastView(error: error.withLocalizedTitle(NSLocalizedString("Unable to Check for Updates", comment: "")))
|
let toastView = ToastView(error: error.withLocalizedTitle(NSLocalizedString("Unable to Check for Updates", comment: "")))
|
||||||
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
|
|||||||
@@ -21,19 +21,12 @@ extension UpdateCollectionViewCell
|
|||||||
{
|
{
|
||||||
var mode: Mode = .expanded {
|
var mode: Mode = .expanded {
|
||||||
didSet {
|
didSet {
|
||||||
switch self.mode {
|
self.update()
|
||||||
case .collapsed:
|
|
||||||
self.versionDescriptionTextView.isCollapsed = true
|
|
||||||
case .expanded:
|
|
||||||
self.versionDescriptionTextView.isCollapsed = false
|
|
||||||
}
|
|
||||||
self.setNeedsLayout()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBOutlet var bannerView: AppBannerView!
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
// @IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||||
@IBOutlet var versionDescriptionTextView: CollapsingMarkdownView!
|
|
||||||
|
|
||||||
@IBOutlet private var blurView: UIVisualEffectView!
|
@IBOutlet private var blurView: UIVisualEffectView!
|
||||||
|
|
||||||
@@ -92,16 +85,16 @@ extension UpdateCollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||||
// {
|
{
|
||||||
// // Ensure cell is laid out so it will report correct size.
|
// Ensure cell is laid out so it will report correct size.
|
||||||
// self.versionDescriptionTextView.setNeedsLayout()
|
self.versionDescriptionTextView.setNeedsLayout()
|
||||||
// self.versionDescriptionTextView.layoutIfNeeded()
|
self.versionDescriptionTextView.layoutIfNeeded()
|
||||||
//
|
|
||||||
// let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||||
//
|
|
||||||
// return size
|
return size
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UpdateCollectionViewCell
|
private extension UpdateCollectionViewCell
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<objects>
|
<objects>
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||||
<subviews>
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="50" width="343" height="75"/>
|
||||||
<subviews>
|
<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"/>
|
<rect key="frame" x="15" y="0.0" width="313" height="26"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
<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"/>
|
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<systemColor name="secondaryLabelColor">
|
<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>
|
</systemColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -319,8 +319,7 @@ private extension NewsViewController
|
|||||||
let app = self.dataSource.item(at: indexPath)
|
let app = self.dataSource.item(at: indexPath)
|
||||||
guard let storeApp = app.storeApp else { return }
|
guard let storeApp = app.storeApp else { return }
|
||||||
|
|
||||||
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
||||||
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
self.open(installedApp)
|
self.open(installedApp)
|
||||||
}
|
}
|
||||||
@@ -339,8 +338,7 @@ private extension NewsViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
Task<Void, Never>(priority: .userInitiated) { @MainActor in
|
Task<Void, Never>(priority: .userInitiated) { @MainActor in
|
||||||
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||||
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
|
|
||||||
{
|
{
|
||||||
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,87 +84,71 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
|||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
// Sign In
|
||||||
// try to use cached session
|
self.signIn() { (result) in
|
||||||
if
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// new login
|
switch result
|
||||||
do {
|
{
|
||||||
let (account, session) = try await withUnsafeThrowingContinuation { c in
|
case .failure(let error): self.finish(.failure(error))
|
||||||
self.signIn() { (result) in
|
case .success((let account, let session)):
|
||||||
c.resume(with: result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.context.session = session
|
self.context.session = session
|
||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
|
||||||
|
|
||||||
let team = try await withUnsafeThrowingContinuation { c in
|
// Fetch Team
|
||||||
self.fetchTeam(for: account, session: session) { (result) in
|
self.fetchTeam(for: account, session: session) { (result) in
|
||||||
c.resume(with: result)
|
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
|
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
||||||
{
|
{
|
||||||
Logger.sideload.notice("Authenticating Apple ID...")
|
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)
|
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
|
||||||
{
|
{
|
||||||
self.appleIDEmailAddress = appleID
|
self.appleIDEmailAddress = appleID
|
||||||
@@ -502,8 +444,6 @@ private extension AuthenticationOperation
|
|||||||
verificationHandler: verificationHandler) { (account, session, error) in
|
verificationHandler: verificationHandler) { (account, session, error) in
|
||||||
if let account = account, let session = session
|
if let account = account, let session = session
|
||||||
{
|
{
|
||||||
Keychain.shared.appleIDAdsid = session.dsid
|
|
||||||
Keychain.shared.appleIDXcodeToken = session.authToken
|
|
||||||
completionHandler(.success((account, session)))
|
completionHandler(.success((account, session)))
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -807,30 +747,3 @@ extension AuthenticationOperation
|
|||||||
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
|
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -99,10 +99,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
|||||||
self.finish(.failure(RefreshError(.noInstalledApps)))
|
self.finish(.failure(RefreshError(.noInstalledApps)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
if UserDefaults.standard.enableEMPforWireguard {
|
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
|
||||||
}
|
|
||||||
target_minimuxer_address()
|
target_minimuxer_address()
|
||||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
self.appName = app.name
|
self.appName = app.name
|
||||||
self.bundleIdentifier = context.bundleIdentifier
|
self.bundleIdentifier = app.bundleIdentifier
|
||||||
self.sourceURL = app.url
|
self.sourceURL = app.url
|
||||||
self.destinationURL = destinationURL
|
self.destinationURL = destinationURL
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
guard let latestVersion = storeApp.latestAvailableVersion else {
|
guard let latestVersion = storeApp.latestAvailableVersion else {
|
||||||
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
|
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
|
||||||
throw OperationError.unknown(failureReason: failureReason)
|
throw OperationError.unknown(failureReason: failureReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to download latest _available_ version, and fall back to older versions if necessary.
|
// Attempt to download latest _available_ version, and fall back to older versions if necessary.
|
||||||
appVersion = latestVersion
|
appVersion = latestVersion
|
||||||
@@ -99,8 +99,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
if let installedApp = storeApp.installedApp
|
if let installedApp = storeApp.installedApp
|
||||||
{
|
{
|
||||||
// guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
|
guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
|
||||||
guard installedApp.hasUpdate else { return self.finish(.failure(error)) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
||||||
@@ -224,6 +223,12 @@ private extension DownloadAppOperation
|
|||||||
fileURL = sourceURL
|
fileURL = sourceURL
|
||||||
self.progress.completedUnitCount += 3
|
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
|
else
|
||||||
{
|
{
|
||||||
// Regular app
|
// Regular app
|
||||||
@@ -317,6 +322,107 @@ private extension DownloadAppOperation
|
|||||||
self.printWithTid("download started: \(downloadURL)")
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ struct OperationError: ALTLocalizedError {
|
|||||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||||
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
||||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||||
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID. 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 .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 .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
|
||||||
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
||||||
@@ -220,16 +220,16 @@ struct OperationError: ALTLocalizedError {
|
|||||||
case .openAppFailed:
|
case .openAppFailed:
|
||||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||||
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
|
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 .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.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 .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 of and your Firewall has been set correctly on your server.", 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 iDevice by running 'SideJITServer -y', or try refreshing SideJITServer from Settings.", 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 same Wi-Fi as SideJITServer", comment: "")
|
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
|
||||||
case .refreshsidejit: return NSLocalizedString("Unable to find app; Please try refreshing SideJITServer from Settings.", comment: "")
|
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
|
||||||
case .anisetteV1Error: return NSLocalizedString("An error occurred while getting anisette data from a V1 server: %@. Try using another anisette server.", 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 while provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", 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 while getting anisette data from a V3 server: %@. 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 the cache: %@", comment: "")
|
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
|
||||||
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
|
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
|
||||||
|
|
||||||
case .refreshAppFailed:
|
case .refreshAppFailed:
|
||||||
@@ -238,10 +238,10 @@ struct OperationError: ALTLocalizedError {
|
|||||||
|
|
||||||
case .invalidParameters:
|
case .invalidParameters:
|
||||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
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:
|
case .invalidOperationContext:
|
||||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
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 .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
|
||||||
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", 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: "")
|
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
|
||||||
@@ -260,7 +260,7 @@ struct OperationError: ALTLocalizedError {
|
|||||||
var recoverySuggestion: String? {
|
var recoverySuggestion: String? {
|
||||||
switch self.code
|
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 .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:
|
case .maximumAppIDLimitReached:
|
||||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||||
@@ -308,9 +308,9 @@ extension MinimuxerError: LocalizedError {
|
|||||||
case .NoDevice:
|
case .NoDevice:
|
||||||
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||||
case .NoConnection:
|
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:
|
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:
|
case .CreateDebug:
|
||||||
return self.createService(name: "debug")
|
return self.createService(name: "debug")
|
||||||
@@ -338,7 +338,7 @@ extension MinimuxerError: LocalizedError {
|
|||||||
case .CreateAfc:
|
case .CreateAfc:
|
||||||
return self.createService(name: "AFC")
|
return self.createService(name: "AFC")
|
||||||
case .RwAfc:
|
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):
|
case .InstallApp(let message):
|
||||||
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
|
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
|
||||||
case .UninstallApp:
|
case .UninstallApp:
|
||||||
@@ -350,38 +350,6 @@ extension MinimuxerError: LocalizedError {
|
|||||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||||
case .ProfileRemove:
|
case .ProfileRemove:
|
||||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
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: "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,14 +45,8 @@ extension VerificationError
|
|||||||
VerificationError(code: .mismatchedHash, app: app, hash: hash, expectedHash: expectedHash)
|
VerificationError(code: .mismatchedHash, app: app, hash: hash, expectedHash: expectedHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mismatchedVersion(version: String,
|
static func mismatchedVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
|
||||||
expectedVersion: String,
|
VerificationError(code: .mismatchedVersion, app: app, version: version, expectedVersion: expectedVersion)
|
||||||
app: AppProtocol) -> VerificationError
|
|
||||||
{
|
|
||||||
VerificationError(code: .mismatchedVersion, app: app,
|
|
||||||
version: version,
|
|
||||||
expectedVersion: expectedVersion
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mismatchedBuildVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
|
static func mismatchedBuildVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
|
||||||
@@ -161,11 +155,11 @@ struct VerificationError: ALTLocalizedError
|
|||||||
|
|
||||||
case .mismatchedVersion:
|
case .mismatchedVersion:
|
||||||
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
|
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:
|
case .mismatchedBuildVersion:
|
||||||
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
|
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:
|
case .undeclaredPermissions:
|
||||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
formatter.calendar = Calendar(identifier: .gregorian)
|
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'"
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||||
let dateString = formatter.string(from: Date())
|
let dateString = formatter.string(from: Date())
|
||||||
formattedJSON["date"] = dateString
|
formattedJSON["date"] = dateString
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
|
|||||||
Logger.sideload.notice("Fetching provisioning profiles for app \(self.context.bundleIdentifier, privacy: .public)...")
|
Logger.sideload.notice("Fetching provisioning profiles for app \(self.context.bundleIdentifier, privacy: .public)...")
|
||||||
|
|
||||||
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
|
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
|
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -63,27 +62,25 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
|
|||||||
|
|
||||||
let profile = try result.get()
|
let profile = try result.get()
|
||||||
|
|
||||||
var profiles = [effectiveBundleId: profile]
|
var profiles = [app.bundleIdentifier: profile]
|
||||||
var error: Error?
|
var error: Error?
|
||||||
|
|
||||||
let dispatchGroup = DispatchGroup()
|
let dispatchGroup = DispatchGroup()
|
||||||
|
|
||||||
if !self.context.useMainProfile {
|
for appExtension in app.appExtensions
|
||||||
for appExtension in app.appExtensions
|
{
|
||||||
{
|
dispatchGroup.enter()
|
||||||
dispatchGroup.enter()
|
|
||||||
|
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
|
||||||
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
|
switch result
|
||||||
switch result
|
{
|
||||||
{
|
case .failure(let e): error = e
|
||||||
case .failure(let e): error = e
|
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
|
||||||
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchGroup.leave()
|
|
||||||
|
|
||||||
self.progress.completedUnitCount += 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatchGroup.leave()
|
||||||
|
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,36 +119,6 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
|
|||||||
return value
|
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
|
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
|
// 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.
|
// bundle identifier anyway to prevent collisions with the previous team.
|
||||||
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
|
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
|
||||||
let effectiveParentBundleID = self.context.bundleIdentifier
|
|
||||||
|
|
||||||
let updatedParentBundleID: String
|
let updatedParentBundleID: String
|
||||||
|
|
||||||
if app.isAltStoreApp
|
if app.isAltStoreApp
|
||||||
{
|
{
|
||||||
// Use legacy bundle ID format for AltStore (and its extensions).
|
// 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
|
else
|
||||||
{
|
{
|
||||||
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.
|
||||||
}
|
|
||||||
|
|
||||||
if let parentApp = parentApp,
|
|
||||||
app.bundleIdentifier.hasPrefix(parentBundleID + ".")
|
|
||||||
{
|
|
||||||
let suffix = String(app.bundleIdentifier.dropFirst(parentBundleID.count))
|
|
||||||
bundleID = updatedParentBundleID + suffix
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bundleID = updatedParentBundleID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
|
||||||
}
|
}
|
||||||
|
|
||||||
let preferredName: String
|
let preferredName: String
|
||||||
@@ -267,7 +223,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
|
|
||||||
//process
|
//process
|
||||||
self.fetchProvisioningProfile(
|
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{
|
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
|
||||||
@@ -381,8 +374,8 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// modify Operations are allowed for the app groups and other stuffs
|
// modify Operations are allowed for the app groups and other stuffs
|
||||||
override func fetchProvisioningProfile(for appID: ALTAppID,
|
func fetchProvisioningProfile(appID: ALTAppID,
|
||||||
app: ALTApplication,
|
for app: ALTApplication,
|
||||||
team: ALTTeam,
|
team: ALTTeam,
|
||||||
session: ALTAppleAPISession,
|
session: ALTAppleAPISession,
|
||||||
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||||
@@ -403,7 +396,7 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
|
|||||||
case .success(let appID):
|
case .success(let appID):
|
||||||
|
|
||||||
// Fetch Provisioning Profile
|
// 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
|
entitlements[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (key, value) in [ALTEntitlement.increasedMemoryLimit : true]
|
||||||
|
{
|
||||||
|
entitlements[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
let requiredFeatures = entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
let requiredFeatures = entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||||
return (feature, value)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import CoreData
|
|||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import Roxas
|
import Roxas
|
||||||
import SemanticVersion
|
|
||||||
|
|
||||||
@objc(FetchSourceOperation)
|
@objc(FetchSourceOperation)
|
||||||
final class FetchSourceOperation: ResultOperation<Source>
|
final class FetchSourceOperation: ResultOperation<Source>
|
||||||
@@ -177,6 +176,7 @@ final class FetchSourceOperation: ResultOperation<Source>
|
|||||||
}
|
}
|
||||||
|
|
||||||
try self.verify(source, response: response)
|
try self.verify(source, response: response)
|
||||||
|
try self.verifyPledges(for: source, in: childContext)
|
||||||
|
|
||||||
try childContext.save()
|
try childContext.save()
|
||||||
|
|
||||||
@@ -246,20 +246,66 @@ private extension FetchSourceOperation
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
let incomingSourceID = source.identifier
|
if let previousSourceID = self.$source.identifier
|
||||||
if let previousSourceID = self.$source.identifier,
|
|
||||||
incomingSourceID != previousSourceID
|
|
||||||
{
|
{
|
||||||
// if let version = BuildInfo().marketing_version,
|
guard source.identifier == previousSourceID else { throw SourceError.changedID(source.identifier, previousID: previousSourceID, source: source) }
|
||||||
// SemanticVersion(version)! <= SemanticVersion("0.6.1")!
|
}
|
||||||
// {
|
}
|
||||||
// // delete the source, so that incoming will be saved.
|
|
||||||
// self.source?.managedObjectContext?.delete(self.source!)
|
func verifyPledges(for source: Source, in context: NSManagedObjectContext) throws
|
||||||
// }
|
{
|
||||||
// else
|
guard let patreonURL = source.patreonURL, let patreonAccount = DatabaseManager.shared.patreonAccount(in: context) else { return }
|
||||||
// {
|
|
||||||
throw SourceError.changedID(source.identifier, previousID: self.$source.identifier ?? "nil", source: source)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
}
|
}
|
||||||
|
|
||||||
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber, storeBuildVersion: storeBuildVersion)
|
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber, storeBuildVersion: storeBuildVersion)
|
||||||
installedApp.useMainProfile = self.context.useMainProfile
|
|
||||||
|
|
||||||
installedApp.needsResign = false
|
installedApp.needsResign = false
|
||||||
|
|
||||||
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
|
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
|
||||||
@@ -98,22 +96,22 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
let resignedParentBundleID = resignedApp.bundleIdentifier
|
let resignedParentBundleID = resignedApp.bundleIdentifier
|
||||||
|
|
||||||
let resignedBundleID = appExtension.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("`parentBundleID`: \(parentBundleID)")
|
||||||
print("`resignedParentBundleID`: \(resignedParentBundleID)")
|
print("`resignedParentBundleID`: \(resignedParentBundleID)")
|
||||||
print("`appExBundleID`: \(appExBundleID)")
|
print("`resignedBundleID`: \(resignedBundleID)")
|
||||||
print("`resignedAppExBundleID`: \(resignedBundleID)")
|
print("`originalBundleID`: \(originalBundleID)")
|
||||||
|
|
||||||
let installedExtension: InstalledExtension
|
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
|
installedExtension = appExtension
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: appExBundleID, context: backgroundContext)
|
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
installedExtension.update(resignedAppExtension: appExtension)
|
installedExtension.update(resignedAppExtension: appExtension)
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ class AppOperationContext
|
|||||||
|
|
||||||
var app: ALTApplication?
|
var app: ALTApplication?
|
||||||
var provisioningProfiles: [String: ALTProvisioningProfile]?
|
var provisioningProfiles: [String: ALTProvisioningProfile]?
|
||||||
var appexBundleIds: [String: String]?
|
|
||||||
var useMainProfile = false
|
|
||||||
|
|
||||||
var isFinished = false
|
var isFinished = false
|
||||||
|
|
||||||
|
|||||||
@@ -55,23 +55,8 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
|
|||||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
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,
|
private func removeAppExtensions(from targetAppBundle: ALTApplication,
|
||||||
localAppExtensions: Set<ALTApplication>?,
|
localAppExtensions: Set<ALTApplication>?,
|
||||||
@@ -100,7 +85,7 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
|
|||||||
presentingViewController.present(alertController, animated: true){
|
presentingViewController.present(alertController, animated: true){
|
||||||
|
|
||||||
// if for any reason the view wasn't presented, then just signal that as error
|
// 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." +
|
let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." +
|
||||||
"\nDid you move to different screen or background after starting the operation?"
|
"\nDid you move to different screen or background after starting the operation?"
|
||||||
self.finish(.failure(
|
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
|
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||||
self.finish(.failure(OperationError.cancelled))
|
self.finish(.failure(OperationError.cancelled))
|
||||||
}))
|
}))
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions (Use Main Profile)", comment: ""), style: .default) { (action) in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", 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
|
|
||||||
self.finish(.success(()))
|
self.finish(.success(()))
|
||||||
})
|
})
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||||
do {
|
do {
|
||||||
try Self.removeExtensions(from: targetAppBundle.appExtensions)
|
try Self.removeExtensions(from: targetAppBundle.appExtensions)
|
||||||
try self.updateManifest()
|
|
||||||
return self.finish(.success(()))
|
return self.finish(.success(()))
|
||||||
} catch {
|
} catch {
|
||||||
return self.finish(.failure(error))
|
return self.finish(.failure(error))
|
||||||
|
|||||||
@@ -55,8 +55,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
|||||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||||
|
|
||||||
let effectiveBundleId = self.context.bundleIdentifier
|
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles, appexBundleIds: context.appexBundleIds ?? [:]) { (result) in
|
|
||||||
guard let appBundleURL = self.process(result) else { return }
|
guard let appBundleURL = self.process(result) else { return }
|
||||||
|
|
||||||
// Resign app bundle
|
// Resign app bundle
|
||||||
@@ -66,13 +65,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
|||||||
// Finish
|
// Finish
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let updatedApp = AnyApp(
|
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||||
name: app.name,
|
|
||||||
bundleIdentifier: effectiveBundleId,
|
|
||||||
url: app.fileURL,
|
|
||||||
storeApp: app.storeApp
|
|
||||||
)
|
|
||||||
let destinationURL = InstalledApp.refreshedIPAURL(for: updatedApp)
|
|
||||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||||
print("Successfully resigned app to \(destinationURL.absoluteString)")
|
print("Successfully resigned app to \(destinationURL.absoluteString)")
|
||||||
|
|
||||||
@@ -114,26 +107,22 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
private extension ResignAppOperation
|
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 progress = Progress.discreteProgress(totalUnitCount: 1)
|
||||||
|
|
||||||
let bundleIdentifier = context.bundleIdentifier
|
let bundleIdentifier = app.bundleIdentifier
|
||||||
let openURL = InstalledApp.openAppURL(for: app)
|
let openURL = InstalledApp.openAppURL(for: app)
|
||||||
|
|
||||||
let fileURL = app.fileURL
|
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 identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
|
||||||
guard let profile = context.useMainProfile ? profiles.values.first : profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
|
guard let profile = profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
|
||||||
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||||
|
|
||||||
if let forcedBundleIdentifier = appexBundleIds[identifier] {
|
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||||
infoDictionary[kCFBundleIdentifierKey as String] = forcedBundleIdentifier
|
|
||||||
} else {
|
|
||||||
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
infoDictionary[Bundle.Info.altBundleID] = identifier
|
infoDictionary[Bundle.Info.altBundleID] = identifier
|
||||||
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>"
|
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>"
|
||||||
infoDictionary.removeValue(forKey: "DTXcode")
|
infoDictionary.removeValue(forKey: "DTXcode")
|
||||||
@@ -204,7 +193,7 @@ private extension ResignAppOperation
|
|||||||
if app.isAltStoreApp
|
if app.isAltStoreApp
|
||||||
{
|
{
|
||||||
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID }
|
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.devicePairingString] = "<insert pairing file here>"
|
||||||
additionalValues[Bundle.Info.deviceID] = udid
|
additionalValues[Bundle.Info.deviceID] = udid
|
||||||
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
||||||
@@ -248,7 +237,7 @@ private extension ResignAppOperation
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare app
|
// Prepare app
|
||||||
try prepare(appBundle, bundleID: bundleIdentifier, additionalInfoDictionaryValues: additionalValues)
|
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
||||||
try self.removeMissingAppExtensionReferences(from: appBundle)
|
try self.removeMissingAppExtensionReferences(from: appBundle)
|
||||||
|
|
||||||
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
||||||
@@ -265,8 +254,7 @@ private extension ResignAppOperation
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
guard let appExtension = Bundle(url: fileURL) else { throw ALTError(.missingAppBundle) }
|
guard let appExtension = Bundle(url: fileURL) else { throw ALTError(.missingAppBundle) }
|
||||||
let updatedAppExBundleId = appExtension.bundleIdentifier?.replacingOccurrences(of: app.bundleIdentifier, with: bundleIdentifier)
|
try prepare(appExtension)
|
||||||
try prepare(appExtension, bundleID: updatedAppExBundleId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,13 +38,11 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
{
|
{
|
||||||
let permissionsMode: PermissionReviewMode
|
let permissionsMode: PermissionReviewMode
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
var customBundleId: String?
|
|
||||||
|
|
||||||
init(permissionsMode: PermissionReviewMode, context: InstallAppOperationContext, customBundleId: String? = nil)
|
init(permissionsMode: PermissionReviewMode, context: InstallAppOperationContext)
|
||||||
{
|
{
|
||||||
self.permissionsMode = permissionsMode
|
self.permissionsMode = permissionsMode
|
||||||
self.context = context
|
self.context = context
|
||||||
self.customBundleId = customBundleId
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@@ -67,8 +65,7 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
|
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
|
||||||
let bundleId = customBundleId ?? app.bundleIdentifier
|
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
||||||
guard bundleId == self.context.bundleIdentifier else {
|
|
||||||
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
|
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,16 +82,17 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) }
|
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.verifyHash(of: app, at: ipaURL, matches: appVersion)
|
||||||
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
|
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
|
||||||
|
try await self.verifyPermissions(of: app, match: 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.finish(.success(()))
|
self.finish(.success(()))
|
||||||
}
|
}
|
||||||
@@ -131,17 +129,24 @@ private extension VerifyAppOperation
|
|||||||
{
|
{
|
||||||
let (version, buildVersion) = await $appVersion.perform { ($0.version, $0.buildVersion) }
|
let (version, buildVersion) = await $appVersion.perform { ($0.version, $0.buildVersion) }
|
||||||
|
|
||||||
// marketplace buildVersion validation
|
let downloadedIpaRevision = Bundle(url: app.fileURL)!.object(forInfoDictionaryKey: "BuildRevision") as? String ?? ""
|
||||||
if let buildVersion
|
let sourceJsonIpaRevision = appVersion.revision
|
||||||
{
|
|
||||||
guard buildVersion == app.buildVersion else {
|
// if not beta but version matches, then accept it, else compare revisions between source and downloaded
|
||||||
throw VerificationError.mismatchedBuildVersion(app.buildVersion, expectedVersion: buildVersion, app: app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if version != app.version {
|
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
|
func verifyPermissions(of app: ALTApplication, @AsyncManaged match appVersion: AppVersion) async throws
|
||||||
|
|||||||
281
AltStore/Operations/VerifyAppPledgeOperation.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Original</string>
|
<string>Original</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>App</string>
|
|
||||||
<key>iconName</key>
|
|
||||||
<string>AppIcon</string>
|
<string>AppIcon</string>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
@@ -20,88 +18,66 @@
|
|||||||
<string>Blue</string>
|
<string>Blue</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Blue</string>
|
<string>Blue</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>BlueIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Dark</string>
|
<string>Dark</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Dark</string>
|
<string>Dark</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>DarkIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Honeydew</string>
|
<string>Honeydew</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Honeydew</string>
|
<string>Honeydew</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>HoneydewIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Pride</string>
|
<string>Pride</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Pride</string>
|
<string>Pride</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>PrideIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Sandy</string>
|
<string>Sandy</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Sandy</string>
|
<string>Sandy</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>SandyIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Sky</string>
|
<string>Sky</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Sky</string>
|
<string>Sky</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>SkyIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Snow</string>
|
<string>Snow</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Snow</string>
|
<string>Snow</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>SnowIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Starburst</string>
|
<string>Starburst</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Starburst</string>
|
<string>Starburst</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>StarburstIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Storm</string>
|
<string>Storm</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Storm</string>
|
<string>Storm</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>StormIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Vista</string>
|
<string>Vista</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Vista</string>
|
<string>Vista</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>VistaIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>Winter</string>
|
<string>Winter</string>
|
||||||
<key>imageName</key>
|
<key>imageName</key>
|
||||||
<string>Winter</string>
|
<string>Winter</string>
|
||||||
<key>iconName</key>
|
|
||||||
<string>WinterIcon</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 50 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "App.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Blue.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Dark.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 373 KiB After Width: | Height: | Size: 373 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Honeydew.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 719 KiB After Width: | Height: | Size: 719 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Pride.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 352 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Sandy.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Sky.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 453 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Snow.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Starburst.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 464 KiB After Width: | Height: | Size: 464 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Storm.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Vista.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||