Compare commits

...

186 Commits

Author SHA1 Message Date
mahee96
a6be43da53 debloat: remove patreon stuff carried over from altstore 2.0...not required by sidestore in-app since sidestore manages in web + remove old tests from altstore 2026-02-22 14:54:01 +05:30
mahee96
c807154873 cleanup: removed analytics stuff - we can add a decent analytics management later if we need 2026-02-22 13:57:43 +05:30
mahee96
1103a7f0b4 bug-fix: should be checking for iconName and not imageName anymore 2026-02-22 11:27:05 +05:30
mahee96
8a63725113 icons: use 256x256 for preview imageset since 75x75 was too poor 2026-02-22 11:19:50 +05:30
mahee96
4a73efe680 icons: fix incorrect name entry 2026-02-22 11:01:15 +05:30
mahee96
be4d62fb1e bug-fix: emp for wireguard requires an enum entry for the indexPath calc 2026-02-22 10:59:04 +05:30
mahee96
7683447eea icons: change app icon screen was not showing icon previews - added tiny 75x75 imageset assets since iOS 18+ cannot use AppIconSet for UIImage(named:) 2026-02-22 10:58:32 +05:30
mahee96
d18422af00 CI: skip info on upstream branch for deploy message in nightly since it is already the last beta before stable 2026-02-22 07:39:24 +05:30
mahee96
0062374d32 CI: disabled test tracks completely by commenting out 2026-02-22 07:29:56 +05:30
mahee96
d45803a7cc settings: split out beta testing section so that users can enable it confidently as they see 2026-02-22 07:25:07 +05:30
mahee96
661fda4988 CI: fixed the heredoc parsing issues when single quotes are present in the notes by using a local function in shell script 2026-02-22 06:22:41 +05:30
mahee96
83ed17c443 cleanup: improved messaging for appID customization 2026-02-22 05:31:26 +05:30
mahee96
096bb85bc1 bug-fix: customizeAppId key should be used from UserDefaults.standard instead of UserDefaults.shared 2026-02-22 05:31:10 +05:30
mahee96
786f06800f bug-fix: main-profile processing should account for the effective AppID which is in context.bundleIdentifier 2026-02-22 05:30:39 +05:30
mahee96
146b69944a bug-fix: switch toggle handler shouldn't disable the switch itself 2026-02-22 05:29:55 +05:30
mahee96
37cc2cfca6 cleanup: updated strings and variables to reflect appExBundleID 2026-02-22 04:11:58 +05:30
mahee96
31fe7fed8c bug-fix: appEx bundleID when bundleID is overridden, wasn't handled correctly in PR #1158 2026-02-22 04:11:25 +05:30
Magesh K
dbc20391e5 Merge pull request #1158 from SideStore/appId-customization
Feature: AppId customization during manual IPA install via "My Apps" screen
2026-02-20 05:36:24 +05:30
Magesh K
aca8193f1c Merge branch 'develop' into appId-customization 2026-02-20 05:35:26 +05:30
ny
fabe093b1d fix: remove pointless error toasts 2026-02-17 12:44:25 -05:00
mahee96
277b5b0bd4 settings: added new switch to allow enabling appId customization (which will be shown during install) 2026-02-07 07:59:39 +05:30
peroka-net
e1550811eb feat: Add UntitledCharts to Trusted Sources (#1160)
* Add new trusted app 'com.sbuga.retrosekai'
2026-02-04 11:05:06 -05:00
mahee96
255db2bac0 fix: cached app after download was not respecting overriden appId/bundleId 2026-02-02 11:28:48 +05:30
mahee96
30c03aad42 feature: added a prompt before installing to customize appId 2026-02-02 08:44:49 +05:30
mahee96
d70c916222 cleanup - removed unused commented code (already validated in 0.6.2 that refresh path doesnt need reinstalls since it only resigns the app) 2026-02-02 08:41:42 +05:30
mahee96
7c038568f8 added gitignore for minimuxer submodule 2026-02-02 08:39:50 +05:30
mahee96
6d511de31d Fix: base bundleID for AltBackup debug config was incorrect 2026-02-02 08:39:32 +05:30
CelloSerenity
8730ae42c6 Update trustedapps.json 2026-01-26 23:14:00 -05:00
CelloSerenity
7d7a44bc61 Update trustedapps.json
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2026-01-26 14:16:05 -05:00
Huge_Black
639753149f fix typo & log token login error 2026-01-24 23:43:50 -05:00
Huge_Black
cc5aeaf099 Authentication rework, cache session, save and login with adsid and xcodeToken to alleviate GSA connection issue in some regions. Fix a timezone issue when extracting anisette data 2026-01-24 23:43:50 -05:00
basketshoe
1d642e2ffa Update pairing file help link in alert (#1147)
Signed-off-by: basketshoe <54729294+basketshoe@users.noreply.github.com>
2026-01-10 14:33:52 -05:00
CelloSerenity
decf21f177 Update trusted apps identifiers and source URLs
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-12-17 23:21:03 -05:00
CelloSerenity
b2f42160f9 Sources: Update/Remove Sources from Trusted Sources (#1131)
* Add new trusted apps and remove outdated ones

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update trustedapps.json

* Update trustedapps.json

Co-authored-by: Stern <xsternent@gmail.com>
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update trustedapps.json

---------

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
Co-authored-by: Stern <xsternent@gmail.com>
2025-12-16 20:03:50 -05:00
CelloSerenity
4f8a70d31e LocalDevVPN (#1126) 2025-12-04 21:40:39 -05:00
Stossy11
bedda8c6a4 Merge pull request #1078 from CelloSerenity/csdev
Update error codes
2025-11-25 08:06:18 +11:00
CelloSerenity
d042267ff9 Update error messages to reference iloader
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-11-20 16:50:45 -07:00
CelloSerenity
4cc534c89e Merge branch 'SideStore:develop' into csdev 2025-11-20 16:49:02 -07:00
mahee96
606140a7be entitlements: code_sign_entitlements declaration was missing in AltStore.debug/release modes causing AppGroups access failure (nil in getPreviousBackupURL) when long pressing app entry in MyApps list
Default entitlements are to come from AltStoreFree.entitlements, for paid entitlements one can use CodeSigning.xcconfig to override it to point to AltStore.entitlements instead.
2025-11-16 04:41:26 +05:30
mahee96
55761029a3 Fix: removed forced entry of libem_proxy-sim.a from depenencies which would otherwise cause build failure when targeting iOS device 2025-11-16 03:24:56 +05:30
mahee96
4e6756d4d5 EMP-toggle: UI corner radius fix 2025-11-16 03:07:29 +05:30
mahee96
c64b0c99de EMP: added back EMP entry/exit points as it was in 0.6.1 but with option to enable/disable it via settings 2025-11-16 03:01:29 +05:30
mahee96
e19b147962 EMP: added back em_proxy as it was in 0.6.1 2025-11-16 03:00:43 +05:30
mahee96
85427ecbc6 CI: deploy - added filtering to upload only available files 2025-11-16 01:25:01 +05:30
mahee96
ecf5e2d770 CI: delete obsoleted scripts 2025-11-16 01:24:24 +05:30
mahee96
2f2df9ca43 CI: refine upload list in deploy job based on test enablement 2025-11-16 01:02:42 +05:30
mahee96
31520354bd CI: remove pod related steps 2025-11-16 00:51:02 +05:30
mahee96
25f13faa67 Pods: removed pods entirely and replace pod dependencies with swift packages 2025-11-16 00:46:51 +05:30
Pratik Lohani
dcaddcc219 Fix expired Discord link in README.md (#1101) 2025-11-14 14:24:35 -05:00
CelloSerenity
fa1757ff1d Merge branch 'SideStore:develop' into csdev 2025-11-08 19:54:10 -07:00
CelloSerenity
f0cb86aff4 Update README.md 2025-11-08 19:49:50 -07:00
CelloSerenity
7a34d7ac67 Fix version numbering 2025-11-08 19:23:59 -07:00
ny
cfd9247fdd fix: copied too much (*sigh* UIKit) 2025-11-07 17:17:08 -05:00
ny
15ffe766d3 fix: Account exporting/importing 2025-11-07 16:54:39 -05:00
nythepegasus
a0ae0cb2b1 feat: Updated deprecated Pojav identifier to new Amethyst repo
Signed-off-by: nythepegasus <mobile@nythepegas.us>
2025-10-22 17:04:05 -04:00
mahee96
dc53f19947 UITests: disable spotlight dismiss which is causing failures in test 2025-10-16 00:16:11 +05:30
mahee96
baf3594a8e UITests: added more idling waits since we removed(mocked) quiescent idling wait from testing framework at runtime. 2025-10-15 23:33:48 +05:30
mahee96
1d4666e79e UITests: fixes for iOS 26 compatibility 2025-10-15 22:52:31 +05:30
mahee96
b4df06f742 CI: updated to macOS 26 + xcode 26 for runners 2025-10-15 20:32:42 +05:30
mahee96
d41a6b17d2 iOS26: added support for iOS 26 deployment target + CI (fixed layout issues, added splash screen, fixed nav title insets). 2025-10-15 20:22:35 +05:30
CelloSerenity
d98e0dde72 Update OperationError.swift
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-10-10 15:34:12 -06:00
CelloSerenity
f23a7e9c82 Enhance error messages for UDID and SideJIT issues
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-10-10 14:56:54 -06:00
CelloSerenity
7ed45c7cb1 Update error messages for clarity and detail
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-10-10 14:53:39 -06:00
ny
c319524df6 feat: add the ability to import/export account data 2025-09-07 13:47:04 -04:00
mahee96
724e8db980 - [ConsoleLogger]: Fix writing large data by chunking + stale handle capture 2025-08-06 02:03:18 +05:30
mahee96
669d33183e - [ConsoleLogger]: Fix race conditions during shutdown and possible crashes by writing to closed stream handler 2025-07-30 23:35:24 +05:30
CelloSerenity
a12b6cd62b Small fixes (#1020)
* here

* Update PatreonComponents.swift

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update ErrorLogViewController.swift

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update README.md

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update trustedapps.json

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Revert "Update trustedapps.json"

This reverts commit 10a38895ea.

* Update Build.xcconfig

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

---------

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-07-18 21:24:55 -04:00
Huge_Black
685d956775 Use main profile when refreshing (#1013) 2025-06-22 08:31:13 -04:00
Huge_Black
00ed6e61be Add option for using main bundle's profile for app extensions, update AltSign & libmd (#1012) 2025-06-22 04:13:15 -04:00
neo
7057d59992 Merge pull request #1011 from CelloSerenity/develop 2025-06-14 17:54:05 -04:00
CelloSerenity
6d308487c1 Update LaunchViewController.swift
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-06-14 14:04:20 -06:00
Huge_Black
6c45eb096f Add import/export certificate feature (#1008) 2025-06-12 00:47:33 -04:00
Stern
4b6ffa5d4a Merge pull request #995 from CelloSerenity/develop
Update OperationError.swift for idevice_pair
2025-05-24 21:06:24 -04:00
CelloSerenity
728b12d004 Update OperationError.swift
Co-authored-by: Stern <xsternent@gmail.com>
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-05-24 19:04:25 -06:00
CelloSerenity
97160569ba Update OperationError.swift 2025-05-24 18:52:56 -06:00
polymo1
1b754e137a Merge pull request #987 from owoellen/patch-1
Change front-facing references to AltStore
2025-05-19 18:35:07 -04:00
ellen!
4176b8c83c Update LockScreenWidget.swift
Signed-off-by: ellen! <78277148+owoellen@users.noreply.github.com>
2025-05-19 22:45:50 +01:00
ellen!
479f877dbf Update Authentication.storyboard
Signed-off-by: ellen! <78277148+owoellen@users.noreply.github.com>
2025-05-19 22:43:45 +01:00
Stossy11
4ff643805b Feat: Add Pairing File Export URL callback and replace Wireguard with StosVPN (#962)
* Add Pairing File Export URL callback and replace Wireguard with StosVPN

* Fix Data()

* Add Switching Tab

* Fix Pairing Callback Template and fix spelling mistake

* Add Observer for openExportPairingFileConfirm

* Add Logging and fix certificate callback

* add func to Scene Delegate

* Fix ToastView and Logging

* Remove ?

* Fix Logging

* Call AppDelegate instead of SceneDelegate

* Fix Scene Delegate

* Set back SceneDelegate

* Add Logging

* Fix error

* Fix Case Sensitive

* Remove openExportPairingFileConfirm and fix AppDelegate.exportPairingFileNotification

* Remove Alert

* Remove Unnecessary code

* rename export() to exportPairingFile()
2025-04-24 01:48:29 -04:00
Huge_Black
0e3c3dddfe Add export certificate feature (#959) 2025-04-19 20:48:50 -04:00
Jackson Coxson
7415fe6204 Bump minimuxer 2025-04-16 15:16:24 -04:00
Jackson Coxson
c128c9268b Update SideStore for new minimuxer 2025-04-16 15:16:24 -04:00
Magesh K
131a0289a2 Merge pull request #935 from mahee96/develop-minimuxer
Disable minimuxer ready check which shows NO WIFI/VPN error since stosVPN can work on cellular too
2025-04-09 00:03:04 -07:00
Magesh K
116f045e51 Merge pull request #945 from SideStore/develop-alpha
StosVPN-integration: Removed em_proxy and EmotionalDamage module in favour of stosVPN
2025-04-08 23:39:58 -07:00
Magesh K
c68efd2b44 Merge branch 'develop' into develop-alpha 2025-04-08 23:38:52 -07:00
mahee96
e519389780 - bump version to 0.6.2 for next release cycle 2025-04-08 23:37:25 -07:00
mahee96
e6135c6518 em_proxy: removed em_proxy submodule from git 2025-04-08 23:08:57 -07:00
mahee96
3d444f301d - stosVPN-integration: Removed em_proxy and EmotionalDamage module in favour of stosVPN 2025-04-08 23:03:49 -07:00
mahee96
7adfd3d3e8 Merge branch 'develop' into develop-alpha 2025-04-08 22:46:26 -07:00
polymo1
591913743e Merge pull request #940 from Br0des/develop
Added StosVPN to the EM Proxy part.
2025-04-05 22:38:53 -04:00
Brodes
77d95fe278 Added StosVPN to the EM Proxy part.
yeah i'm petty >:3

Signed-off-by: Brodes <144500576+Br0des@users.noreply.github.com>
2025-04-05 20:13:07 -06:00
mahee96
7ec6324b62 - minimuxer-ready: disabled minimuxer ready check which shows NO WIFI/VPN error since stosVPN can work on cellular too. 2025-04-03 00:49:07 -07:00
Joseph LaFreniere
0cd62d371a [Skip Ci] Fix typo "levaraging" -> "leveraging" (#926)
Signed-off-by: Joseph LaFreniere <git@lafreniere.xyz>
2025-03-31 15:12:25 -07:00
Magesh K
9771f6bb9a Merge pull request #920 from mahee96/develop
Migration: Fixes for migration issues when migrating from 0.5.9 to 0.6.0
2025-03-25 20:37:16 -07:00
mahee96
e553efbad5 - migration-fix: more reinforcements for 0.6.0 to 0.6.1 2025-03-24 01:32:01 -07:00
mahee96
a4dfd28a3c - [Fix]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-24 00:19:05 -07:00
mahee96
291d7fd8d9 - [Fix]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-23 21:44:06 -07:00
mahee96
a7496e08e3 - [WIP]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-23 12:09:58 -07:00
mahee96
2f3be07b5d - fix: attempt to fix "app no longer available" issues due to fetch profiles not updating appFeatures and appGroups (possible regression from PR#846) 2025-03-23 12:00:31 -07:00
mahee96
b57d279670 - [WIP]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-23 11:59:01 -07:00
mahee96
cbde3e6495 Revert "- Fixed: attempt migrations fix from 0.5.10 to 0.6.0"
This reverts commit ae8e9a3506.
2025-03-23 11:57:16 -07:00
polymo1
117f31e158 Merge pull request #915 from neoarz/patch-3
Update SettingsViewController.swift
2025-03-15 10:13:42 -04:00
neoarz
420efcbb11 Update SettingsViewController.swift
Signed-off-by: neoarz <164915254+neoarz@users.noreply.github.com>
2025-03-15 06:30:03 -04:00
mahee96
dc29b65bd5 - fix: attempt to fix "app no longer available" issues due to fetch profiles not updating appFeatures and appGroups (possible regression from PR#846) 2025-03-15 01:58:08 +05:30
Zero King
1e64f50ab9 fix: typo in hasUpdate comparison
Signed-off-by: Zero King <l2dy@icloud.com>
2025-03-13 03:29:02 +05:30
mahee96
ae8e9a3506 - Fixed: attempt migrations fix from 0.5.10 to 0.6.0 2025-03-11 04:44:23 +05:30
Magesh K
3785891923 Merge pull request #900 from l2dy/patch-1
fix: typo in hasUpdate comparison
2025-03-04 03:33:32 +05:30
Zero King
e85db67ac7 fix: typo in hasUpdate comparison
Signed-off-by: Zero King <l2dy@icloud.com>
2025-03-02 01:47:04 +08:00
mahee96
39d0835f5b - CI: updated stable.yml for recent fixes 2025-03-01 01:30:15 +05:30
mahee96
c8127fb3b9 - CI: updated stable.yml for recent fixes 2025-02-28 23:41:31 +05:30
Magesh K
729fca9100 Merge pull request #897 from SideStore/markdown-for-update-description
Feature: Render in-app update description as (full) markdown
2025-02-28 05:22:37 +05:30
mahee96
c6703d66c1 - Feature: Markdown view integration complete (if issues arrise can fix it asap) 2025-02-28 05:09:37 +05:30
mahee96
2197161d55 - Fix: CollapsingMarkdownView was not reloading properly during layoutSubviews phase 2025-02-28 02:28:34 +05:30
mahee96
cfaf79f878 - Fixes: disabled animations for the CollapsingMarkdownView when expanding/collapsing due to visual glitches 2025-02-28 01:02:41 +05:30
mahee96
2bea980d1f - Feature: Added markdown rendering for in-app update description field 2025-02-27 23:39:03 +05:30
mahee96
f11e27c712 - CI: serialize on whole workflow instead of individual jobs 2025-02-27 05:37:35 +05:30
mahee96
b316e84f0d - CI: serialize on whole workflow instead of individual jobs 2025-02-27 05:36:41 +05:30
mahee96
4668f8499b - CI: fix release notes updater for invalid revision as input 2025-02-27 05:23:20 +05:30
mahee96
f9aedaba04 - CI: fix release notes updater for invalid revision as input 2025-02-27 05:22:11 +05:30
mahee96
8cb3de9ab5 - CI: enable build on push for nightly temporarily 2025-02-27 05:03:42 +05:30
mahee96
ca57d58219 - CI: fix: deploy errors 2025-02-27 05:01:27 +05:30
mahee96
6a56fbd206 -CI: fix: deploy must run if tests were sccessful or skipped 2025-02-27 04:57:34 +05:30
mahee96
cec3825de0 - Makefile: build-tests need destination param same as run-tests 2025-02-27 04:44:35 +05:30
mahee96
b3e99d1ae3 - CI: Try composite build 2025-02-27 04:30:45 +05:30
mahee96
7243d79646 - UITests: Fix issue with UITests bundleID causing test failures in CI 2025-02-27 02:50:16 +05:30
Magesh K
e50da6603c - CI: replaced irgaly/xcode-cache with gh-actions-cache
- CI: serialization bug fix - reverted concurrency lock to workflow level instead of job level which was causing issues due to incorrect cache at expected jobs
- CI: Boot simulator in async instead of doing in a blocking step
- CI: integrate publishing release notes
- CI: moved posting to beta-build-num repo into build-job instead of deploy coz we want to roll the beta-build-num for each success of build-job even if the workflow failed.
2025-02-27 02:49:20 +05:30
Magesh K
136f07e4b9 Merge pull request #894 from SideStore/coredata-migration-fix
Version 0.6.0: CoreData Migrations fix including xcdatamodels and xcmappingmodels
2025-02-26 12:57:22 +05:30
Magesh K
f4d367b857 - Migrations-Fix: fixed the migrations by creating custom migration policy for Source and StoreApp to support sourceID renaming and ReleaseTracks integration respectively, removed intermediate models and xcmappingmodels since we jumped directly from altstore 1.6.3 to 2.0.x 2025-02-26 12:43:26 +05:30
Magesh K
3e96583525 Merge pull request #888 from SideStore/develop-alpha
Feature: Bulk sources-add in sources screen with capability to stage them before persisting into database
2025-02-26 11:47:46 +05:30
Magesh K
84bb1f7c08 - Bug-Fix: use normal keyboardType instead of url keyboardType for input field in add-source view 2025-02-24 20:09:14 +05:30
Magesh K
a5aec978bb - CI: Optimization: Split tests-build from tests-run so that tests-run failure doesn't invalidate last build in cache 2025-02-24 19:42:07 +05:30
Magesh K
d677292bd3 - CI: Fix: VERSION_IPA was not using $marketing_version 2025-02-23 23:16:18 +05:30
Magesh K
722f67d3c7 - UITests: Fix: exclude pojavlauncher from testing since it is unreliable and fetch fails 2025-02-23 22:16:40 +05:30
Magesh K
07e0aea24f - CI: Split sequential steps into build, deploy, publish jobs and use concurrency for build and test 2025-02-23 21:17:41 +05:30
Magesh K
673f2ba693 - UITests: Added more tests for testing repeatability 2025-02-23 18:04:57 +05:30
Magesh K
0070519736 - UITests: Fixes for setup/teardown and added repeatability tests 2025-02-23 15:12:55 +05:30
Magesh K
359b38609b - CI: Makefile and CI bug-fixes 2025-02-22 20:30:56 +05:30
Magesh K
348a24d885 - UITest-Fix: Add proper delays to wait for the UI Elements to appear on screen 2025-02-22 20:15:02 +05:30
Magesh K
ebdd0d4cb4 - UITest-Fix: use home-screen(springboard) to delete the app coz sometime spotlight indexing is not up-to-date and this causes issues. 2025-02-22 19:55:05 +05:30
Magesh K
614ab4cd33 - CI: improvements: dispatch simulator boot up in background and other fixes 2025-02-22 19:11:13 +05:30
Magesh K
ca38008328 - .gitignore: Added more files to gitignore 2025-02-22 18:39:07 +05:30
Magesh K
e5713fa3a9 - CI: Fixes to make build re-useable 2025-02-22 18:35:49 +05:30
Magesh K
35e3cf1e14 - UITests: Fixed some issues with CI simulator delays 2025-02-22 18:35:23 +05:30
Magesh K
ca8c394ae0 - CI: Improve build log capture in case of errors 2025-02-22 17:12:12 +05:30
Magesh K
5323fdadcf - CI: Included tests for CI builds 2025-02-22 02:37:30 +05:30
Magesh K
e43bff5f8f - Makefile: Added test and clean targets, fixed override params bug when empty 2025-02-22 01:44:24 +05:30
Magesh K
4659d617f8 - Bug-Fix: bulk added source previews were shown out of order 2025-02-22 00:25:49 +05:30
Magesh K
87fe360927 - UnitTests: Moved DS unit tests into their own target 2025-02-21 19:24:40 +05:30
Magesh K
71212130c5 - Bug-Fix: Use LinkedHashMap instead of swift standard dict which preserves insertion order 2025-02-21 19:09:07 +05:30
Magesh K
6370105c85 - Feature: Implement Bulk add for Sources 2025-02-21 19:06:17 +05:30
Magesh K
15f4ae7b5a - UnitTests: Added unitttests for new datastructures - LinkedHashMap and TreeMap 2025-02-21 19:04:40 +05:30
Magesh K
08e11eece4 - DataStructures: Implemented LinkedHashMap and TreeMap (RB Tree) in swift 2025-02-21 19:03:28 +05:30
Magesh K
1a43ad4aa3 - UITests: Added new - testBulkAddRecommendedSources() 2025-02-21 18:01:40 +05:30
Magesh K
a5ec12e3df - UITests: Setup for UI Tests 2025-02-21 12:20:18 +05:30
Magesh K
c0400446bc - Settings-Fix: deselect rows after user selection and before performing actions 2025-02-16 21:32:24 +05:30
Magesh K
13c3d0c1e9 - Fix: Use "EXPIRED" marker in MyApps screen instead of -ve interval 2025-02-16 20:48:06 +05:30
Magesh K
92edd4b800 - Renamed beta track to nightly 2025-02-16 20:28:57 +05:30
Magesh K
eb0e1326b9 - ReleaseTracks: Added in-app ReleaseTracks switching support 2025-02-10 13:53:31 +05:30
Magesh K
a8fd1a3e83 - Minor fixes and cleanup 2025-02-09 17:31:00 +05:30
Magesh K
533655c96b - Fixed spacing in App View title 2025-02-08 20:28:29 +05:30
Magesh K
a322f9b5e9 - Make unused AppPermissions attribs optional 2025-02-08 14:24:00 +05:30
Magesh K
4805ed8d3b - CI: fix typo 2025-02-08 14:24:00 +05:30
Magesh K
caa38cfcae - CI: updated nightly to print new commits when running on schedule 2025-02-08 13:50:45 +05:30
Magesh K
77833c6ffc - Store: Reverted localized version for Store and version to be independent 2025-02-08 13:38:37 +05:30
Magesh K
61086a681a - Beta-Builds: Updated bundleID suffix with beta build to match source 2025-02-08 12:17:45 +05:30
Magesh K
cf81d2876c - Settings: Fixed last row corners not rounded 2025-02-08 11:56:42 +05:30
Magesh K
e5144d112a - CI update for nightly 2025-02-08 11:44:40 +05:30
Magesh K
bf766c1b84 - Multiple fixes and CI setup 2025-02-08 11:36:55 +05:30
Magesh K
e608211f32 Merge pull request #871 from neoarz/develop
Fix Repository App Previews for “countdown”
2025-02-03 05:11:07 +05:30
neoarz
ef9135d7ee Update StoreApp.swift 2025-02-02 17:52:57 -05:00
neoarz
8b2c92d94c Merge branch 'SideStore:develop' into develop 2025-02-02 17:34:42 -05:00
Magesh K
ac486a4723 [DisableIdleTimeout]: Fix: account for concurrency 2025-02-02 04:38:46 +05:30
Magesh K
c3847276f7 [DisableIdleTimeout]: Fix: moved setting isIdleTimerDisabled = false to AppManager.finish() 2025-02-02 04:18:36 +05:30
Magesh K
e43e962bcc [AppIDs-View]: updated header section size 2025-02-02 03:55:11 +05:30
Magesh K
0245f6072a [cleanup]: Sources tab add source storyboard 2025-02-02 03:55:11 +05:30
Magesh K
ac63314a91 [cleanup]: Disable patrons related stuff as it is non-functional 2025-01-29 02:48:12 +05:30
Magesh K
803eb615cd [Fix]: VerifyError message was missing version info for mismatched versions 2025-01-29 02:38:27 +05:30
Magesh K
b218437388 [Fix]: Throw error only if the popup is not displayed 2025-01-29 02:04:00 +05:30
Magesh K
df2ffb1235 [Roxas]: updated with upstream 2025-01-28 22:38:29 +05:30
Magesh K
7ed8c20dfc [Backup]: Fix: update logic during install missed since override keyword was missing 2025-01-22 04:41:46 +05:30
Magesh K
203a7e6f11 [CI]: set to BUILD_CHANNEL based on CI build type 2025-01-22 02:02:50 +05:30
Magesh K
c6f843ebc3 [BuildInfo]: Added bundleInfo inspection to create version tag in Settings screen 2025-01-22 01:47:21 +05:30
Magesh K
13d924abf6 [CONTRIBUTING]: updated build steps 2025-01-21 23:47:46 +05:30
Magesh K
1641f6e93f [ToastView]: Fix: restore back to printing localizedDescription as before 2025-01-21 21:54:11 +05:30
neoarz
c0a81edf6b Update trustedapps.json
Signed-off-by: neoarz <164915254+neoarz@users.noreply.github.com>
2025-01-21 07:05:16 -05:00
neoarz
e9391b7a21 Update trustedapps.json
Signed-off-by: neoarz <164915254+neoarz@users.noreply.github.com>
2025-01-20 23:32:24 -05:00
226 changed files with 13391 additions and 13584 deletions

63
.github/maintenance/cache.py vendored Normal file
View File

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

View File

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

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# Ensure we are in root directory
cd "$(dirname "$0")/../.."
DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1
write() {
sed -e "/MARKETING_VERSION = .*/s/$/-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 Executable file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,105 @@
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 Normal file
View File

@@ -0,0 +1,358 @@
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 Normal file
View File

@@ -0,0 +1,281 @@
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 Normal file
View File

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

View File

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

View File

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

View File

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

8
.gitignore vendored
View File

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

6
.gitmodules vendored
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -2,24 +2,14 @@
<!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.files.user-selected.read-write</key>
<array>
<string></string>
</array>
<key>com.apple.developer.applesignin</key>
<array>
<string></string>
</array> -->
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
<true/>
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
<true/>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -538,7 +538,8 @@ private extension BrowseViewController
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)
}
@@ -563,7 +564,8 @@ private extension BrowseViewController
}
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(_:))
}

View File

@@ -482,7 +482,8 @@ private extension FeaturedViewController
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)
}
@@ -500,7 +501,8 @@ private extension FeaturedViewController
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(_:))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -319,7 +319,8 @@ private extension NewsViewController
let app = self.dataSource.item(at: indexPath)
guard let storeApp = app.storeApp else { return }
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
{
self.open(installedApp)
}
@@ -338,7 +339,8 @@ private extension NewsViewController
}
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(_:))
}

View File

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

View File

@@ -99,7 +99,10 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
self.finish(.failure(RefreshError(.noInstalledApps)))
return
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
if UserDefaults.standard.enableEMPforWireguard {
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
}
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

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

View File

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 313 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 719 KiB

After

Width:  |  Height:  |  Size: 719 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 352 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 453 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 464 KiB

After

Width:  |  Height:  |  Size: 464 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

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