Compare commits

...

386 Commits

Author SHA1 Message Date
June
d49098a8be better notifications 2024-08-17 23:48:05 +09:00
June Park
b4e18c50d3 Selective app extension removal (#677) 2024-08-16 21:28:42 -04:00
June Park
d18482a04a [no ci] Update pr.yml to cache builds
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-17 10:14:04 +09:00
June
f3d9dd777d Make app extension popup less annoying by not showing when refreshing (it doesn't do anything anyway) 2024-08-17 00:16:55 +09:00
June Park
e117c4b9a3 Remove cache clear
We don't need to do this anymore

Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 15:41:02 +09:00
June
95666178e5 Fix certificate issues 2024-08-16 15:34:57 +09:00
June Park
56403466b9 [skip ci] add caches to stable.yml
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 14:32:25 +09:00
June Park
c7344ef548 [skip ci] add caches to beta
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 14:30:47 +09:00
June Park
71c4abfce8 Make caches not have workflow name
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 14:30:13 +09:00
nythepegasus
0613af2240 Bump SideStore Version to 0.5.8
For this current nightly track of commits until we "officially" release it

Signed-off-by: nythepegasus <me@nythepegas.us>
2024-08-16 01:22:58 -04:00
J. Laymon
dd832ad6df Better pairing file info (#676)
makes the pop up give you a button to press for help rather than giving a plaintext url to a page that doesn't exist
2024-08-16 01:20:53 -04:00
June P
8cf7bc9998 [build] need to see if caching works 2024-08-16 13:40:04 +09:00
June
c1cf11c04c Increase runtime performance 2024-08-16 13:32:35 +09:00
June Park
0058c40f46 Add incremental builds
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-16 13:17:07 +09:00
June
1397389f95 Make app extensions optional across the board 2024-08-16 12:58:06 +09:00
June Park
6a2d3e1d22 Delete AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-15 22:13:19 +09:00
June
46ac704013 more descriptive machine names for generated certs 2024-08-15 22:09:16 +09:00
June Park
0fdab2a5c5 Makes revoking optional (#675) 2024-08-14 20:08:37 -07:00
June Park
8f4586bfef Add nightly.link description
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-15 12:00:39 +09:00
June
52d0c9861f More descriptive errors 2024-08-15 11:21:50 +09:00
stossy11
feace61eb4 Fix SideJITServer Support for iOS 17+ (#674)
* FIx SideJITServer Support

* Fix SideJITServer Address

* Add Warning when Overwriting SideJITServer Address

* Fix Optional Value for SideJITServer URL

* Update SideJITServer Address Overwriting

* Fix Enabling JIT and Fix IP Address Loop

* Fix No WiFi or VPN! error when using SideJITServer
2024-08-14 16:58:26 -07:00
June Park
1f5cc8f283 Make it so we don't flood altstore
Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-14 21:34:22 +09:00
June Park
60b8520237 Add nightly.link support
Pushing directly as this shouldn't mess anything other than CI up

Signed-off-by: June Park <me@pythonplayer123.dev>
2024-08-14 21:14:27 +09:00
Stern
6a67c5e9a2 Merge pull request #673 from 0-Blu/develop
Added more feedback options.
2024-08-13 14:48:17 -04:00
Stephen
bcc241518c Update SettingsViewController.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-13 14:15:31 -04:00
Stephen
6dfa8f1556 Update SettingsViewController.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 22:30:15 -04:00
Stephen
19dde692b2 Added more feedback options.
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 19:21:59 -07:00
Stern
14dc93b5d2 Merge pull request #672 from 0-Blu/develop
UI Improvements for the Anisette Servers View.
2024-08-12 20:46:44 -04:00
Stephen
547620235e Update AnisetteServerList.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 20:31:13 -04:00
Stephen
b43fd0a54b Fixed Dark Mode for AnisetteServerList.swift
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 16:53:45 -04:00
Stephen
8b782c9416 Better AnisetteServerList.swift (NEEDS TESTING!!!!!!)
Signed-off-by: Stephen <158498287+0-Blu@users.noreply.github.com>
2024-08-12 15:14:57 -04:00
nythepegasus
aab4e62e24 Bump SideStore version to 0.5.7
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2024-08-05 21:44:59 -04:00
June Park
1713fccfc4 merge AltStore 1.6.3, add dynamic anisette lists, merge SideJITServer integration
* Change error from Swift.Error to NSError

* Adds ResultOperation.localizedFailure

* Finish Riley's monster commit

3b38d725d7
May the Gods have mercy on my soul.

* Fix format strings I broke

* Include "Enable JIT" errors in Error Log

* Fix minimuxer status checking

* [skip ci] Update the no wifi message to include VPN

* Opens Error Log when tapping ToastView

* Fixes Error Log context menu covering cell content

* Fixes Error Log context menu appearing while scrolling

* Fixes incorrect Search FAQ URL

* Fix Error Log showing UIAlertController on iOS 14+

* Fix Error Log not showing UIAlertController on iOS <=13

* Fix wrong color in AuthenticationViewController

* Fix typo

* Fixes logging non-AltServerErrors as AltServerError.underlyingError

* Limits quitting other AltStore/SideStore processes to database migrations

* Skips logging cancelled errors

* Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion

We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.

However, we kept the underlying Core Data property name the same to avoid extra migration.

* Conforms OperatingSystemVersion to Comparable

* Parses AppVersion.minOSVersion/maxOSVersion from source JSON

* Supports non-NSManagedObjects for @Managed properties

This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.

* Supports optional @Managed properties

* Conforms AppVersion to AppProtocol

* Verifies min/max OS version before downloading app + asks user to download older app version if necessary

* Improves error message when file does not exist at AppVersion.downloadURL

* Removes unnecessary StoreApp convenience properties

* Removes unnecessary StoreApp convenience properties as well as fix other issues

* Remove Settings bundle, add SwiftUI view instead

Fix refresh all shortcut intent

* Update AuthenticationOperation.swift

Signed-off-by: June Park <rjp2030@outlook.com>

* Fix build issues given by develop

* Add availability check to fix CI build(?)

* If it's gonna be that way...

---------

Signed-off-by: June Park <rjp2030@outlook.com>
Co-authored-by: nythepegasus <nythepegasus84@gmail.com>
Co-authored-by: Riley Testut <riley@rileytestut.com>
Co-authored-by: ny <me@nythepegas.us>
2024-08-05 21:43:52 -04:00
Stern
83ece72ae1 Merge pull request #658 from therealFoxster/develop
Rename "AltStore" to "SideStore"
2024-07-19 18:50:32 -04:00
Foxster
d60bcc49e1 Rename "AltStore" to "SideStore" 2024-07-17 16:21:39 -07:00
bogotesr
bc9c37adda Revert "improve UX on intro popup"
This reverts commit 2583c7f617.
2024-07-12 02:40:26 -07:00
bogotesr
2583c7f617 improve UX on intro popup
fixes that the url for the pairing file info was out of date

adds button for taking user to that page rather than just having a url in text
2024-07-12 02:37:06 -07:00
polymo1
fea5229e02 add(readme): discord badge 2024-07-07 20:07:36 -04:00
stossy11
68be615057 Add SideJITServer Support for Enabling JIT on iOS 17+ in app (#630) 2024-06-16 16:43:25 -07:00
Stern
370cafcba0 Merge pull request #612 from stossy11/MyAnisette
Add Stossy11 Anisette Server
2024-04-29 09:03:05 -04:00
stossy11
f923c1602e Add Stossy11 Anisette Server 2024-04-29 23:01:07 +10:00
nythepegasus
50a85be872 Actually fix embedded pairing file issue 2024-04-23 03:20:40 -04:00
Stern
aae4725a3c Merge pull request #604 from wesbryiecom/develop 2024-04-14 20:55:56 -07:00
Wes Bryie
9d76ee9f19 Add ani.wesbryie.com to Anisette list
Signed-off-by: Wes Bryie <wes@wesbryie.com>
2024-04-14 23:43:04 -04:00
Spidy123222
34a101b796 Remove patreon exclusivity message for sources (#594) 2024-03-14 01:39:40 -07:00
nythepegasus
49b1fd751c Remove outdated/down servers for now
Eventually this could be dynamically pulled/inside SideStore
but extra DNS entries and cleaning can work for now
2024-02-23 20:11:00 -05:00
nythepegasus
4c5bf7bb7d Fix pairing file not resetting when embedded 2024-02-23 19:46:31 -05:00
nythepegasus
2d71631d93 [skip ci] Fixed typo
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2024-02-16 13:55:33 -05:00
Stern
fa0d933956 [skip ci] Adds ThatStella7922's Trusted Source
Signed-off-by: Stern <70122891+SternXD@users.noreply.github.com>
2024-02-16 13:44:37 -05:00
June P
b5d6384a07 bugfix(launch):fix analytics notice 2024-01-30 10:52:22 +09:00
June Park
d39644a4c9 Update stable.yml
Signed-off-by: June Park <rjp2030@outlook.com>
2024-01-30 10:40:34 +09:00
June Park
a2feb34dc1 ouch pushed selfhosted code
Signed-off-by: June Park <rjp2030@outlook.com>
2024-01-30 10:40:07 +09:00
June P
7e5fe64153 feat(launch):add analytics notice 2024-01-30 10:04:33 +09:00
June Park
44175d071c Update nightly.yml
Signed-off-by: June Park <rjp2030@outlook.com>
2024-01-25 23:28:58 +09:00
June P
bae26de444 change to self-hosted 2024-01-25 23:18:28 +09:00
naturecodevoid
b78707808d [skip ci] Remove me from feature report assignees
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-12-12 18:32:21 -08:00
naturecodevoid
d41518581a [skip ci] Remove me from bug report assignees
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-12-12 18:32:05 -08:00
Stern
4abbfe6142 [skip ci] ci: Update stable.yml
This removes workflow_dispatch as it was making the builds show as SideStore `develop`

Signed-off-by: Stern <xsternent@gmail.com>
2023-11-28 05:50:44 -05:00
Stern
dae813d80c Merge pull request #550 from SideStore/Add-description-for-idle
Add description on what disable idle timeout toggle + change add to Siri text for accuracy
2023-11-28 03:12:22 -05:00
Spidy123222
af89b178ad Change button text for adding Siri to refresh apps
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-11-27 23:39:20 -08:00
Spidy123222
8c269207fd add description on what disable idle timeout toggle
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-11-27 23:18:36 -08:00
junepark678
42ecd38517 bugfix(NoIdle): Fix slider not being set to correct value on load 2023-11-28 12:00:20 +09:00
June Park
9f7d4dee49 [skip ci] bump nightly to 0.5.5
Signed-off-by: June Park <rjp2030@outlook.com>
2023-11-28 03:02:04 +09:00
June P
458b8e491e bugfix: fix removal of attributes 2023-11-28 02:34:15 +09:00
Stern
495e621e69 Merge pull request #539 from junepark678/NoIdle
feat(Operations): don't idle timeout during installations
2023-11-27 12:12:10 -05:00
June P
c986512b5f bugfix: fix appending to a list that is nil 2023-11-28 02:04:14 +09:00
June Park
d277754ae5 Merge branch 'develop' into NoIdle
Signed-off-by: June Park <rjp2030@outlook.com>
2023-11-28 00:46:48 +09:00
junepark678
2ef2e2f26b bugfix: make it able to be toggled, fix bug in crash on installation 2023-11-28 00:44:47 +09:00
nythepegasus
23a53034fa [skip ci] Bump nightly to 0.5.4
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-11-27 10:39:41 -05:00
Stern
ce57d72a78 [skip ci] ci: add workflow_dispatch
Signed-off-by: Stern <xsternent@gmail.com>
2023-11-27 10:18:06 -05:00
nythepegasus
502b89d890 [skip ci] Add other project maintainers to CODEOWNERS
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-11-27 09:37:25 -05:00
junepark678
5f0015fad0 chore(Clear Cache): do proper error handling 2023-11-27 09:31:36 -05:00
June Park
c81236957b bugfix(settings): fix rounding issues on clear cache button (#536) 2023-11-27 09:31:36 -05:00
Spidy123222
970ab38b27 move debug row 2023-11-27 09:31:36 -05:00
Spidy123222
8a5c31b81d make button function again 2023-11-27 09:31:36 -05:00
Spidy123222
8508fe79b5 change order of settings debug section 2023-11-27 09:31:36 -05:00
Spidy123222
3859e98801 Add button to storyboard 🙄 2023-11-27 09:31:36 -05:00
Spidy123222
a759c7be9e please fix to show button 2023-11-27 09:31:36 -05:00
Spidy123222
12fc6cf6e2 attempt fix settings 2023-11-27 09:31:36 -05:00
Spidy123222
580db6530e fix the mighty error 2023-11-27 09:31:36 -05:00
Spidy123222
9c67c237ee get rest of batcherror 2023-11-27 09:31:36 -05:00
Spidy123222
357d85a72e please o riley build 2023-11-27 09:31:36 -05:00
Spidy123222
88ad828ce0 hopefully fix error code build error 2023-11-27 09:31:36 -05:00
Riley Testut
a95625a34a Adds “Clear Cache” description to Techy Things section footer
(cherry picked from commit 913db5131b)
2023-11-27 09:31:36 -05:00
Riley Testut
95e00d81f5 Adds “Clear Cache” button to remove temporary files and uninstalled app backups
(cherry picked from commit 3adfc9db6d)
2023-11-27 09:31:36 -05:00
junepark678
c2e386a5c5 chore(App IDs, My Apps): change back to full 2023-11-27 09:22:19 -05:00
junepark678
a76aade4ff chore(App IDs, My Apps): change to use DateComponentsFormatter.UnitsStyle.abbreviated 2023-11-27 09:22:19 -05:00
junepark678
65c9986103 bugfix(App IDs, My Apps): fix date display 2023-11-27 09:22:19 -05:00
junepark678
9e2b9b6639 bugfix(App IDs, My Apps): display only necessary information 2023-11-27 09:22:19 -05:00
junepark678
cf373634d7 bugfix(App IDs, My Apps): calculate in correct direction in time (we aren't time travelers) 2023-11-27 09:22:19 -05:00
junepark678
b3d5d976b4 feat(My Apps): make expiration dates more specific 2023-11-27 09:22:19 -05:00
junepark678
c3c31995ce chore(App IDs): localize Unknown string 2023-11-27 09:22:19 -05:00
junepark678
7e92e17429 feat(App IDs): make expiration dates more specific 2023-11-27 09:22:19 -05:00
junepark678
88ab8fa8d7 feat: remove reliance on Info.plist for getting udid 2023-11-27 09:21:54 -05:00
junepark678
ebe78932bf feat(Operations): don't idle timeout during installations 2023-11-26 10:51:33 +09:00
nythepegasus
2e613e6d15 [skip ci] Update Ignited source URL 2023-11-06 09:02:49 -05:00
Spidy123222
35ee92db12 Change pairing file link with new wiki link
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-10-23 13:12:23 -07:00
nythepegasus
04d9f760ad Fix AltWidget App Group issue 2023-10-23 10:12:09 -04:00
naturecodevoid
4f52743be8 Revert most of xcodeproj changes 2023-10-21 20:44:49 -07:00
naturecodevoid
32cae7a5b2 Revert "Attempt to fix submodule dependencies for GH runner"
This reverts commit 5cff914ff3.
2023-10-21 20:30:32 -07:00
naturecodevoid
c2c0e3b790 Use forked libplist 2023-10-21 20:30:23 -07:00
nythepegasus
6d36a30787 [skip ci] Bump nightly to 0.5.3
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-10-21 21:02:41 -04:00
nythepegasus
48a86ec6de Update Roxas 2023-10-21 20:35:32 -04:00
nythepegasus
5cff914ff3 Attempt to fix submodule dependencies for GH runner 2023-10-21 20:34:44 -04:00
nythepegasus
70ea725ce3 Update limd/libplist submodules 2023-10-20 22:04:47 -04:00
nythepegasus
78f12e45f9 Add iOS 17 JIT error notice with other errors 2023-10-20 21:51:24 -04:00
nythepegasus
e5061acc20 Hardcode SideStore's URL scheme for now 2023-10-20 21:51:24 -04:00
nythepegasus
2d7bc51d30 Revert this change 2023-10-20 21:51:24 -04:00
nythepegasus
9128b67ee8 Add newly compiled AltBackup 2023-10-20 21:51:24 -04:00
Bogdan Seniuc
551c004476 Use provisioning profile details instead of guessing active app limit 2023-10-20 21:50:50 -04:00
Spidy123222
ed6a8d1379 [skip ci] Remove emuthreeds from trusted sources 2023-10-18 21:44:55 -07:00
nythepegasus
766fb89e0b Change this to be hardcoded SideStore search 2023-10-17 17:38:09 -04:00
nythepegasus
c5b8cb4459 Remove buggy retry code finally 2023-10-17 17:37:29 -04:00
naturecodevoid
0deae92829 Bump version to 0.5.2 so nightly builds have a higher SemVer version than stable 2023-09-18 16:18:05 -07:00
naturecodevoid
cc5d2f1813 Build nightly with latest minimuxer changes to attempt to fix plist_from_memory crash 2023-09-18 16:17:14 -07:00
naturecodevoid
41151d0d49 0.5.1 2023-09-17 14:01:13 -07:00
Spidy123222
52702264a3 Change version to 0.5.2 2023-09-17 12:36:44 -07:00
naturecodevoid
6e297e1278 Update Swift Packages and submodules (#469)
* Update Swift Packages

* Update submodules

* make sure it builds

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-09-17 10:45:55 -07:00
naturecodevoid
e3bb9b425f [skip ci] Add more information to staging errors (#468)
* Point to my forks and attempt to add more information to staging errors

* Improve error message a bit

* Revert fetch-prebuilt.sh changes

* Undo some whitespace changes

* missed one

* oops

* [skip ci]

* [skip ci]

* [skip ci] remove staging directory from install app error

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
Co-authored-by: Dadoum <dadoum@protonmail.com>
2023-09-17 10:37:49 -07:00
Spidy123222
79255be79c Update Altsign (August 6) (#467)
This changes the git commit or be our latest altsign version.
2023-09-16 01:51:35 -07:00
nythepegasus
7c836f5ba1 Update emuplace source
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-08-13 11:54:00 -04:00
Spidy123222
938bcd14ad Add ignited source
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-08-06 14:15:51 -07:00
Joelle Stickney
229d79fc05 Removed Quantum Source
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-08-06 16:26:24 -04:00
Joelle Stickney
2d3dac2e1d Added Quantum Source to trusted sources
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-08-06 15:30:21 -04:00
nythepegasus
e23f5e7894 [skip ci] Change Discord custom invite to static website invite
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-08-04 13:17:02 -04:00
Spidy123222
571d27c814 Fix message and put in proper spot.
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-27 23:42:32 -07:00
Spidy123222
dde6bd4fe3 Make Notification explanation smaller for refresh
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-27 23:17:58 -07:00
Nythepegasus
6e6dbd9329 Bump version to 0.5.0 2023-07-27 06:56:51 -04:00
Nythepegasus
258268f5ef Update default anisette server 2023-07-27 06:55:31 -04:00
Nythepegasus
9ae49977fb Fix going to the home screen 2023-07-27 06:23:26 -04:00
Nythepegasus
d61c54fa60 Reintroduce notification/pop-up 2023-07-27 06:23:26 -04:00
Nythepegasus
980699af6f Remove extra returns and make sure to decrement 2023-07-27 06:23:26 -04:00
Nythepegasus
cc5c280882 Finally fix retries for minimuxer calls 2023-07-27 06:23:26 -04:00
nythepegasus
090456bba1 [skip ci] Merge pull request #419 from Nythepegasus/chore/up-fixes
[Chore] Pull upstream changes from AltStore
2023-07-27 06:22:54 -04:00
nythepegasus
5354d4eb76 Merge pull request #414 from SideStore/patch/fix-widget
[Patch] Change Widget entitlements to search hardcoded group first
2023-07-27 06:11:48 -04:00
Riley Testut
b986fae611 Enforces 77x31 minimum size for PillButton 2023-07-27 04:47:44 -04:00
Riley Testut
cfcfc3e928 Fixes incorrect cell height for some News items
We need to take layoutMargins into account when calculating the height of the prototype cell.
2023-07-27 04:47:44 -04:00
Riley Testut
f97548fc3a Fixes UIDocumentPickerViewController deprecation warnings 2023-07-27 04:47:44 -04:00
Riley Testut
36913b425c Fixes “variable mutated after capture by sendable closure” warning 2023-07-27 04:47:44 -04:00
Riley Testut
822ea08d89 Fixes AppViewController deprecation warnings 2023-07-27 04:47:43 -04:00
Riley Testut
98dd6f3fe7 Fixes CollapsingTextView “TextKit 1 compatibility mode” runtime warning 2023-07-27 04:47:38 -04:00
Nythepegasus
b3f0dbb155 Change circle and logo to just be circle for now 2023-07-27 02:59:30 -04:00
nythepegasus
6904d931c3 Change the lock screen icon to be legible
Signed-off-by: nythepegasus <me@nythepegas.us>
2023-07-27 02:25:09 -04:00
Nythepegasus
529466a9f7 Merge branch 'develop' into patch/fix-widget 2023-07-24 09:29:03 -04:00
Nythepegasus
77dc695ba1 Revert the retries here as these seem buggier 2023-07-24 09:22:34 -04:00
Nythepegasus
e17776f651 Remove unused Riley and Shane images from bundle 2023-07-24 07:54:20 -04:00
Nythepegasus
0d2f355a74 Update AltWidget assets 2023-07-24 07:18:42 -04:00
Nythepegasus
2ce1576016 Move the first app group to be known shared group 2023-07-24 05:49:36 -04:00
Nythepegasus
0f3be3c494 Update AltWidget/ReleaseEntitlements.plist
Signed-off-by: Nythepegasus <me@nythepegas.us>
2023-07-24 05:40:51 -04:00
Nythepegasus
8c1ca8503a Fix Actions builds for AltWidget 2023-07-24 05:28:26 -04:00
Wes Bryie
32a59c17f4 [skip ci] em_proxy link update (#412)
Signed-off-by: Wes Bryie <wes@wesbryie.tech>
2023-07-21 20:04:44 -07:00
Riley Testut
b4b4ceab0b Fixes updating apps with manually-removed app extensions (e.g. uYou+) 2023-07-17 12:15:04 -04:00
Nythepegasus
be1f27bb9e Revert "Fix building on Xcode 15"
This reverts commit 3de24dcfce.
2023-07-11 02:00:49 -04:00
Nythepegasus
ed10ddb1cb Go to home screen instead of Safari/notif combo 2023-07-11 01:45:14 -04:00
Nythepegasus
dbdb4b0f32 Add various retries to the minimuxer calls 2023-07-11 01:44:11 -04:00
Nythepegasus
59e537362e Xcode 15 keeps adding imobiledevice.swift 2023-07-10 14:33:53 -04:00
Nythepegasus
6d96bf414f These vars don't change, let's use let keyword 2023-07-10 14:32:41 -04:00
Nythepegasus
e7ba778a5f Update AltSign and OpenSSL packages 2023-07-09 14:00:02 -04:00
Nythepegasus
933d349cd5 Fix libfragmentzip's search path 2023-07-09 14:00:02 -04:00
Nythepegasus
3de24dcfce Fix building on Xcode 15 2023-07-09 13:59:55 -04:00
Spidy123222
3275d16b8b [skip ci] use odyssey team unified source
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-08 23:53:53 -07:00
Spidy123222
5bb4cd1dad [skip ci] Add Taurine
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-08 23:32:41 -07:00
Nythepegasus
16b14441fa Update Macley omnisette server IP
Signed-off-by: Nythepegasus <nythepegasus84@gmail.com>
2023-07-03 10:47:54 -04:00
Joshua Laymon
93a6272d30 Merge pull request #387 from bogotesr/develop
Fix up patreon screen and add socials
2023-06-21 21:58:04 -07:00
Spidy123222
0dc526f778 Replace old servers with macley v3 ansiette 2023-06-14 13:56:12 -07:00
bogotesr
183e185812 Change Patreon screen again
Refresh to "support us" and include social media.
2023-06-13 23:14:01 -07:00
Stern
e02453598c Update pr.yml
Signed-off-by: Stern <xsternent@gmail.com>
2023-06-13 20:31:37 -04:00
Stern
24af1b5b5f Update pr.yml
Signed-off-by: Stern <xsternent@gmail.com>
2023-06-13 20:30:41 -04:00
Spidy123222
5864c283f6 Add EmuPalace Source 2023-06-12 20:35:36 -07:00
Spidy123222
be78fa4b91 Upload config (#383)
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-06-12 09:39:20 -07:00
Spidy123222
b3abf69a02 Change label to 0.4.0
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-05-29 19:33:38 -07:00
Spidy123222
c530dc11ae Change default anisette to v3 anisette server (#367) 2023-05-29 19:15:15 -07:00
Joelle Stickney
d368ddbd11 Merge pull request #365 from lonkelle/develop
Co-authored-by: Joe Mattiello <mail@joemattiello.com>
2023-05-24 23:11:22 -04:00
Joelle Stickney
e5c6521a15 Co-authoring all the things
Co-authored-by: Joe Mattiello <mail@joemattiello.com>
2023-05-24 23:08:33 -04:00
Joelle Stickney
898a59768e Update README.md
Co-authored-by: Joe Mattiello <mail@joemattiello.com>
2023-05-24 23:04:45 -04:00
Joelle Stickney
a85bc93142 Update README.md
Co-authored-by: Joe Mattiello <mail@joemattiello.com>

Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-05-24 03:56:58 -04:00
Joelle Stickney
c6c1f9faa0 Update README.md
Co-authored-by: JoeMatt <mail@joemattiello.com>

Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-05-24 03:55:28 -04:00
Joelle Stickney
0eea19c9cc Update README.md
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-05-24 03:53:51 -04:00
naturecodevoid
ed2270ff46 Anisette V3 (#324)
* initial anisette V3 implementation

* update V3 urls and log version

* fix crash where FetchAnisetteDataOperation.clientInfo would be nil when getting anisette V3 without provisioning first

* move adi.pb reset to its own button instead of doing it on sign out

* fallback to V1 if client_info fails

* make sure to unwrap optional strings

* feat(anisette): update v3 usage, improve error messages and names, report v3 errors to the user

* refactor(anisette): reduce duplicate JSON to anisette code

* fixes(anisette v3): improve errors, fix v3 server check, fix some edge cases where SideStore could crash and instead return an error, retry on -45061
2023-05-18 01:30:18 -07:00
SoY0ung
45b6c3b338 Fix 'The name for this app is invalid' error(#361)
Fix 'The name for this app is invalid' error when sideloading with non-ascii name ipa
2023-05-15 12:38:26 +08:00
SoY0ung
84e2284f56 Optimizing function calls
Thanks for @ktprograms advice
2023-05-14 19:06:22 +08:00
SoY0ung
1c0d0be622 Fix 'The name for this app is invalid' error
This error is related to App ID creation failure.
App ID name must be an ascii text. It is not allowed to create an App ID with non-ascii text like Chinese, Japanese.
If the name is NOT an ascii text, using bundleID instead.
2023-05-14 02:55:36 +08:00
naturecodevoid
a9ce0f487d fix: open Safari instead of force closing and provide a fallback for users with notifications disabled 2023-05-06 19:25:37 -07:00
naturecodevoid
07533e0365 fix: ensure minimuxer is started when refreshing in the background 2023-04-16 10:07:04 -07:00
naturecodevoid
ee5ddd4264 fix: use a notification instead of an alert for force close 2023-04-16 09:29:12 -07:00
naturecodevoid
f519d22d81 fix: removing _CodeSignature folder before resigning 2023-04-13 21:21:51 -07:00
naturecodevoid
51ed87086a [skip ci] ci: fully rename SideStore.ipa, even after extracting the artifact zip 2023-04-13 07:30:20 -07:00
naturecodevoid
1ca3aa3cdb fix: force close SideStore after 3 seconds if still reinstalling 2023-04-13 07:20:36 -07:00
naturecodevoid
0178c63f6a fix: hopefully reduce ApplicationVerificationFailed errors by removing _CodeSignature folders since those may cause a problem 2023-04-12 19:53:27 -07:00
naturecodevoid
8a97c409fa fix: add .AltWidget to app group ID when modifying for SideStore 2023-04-12 07:46:14 -07:00
naturecodevoid
3dd0735305 fix: always reinstall when refreshing ourselves 2023-04-11 21:50:15 -07:00
naturecodevoid
536f775baa Revert "Don't reinstall on first SideStore refresh"
This reverts commit 40e1225b87.
2023-04-11 21:12:01 -07:00
naturecodevoid
00f7a684a3 [skip ci] chore: rename tempEnt.plist to ReleaseEntitlements.plist to reduce future confusion 2023-04-11 21:09:32 -07:00
naturecodevoid
d79b166a6a chore: Remove old apps.json/app.json files 2023-04-11 21:05:53 -07:00
naturecodevoid
b3d827f56a refactor: remove minimuxerToOperationError in favor of extending MinimuxerError to be a LocalizedError and remove unused cases from OperationError 2023-04-11 21:04:07 -07:00
naturecodevoid
40bcef1dcb Use XYZ0123456 team ID for tempEnt.plist
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-04-06 20:45:18 -07:00
naturecodevoid
6146f1bdaa Update tempEnt.plist 2023-04-06 12:42:37 -07:00
f1shy-dev
f5d82d9ef0 Update tempEnt.plist to change AltStore to SideStore
Signed-off-by: f1shy-dev <56125930+f1shy-dev@users.noreply.github.com>
2023-04-06 20:23:27 +01:00
naturecodevoid
b2a29ae606 [skip ci] Include commit SHA in nightly version 2023-04-02 15:08:48 -07:00
naturecodevoid
98ccba53a2 [skip ci] Add version to artifact name
we can't do this for releases because some download URLs might rely on it being named SideStore.ipa

Build Info will also have version anyways
2023-04-02 08:01:57 -07:00
naturecodevoid
9bfda36647 [skip ci] Log version 2023-04-02 08:00:11 -07:00
naturecodevoid
5710cdf19c [skip ci] Fix PR commit suffix 2023-04-02 07:58:38 -07:00
naturecodevoid
20cf54bfcd [skip ci] Rename and move the first application groups log 2023-04-02 07:34:48 -07:00
naturecodevoid
2ce639e750 Remove app groups that contain AltStore 2023-04-01 20:03:15 -07:00
naturecodevoid
b1ed413c4f Revert Joelle's fix 2023-04-01 16:15:04 -07:00
naturecodevoid
b8c3060037 Log provisioning profile application groups 2023-04-01 16:10:40 -07:00
naturecodevoid
c3ea4940d7 Reduce duplicate consts 2023-04-01 16:10:05 -07:00
naturecodevoid
40e1225b87 Don't reinstall on first SideStore refresh 2023-04-01 16:09:28 -07:00
naturecodevoid
0c171122b2 refactor minimuxer to use swift-bridge (#321)
also add team ID to the end of the bundle ID for Debug builds to mirror SideServer
2023-04-01 16:02:12 -07:00
Joelle Stickney
6d0f4bb3da Fixes widget refreshing and is more thorough matching store ID 2023-03-28 23:48:24 -04:00
Joelle Stickney
5e2cc6e20c Update store check to check for AltServer or SideServer installation 2023-03-28 01:33:55 -04:00
naturecodevoid
99cb43bbea [skip ci] include commit SHA in PR builds
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-03-24 08:56:30 -07:00
Riley Testut
ca7d8277f7 Fixes “no provisioning profile with the requested identifier…” error
As of March 20, 2023, deleting an app’s auto-generated free provisioning profile is no longer supported. However, fetching the provisioning profile now re-generates is every time, so there’s no need to delete it first.

As a workaround, we now simply use the first profile we fetched if we receive an error when deleting it. This approach should continue to work even if Apple later reverses this change.
2023-03-21 18:52:56 -04:00
Joe Mattiello
337d26333e Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:02:11 -04:00
Joe Mattiello
ebb64d255b Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:00:56 -04:00
Joe Mattiello
7dcb199f68 Update README.md
Add repobeats svg

Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-19 23:28:32 -04:00
naturecodevoid
4334e887de [skip ci] use bundle ID from Build.xcconfig in AltStore.xcconfig 2023-03-12 16:38:59 -07:00
naturecodevoid
4e84dc4cc8 [skip ci] rename the *.dSYM artifact so that macOS treats it as a normal folder 2023-03-07 08:24:28 -08:00
naturecodevoid
1a1ed072bf attach debugging symbols to actions builds 2023-03-07 07:50:31 -08:00
naturecodevoid
ae457f07c4 add https for ani.sidestore.io to Settings.bundle 2023-03-07 07:27:36 -08:00
Nythepegasus
00095942c3 Merge pull request #288 from SideStore/Remove-jk-anisette 2023-03-07 00:54:51 -05:00
Spidy123222
d1caa5fc21 Merge branch 'develop' into Remove-jk-anisette 2023-03-06 12:11:02 -08:00
Spidy123222
813e2f97ac Change version to 0.3.2
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-06 12:10:39 -08:00
Nythepegasus
bcb5a90f5e Add SSL encryption to ani.sidestore.io
Signed-off-by: Nythepegasus <nythepegasus84@gmail.com>
2023-03-06 14:09:16 -05:00
Spidy123222
020a1a3149 Replace sideloady to use sidestore ani
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-05 19:27:25 -08:00
Spidy123222
c4d649ec58 Remove jkcoxson anisette servers.
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-05 13:45:10 -08:00
naturecodevoid
c02cf2c284 Update error codes to match minimuxer error codes (this is why people got Unknown error instead of an actual error message)
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-03-04 08:08:35 -08:00
Spidy123222
c30afd042e Change version to 0.3.1
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-03-01 13:17:09 -08:00
naturecodevoid
17640fe6cf Cherry pick app ID logging fix from duplicate profiles PR 2023-02-25 14:36:37 -08:00
Joe Mattiello
2e4f6ee420 Merge pull request #272 from SideStore/naturecodevoid/prebuild-rust-deps-attempt-2
More actions updates, contributing guide
2023-02-24 00:00:43 -05:00
naturecodevoid
a3768d9221 [skip ci] actions: Add info/automate cache resetting 2023-02-21 17:27:56 -08:00
naturecodevoid
80c3390363 [skip ci] Makefile: Remove build_rust_dependencies 2023-02-21 17:07:58 -08:00
naturecodevoid
a5e3869d8f Project: update CONTRIBUTING.md to use Makefile 2023-02-21 17:01:24 -08:00
naturecodevoid
aa7d7c2d02 Revert "modify actions to work on test branch"
This reverts commit e59fb15926.
2023-02-21 12:51:34 -08:00
naturecodevoid
015f205569 update release descriptions 2023-02-21 12:42:56 -08:00
naturecodevoid
e59fb15926 modify actions to work on test branch 2023-02-21 12:24:25 -08:00
naturecodevoid
173c585f2d cleanup actions, revamp beta action, modify nightly build num system to be day specific 2023-02-21 12:23:12 -08:00
naturecodevoid
6f8c27793e cleanup makefile and add build steps from github actions 2023-02-21 12:19:08 -08:00
naturecodevoid
332b81c803 No more rust 2023-02-20 20:36:39 -08:00
naturecodevoid
4b343b500d fetch-prebuilt.sh whitespace improvement 2023-02-20 19:43:46 -08:00
naturecodevoid
e87c537642 Update CONTRIBUTING.md 2023-02-20 18:58:42 -08:00
naturecodevoid
2e6300cce2 add changes from attempt #1 2023-02-20 18:50:40 -08:00
naturecodevoid
09514d15a6 use prebuilt binaries 2023-02-20 18:48:21 -08:00
naturecodevoid
0de23dcba0 remove submodules 2023-02-20 16:33:11 -08:00
Joss Laymon
bacb153151 Use new pojav url
Signed-off-by: Joss Laymon <71040782+bogotesr@users.noreply.github.com>
2023-02-20 11:21:30 -07:00
naturecodevoid
a01aa299d8 SourcesViewController: Fix 1 trusted source causing an error making all trusted sources fail to load 2023-02-18 20:33:44 -08:00
Spidy123222
44edbddbd8 Replace placeholder video with instructions. (#266) 2023-02-15 21:14:51 -08:00
naturecodevoid
79d677cf3c Revamp issue and PR templates (#253)
* Create config.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Delete bug_report.md

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create bug_report.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create feature_request.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create pull_request_template.md

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-02-06 00:43:44 -05:00
oqammx86
be39b6512f Add sidestore anisette server
Signed-off-by: oqammx86 <84847714+oq-x@users.noreply.github.com>
2023-02-02 13:05:36 -05:00
naturecodevoid
fcfeea35da Revert "Release channel support (#239)"
This reverts commit 7d0eb8c61e.
2023-02-02 08:09:15 -08:00
naturecodevoid
7d0eb8c61e Release channel support (#239)
* Release channel support

- Show SideStore in Browse if it's not from the current SideStore source
- Change SideStore source URL and source ID based on if beta and nightly are in the version string
- Use StoreApp name for InstalledApp name to allow for source-specified name to show up in My Apps

* My Apps: Fix incorrect app name on first launch

* News: fix duplicate news items from multiple SideStore release channel sources

* Trusted Sources: Add stable and beta
2023-02-02 08:05:27 -08:00
naturecodevoid
4d8438a6b6 Update minimuxer 2023-02-01 20:05:26 -08:00
naturecodevoid
f611244e35 Add PR suffix to version in PR workflow
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-26 17:13:32 -08:00
naturecodevoid
546a978d3b Update .gitignore 2023-01-25 06:50:06 -08:00
naturecodevoid
70b23fb073 Merge remote-tracking branch 'upstream/develop' into develop 2023-01-19 17:39:11 -08:00
naturecodevoid
a56ca597d6 Fix build errors 2023-01-19 17:37:43 -08:00
naturecodevoid
679e0228a8 Remove debug
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-19 08:30:47 -08:00
naturecodevoid
e153394323 Merge branch 'develop' of https://github.com/naturecodevoid/SideStore into develop 2023-01-19 07:56:52 -08:00
naturecodevoid
5bd1fcfcfd Merge branch 'develop' of https://github.com/SideStore/SideStore into develop 2023-01-19 07:54:25 -08:00
naturecodevoid
2a392ddc44 SemVer version comparison 2023-01-19 07:52:47 -08:00
Joelle Stickney
b5cb8bc0d9 Updated Patreon Link 2023-01-18 14:41:24 -05:00
Joelle Stickney
fa170bcf98 Update README.md
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-01-13 10:39:12 -05:00
Joe Mattiello
7939d46949 Merge pull request #219 from SoY0ung/file_sharing
Reset Pairing File and checking minimuxer log in app
2023-01-12 20:35:06 -05:00
naturecodevoid
ab9df8201a Merge remote-tracking branch 'upstream/develop' into develop 2023-01-09 17:40:50 -08:00
SoY0ung
4a670ec091 You can check minimuxer log in Error Log Page 2023-01-09 16:17:00 +08:00
SoY0ung
10e57e59c4 You can access SideStore document from File App 2023-01-09 15:19:18 +08:00
SoY0ung
b9ec43ef34 Add 'Reset Pairing File' 2023-01-09 15:15:31 +08:00
SoY0ung
42197cd375 Fix Advanced Setting display error 2023-01-09 14:13:31 +08:00
jawshoeadan
704852973b Merge pull request #214 from jawshoeadan/remove-hardcoded-me
Update AltBackup.ipa to remove any hardcoded stuff
2023-01-07 18:02:56 -08:00
Jawshoeadan
056b4200df Update AltBackup.ipa 2023-01-04 21:55:00 -08:00
Joe Mattiello
250a7d8627 Merge pull request #213 from SideStore/feature/JoeMFixes
Joe M fixes for 1.0
2023-01-04 12:58:56 -05:00
Joseph Mattello
1ba51e161e Add placeholder for minimux retries
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 12:20:08 -05:00
Joseph Mattello
32e58af896 add workflow to attach builds to PR
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:54:29 -05:00
Joseph Mattello
312fa6fe76 final classes marked as final
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:52:12 -05:00
Joseph Mattello
afbe0837ba allow simulator to launch w/o pairing file
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:32:04 -05:00
Joseph Mattello
36ad2a720f log functions inlineable
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:31:51 -05:00
Joseph Mattello
901e3b14bb add final class to some classes
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:31:41 -05:00
Joseph Mattello
588d209f7b refs #160 codable feed structs
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-04 09:31:28 -05:00
naturecodevoid
554c54e6be Revamp actions to have stable, beta and nightly builds (#210)
* start on nightly builds

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update build.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Remove testing logic, final changes

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Nightly release build (#2)

* Update and rename build.yml to nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* trigger on tag

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add version and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Revert "test"

This reverts commit 9dff8d1d878a764a432ef4560300acdb4407313a.

* Remove pr from stable

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add pr.yml

* Add nightly suffix and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add beta

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* [beta] test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Remove test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:55:14 -05:00
naturecodevoid
b0fac34ffc Nightly release build (#2)
* Update and rename build.yml to nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Create stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* trigger on tag

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add version and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Revert "test"

This reverts commit 9dff8d1d878a764a432ef4560300acdb4407313a.

* Remove pr from stable

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add pr.yml

* Add nightly suffix and build number

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update stable.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* add beta

* Update nightly.yml

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* [beta] test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

* Remove test

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
5ede9f7c6b Remove testing logic, final changes
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
c7254fd23e Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
55fcea04af Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
c212c0a6b2 Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
a31fd6709a Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
e367fd2b73 Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
1ca67d0241 Update build.yml
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
naturecodevoid
8ffa952ff9 start on nightly builds
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-01-04 08:54:21 -05:00
Joelle Stickney
da246fa30b Revert "Change default anisette server"
This reverts commit 25103c1188.
2023-01-04 08:52:59 -05:00
Joseph Mattello
13f306742e Revert "Merge pull request #189 from Nythepegasus/feature/retries"
This reverts commit 50841f5e24, reversing
changes made to d797ddd668.
2023-01-04 08:48:33 -05:00
Joelle Stickney
f3815dc45e Merge pull request #209 from SideStore/feature/deeplink_settings
Open Settings.app from SettingsVC
2023-01-01 12:33:31 -05:00
Joseph Mattello
d086254012 add button to open Settings.app in SettingsVC
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2023-01-01 12:20:08 -05:00
Spidy123222
bc4d5ba097 Fix sidestore version
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-30 19:07:17 -08:00
Spidy123222
c556783fe3 Bump version to 0.3.0 (#204)
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-30 15:48:15 -08:00
Joe Mattiello
5fba4c12aa Merge pull request #202 from SideStore/pullrequests/jawshoeadan/develop
Merge all of Riley's new error handling stuff REBASED
2022-12-30 17:10:45 -05:00
Joseph Mattello
7e0dde3ece fix 2 missing .c’s in libmobiledevice build
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 17:09:44 -05:00
Joseph Mattello
fc03e83531 fix wrong libframentzip.a link target
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 17:09:44 -05:00
Nythepegasus
4c441077c7 Add a bunch more "logging" throughout signing 2022-12-30 17:09:44 -05:00
Nythepegasus
4a5ca81e9a Show file extensions to help user choose file 2022-12-30 17:09:44 -05:00
Nythepegasus
75eebe8f8c Use right Bundle ID for AltWidget 2022-12-30 17:09:44 -05:00
Nythepegasus
271a8cdac5 Change the Bundle ID to always be SideStore 2022-12-30 17:09:44 -05:00
Nythepegasus
25103c1188 Change default anisette server 2022-12-30 17:09:44 -05:00
Nythepegasus
d81058e606 Add our own Analytics key 2022-12-30 17:09:44 -05:00
Nythepegasus
693df54b3b Change version to be 0.1.2 2022-12-30 17:09:43 -05:00
Riley Testut
ae6ed99dc4 Displays “TODAY” as section header for logged errors that occured that day 2022-12-30 17:09:15 -05:00
Riley Testut
14bd58e741 Prevents simultaneous database access from multiple AltStores
AltStore now sends a “WillAccessDatabase” notification before loading the persistent store, which causes other AltStore instances in memory to exit (either immediately, or upon returning to foreground).

This prevents multiple AltStore instances from simultaneously accessing the same database, which could result in corrupted data (especially if they used different database model versions).
2022-12-30 17:09:15 -05:00
Riley Testut
6d35a7a4ba Fixes widgets potentially not updating after refreshing apps 2022-12-30 17:09:15 -05:00
Riley Testut
46b0d1ceac Always displays PatreonViewController loading indicator when fetching patrons
Previously, we only showed the loading indicator if user had not yet cached any Friend Zone patrons.
2022-12-30 17:09:15 -05:00
Riley Testut
67a66d2fcd Fixes ErrorLogViewController’s dark mode appearance 2022-12-30 17:09:15 -05:00
Riley Testut
43e90b57ea [Apps] Updates AltStore beta to 1.6b2 2022-12-30 17:09:14 -05:00
Riley Testut
c80740e590 Updates LaunchViewController error alert to include more detail
Uses debugDescription over localizedDescription, because that makes it significantly easier to debug the underlying problem from a screenshot.
2022-12-30 17:09:14 -05:00
Riley Testut
54ccb9611e Fixes “error migrating persistent store” issue
We now set AppVersion.sourceID during migration, which fixes AppVersion entries conflicting across different Sources if multiple contain the same app + version.
2022-12-30 17:09:14 -05:00
Riley Testut
8fcb897800 [Apps] Updates AltStore beta to 1.6b1 2022-12-30 17:09:14 -05:00
Riley Testut
699eda5d1b [AltWidget] Adds “icon” style lock screen widget 2022-12-30 17:09:14 -05:00
Riley Testut
d7d0a83550 [AltWidget] Replaces ProgressRing with SwiftUI.Gauge 2022-12-30 17:09:14 -05:00
Riley Testut
e3c331c911 [AltServer] Fixes potential race condition crash when managing connections 2022-12-30 17:09:14 -05:00
Riley Testut
eda4dd6aec Updates app version to 1.6b1 2022-12-30 17:09:14 -05:00
Riley Testut
8ad7be474d Resolves AppVersion context-level conflict after migrating from Core Data model v10 2022-12-30 17:09:14 -05:00
Riley Testut
a64435f155 Migrates Core Data model from v10 to v11 2022-12-30 17:09:14 -05:00
Riley Testut
fa160124d2 Supports new “versions” key in source JSON
Allows sources to list multiple versions of an app.

Preserves backwards compatibility by assigning legacy version values when assigning AppVersions.
2022-12-30 17:09:14 -05:00
Riley Testut
5765cb8330 Adds AppVersion Core Data entity
Preserves redundant fields on StoreApp in database model for backwards compatibility.
2022-12-30 17:09:14 -05:00
Riley Testut
f472b227bb Automatically purges LoggedErrors older than one month
Occurs whenever app enters background.
2022-12-30 17:09:14 -05:00
Riley Testut
d2b419c42e Adds Error Log screen
Allows users to view a history of all errors that occured when performing app operations.
2022-12-30 17:08:49 -05:00
Riley Testut
09d4de660f Fixes CollapsingTextView incorrectly showing More button 2022-12-30 17:08:49 -05:00
Riley Testut
728dcd8523 Adds LoggedError Core Data entity
Allows us to save certain errors to disk so that they can be viewed again later from an error log.
2022-12-30 17:08:49 -05:00
Riley Testut
93cf9bf6a9 Makes AppProtocol.url optional
Allows us to create AnyApp values without a valid file URL.
2022-12-30 17:08:49 -05:00
Joe Mattiello
50841f5e24 Merge pull request #189 from Nythepegasus/feature/retries
Add various retries throughout the Rust function calls
2022-12-30 17:04:43 -05:00
Nythepegasus
fc6d92d1fc Add retries in the various minimuxer functions 2022-12-30 17:04:19 -05:00
Nythepegasus
7162a029bb Retry AFC 10 times before giving up 2022-12-30 17:04:19 -05:00
Joseph Mattello
d797ddd668 closes #93 redo of bundle id’s from .app
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 17:03:58 -05:00
Spidy123222
989e8c3aa6 Add SideKit integration (#149)
* Add a URL scheme

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>

* Add sidestore as a scheme and fix spacing

* Undo formatting fix

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>

Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-30 17:03:18 -05:00
Joe Mattiello
08b79af242 Merge pull request #192 from LitRitt/new-ui-color
Change UI Color
2022-12-30 16:56:54 -05:00
LitRitt
0d2f346a30 Changes default app tint color
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
39f1d5f5fd Probably does nothing but changed just in case
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
05008bb7f8 Changes the color of navigations glyphs and colored text
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
be90d6fc45 Changes settings highlight color
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
LitRitt
a1bcdf9924 Change settings background color
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-30 15:59:33 -05:00
Joe Mattiello
b0e001393c Merge pull request #142 from SideStore/feature/cargo_in_xcode
Build Rust depends in XCode
2022-12-30 15:59:18 -05:00
Joseph Mattello
2d08941f6a udpate em_proxy.h
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:47:27 -05:00
Joseph Mattello
d0fef1f312 fix header paths
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:41:40 -05:00
Joseph Mattello
68342cb0d4 fix hardcoded paths in generated cargo xcodeprojs
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:28:03 -05:00
Joseph Mattello
2b419212a7 replace fragmentzip.a with xcodeproj
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:23:06 -05:00
Joseph Mattello
b2cbc7e34d github action please work
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:17:55 -05:00
Joseph Mattello
61247e575b fix build for updated submodules
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 15:17:47 -05:00
Joseph Mattello
31e18266d1 build.yml updates formatting and xcode ver
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:59:16 -05:00
Joseph Mattello
df8a8de889 update libplist to head
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:57:54 -05:00
Joseph Mattello
8a037d6b29 update libmobiledevice to head
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:57:05 -05:00
Joseph Mattello
47b555b98c project: fix wrong paths for depends
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:56:52 -05:00
Joseph Mattello
0c2dae475e cargo: fix github action?
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 14:49:13 -05:00
Joseph Mattello
dc676d04d8 Add rust depends to xcode build
Signed-off-by: Joseph Mattello <mail@joemattiello.com>

cargo script for action

fixes path

Signed-off-by: Joseph Mattello <mail@joemattiello.com>

cargo.sh More robust env for xcode cli

Signed-off-by: Joseph Mattello <mail@joemattiello.com>

rust: add xcode projs made with cargo-xcode

Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-30 13:21:57 -05:00
Joelle Stickney
15b54bff50 Merge pull request #201 from SideStore/Add-Community-Store-Trust
Add Community store to Trusted Sources
2022-12-29 01:09:51 -05:00
Spidy123222
47bd4b4c0b Update trustedapps.json
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-28 23:50:24 -05:00
Joelle Stickney
3c8b36ddfe Merge pull request #200 from SideStore/Add-Yattee-Source
Add Yattee to trusted sources
2022-12-25 05:42:05 -05:00
Spidy123222
608df3fddd Add Yattee to trusted sources
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-12-25 02:22:06 -08:00
Joelle Stickney
c092c285ee Removed Jackson from CODEOWNERS
Due to his two year mission trip and the fact he, as an codeowner, gets added to SideStore issues / PRs automatically, it seemed best to remove him atm.
2022-12-24 18:19:17 -05:00
Joelle Stickney
93b745e379 More credit adjustments 2022-12-24 17:19:55 -05:00
Joelle Stickney
c18db77ade Turn off all background refresh notifications
This removes all background refresh notifications but keeps the "Reminder to open SideStore every so often", and the "x app expires soon" local push notifications.
2022-12-19 23:26:15 -05:00
Joelle Stickney
2c0b167e6b Adjusted credits 2022-12-19 07:36:44 -05:00
Joelle Stickney
313254d0c8 Credit text changes 2022-12-19 06:40:10 -05:00
f1shy-dev
6f519c97d3 Fix .mobiledevicepairing files (#186)
* Fix .mobiledevicepairing file selection and also add support for .xml pairing files

* Add a more meaningful message for when pairing file selection is cancelled.
2022-12-17 14:13:53 -07:00
LitRitt
17a3e16b1d Change browse icon (#188)
Now matches new icon

Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>

Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-17 14:13:38 -07:00
Joseph Mattello
8199358088 Info.plist add LSSupportsOpeningDocumentsInPlace 1
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 04:29:10 -05:00
Joseph Mattello
412928eeaa AltWidget version set to $(MARKETING_VERSION)
fixes xcode warning that version of app and extension don’t match

Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 04:28:53 -05:00
Joseph Mattello
51e1b935bd fix 3 more style .white deprecations to .medium
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:13:30 -05:00
Joseph Mattello
742b51e5e2 fix deprecated style .white to .medium
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:12:13 -05:00
Joseph Mattello
fdb5e2eebb fix xcode warning
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:11:22 -05:00
Joseph Mattello
0192f64cd2 Add AltStore Release scheme
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:10:45 -05:00
Joseph Mattello
193298ac87 fix crash for missing patreon images
Signed-off-by: Joseph Mattello <mail@joemattiello.com>
2022-12-17 03:10:28 -05:00
Joe Mattiello
a81cb81799 Merge pull request #181 from LitRitt/change-designer
Update app icon and designer
2022-12-11 21:56:58 -05:00
LitRitt
ad8a7fdc9b Update Settings.storyboard
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-11 15:54:18 -08:00
LitRitt
5440afcebe Update SettingsViewController.swift
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-11 15:54:18 -08:00
LitRitt
715d7e664c Update App Icon
Signed-off-by: LitRitt <78584620+LitRitt@users.noreply.github.com>
2022-12-11 15:54:18 -08:00
jawshoeadan
aa182cfa68 Merge pull request #179 from jawshoeadan/beta-apps
Fix beta apps on the my apps view, and fix deactivating/activating apps
2022-12-11 16:43:46 -06:00
Jawshoeadan
f92dd7a872 Remove all restrictions based on Patreon account (hopefully) 2022-12-11 15:41:43 -06:00
Jawshoeadan
b02b9197d0 Fix beta apps on the my apps view, and fix deactivating/activating apps 2022-12-08 19:24:28 -06:00
Jawshoeadan
86d02be70c Remove Patreon check to show beta apps in store view 2022-12-08 10:28:34 -06:00
Spidy123222
cb990978ee add pokemmo into trusted sources. (#176) 2022-12-07 16:36:17 -08:00
Joshua Laymon
a103202c92 Merge pull request #171 from bogotesr/settings
Better anisette settings
2022-12-05 11:39:08 -07:00
bogotesr
9d7b133037 Add crystal's server 2022-12-04 15:43:08 -07:00
bogotesr
f727f2a1a9 describe what the toggle does 2022-12-03 21:19:31 -07:00
bogotesr
03034768d9 Better anisette settings
adds multivalue selector for some anisette servers
2022-12-03 13:48:33 -07:00
Joe Mattiello
aed3e20e08 Merge pull request #164 from SideStore/update-em-submodules 2022-12-01 20:02:04 -05:00
JJTech0130
74bac6d986 update em_proxy and minimuxer submodules 2022-11-30 18:17:01 -05:00
Fabian Thies
7ebecc353a Add missing App Permission Types (#159)
* Add missing app permission types

* Remove old unused icons for photos, background fetch and background audio permission types

* Add missing icons for contacts and reminders permission types

* Add missing camera permission icon and name

* Switch permission icons to filled versions for a more cohesive look
2022-11-29 06:30:51 -07:00
Jackson Coxson
f0302b0d1e Add an issue template (#157)
Update issue templates
2022-11-26 19:34:52 -07:00
Spidy123222
0b004ad089 Update app.json
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-11-22 21:54:37 -08:00
222 changed files with 9405 additions and 2801 deletions

2
.github/CODEOWNERS vendored
View File

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

39
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

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

10
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

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

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
### Changes
<!-- Fill this list with what your PR changes. Example: -->
- Fix bug
- Change UI for QOL
<!-- If your PR is ready to be merged, you can remove this section. -->
### Todo before merge
<!-- Example: -->
- [x] Finish UI changes
- [ ] Test

View File

@@ -0,0 +1,77 @@
name: Add artifact links to pull request and related issues
on:
workflow_run:
workflows: [Pull Request SideStore build]
types: [completed]
jobs:
artifacts-url-comments:
name: add artifact links to pull request and related issues job
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: add artifact links to pull request and related issues step
uses: tonyhallett/artifacts-url-comments@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
prefix: Builds for this Pull Request are available at
suffix: Have a nice day.
format: name
addTo: pull
# addTo: pullandissues
nightly-link-comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
# This snippet is public-domain, taken from
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
script: |
async function upsertComment(owner, repo, issue_number, purpose, body) {
const {data: comments} = await github.rest.issues.listComments(
{owner, repo, issue_number});
const marker = `<!-- bot: ${purpose} -->`;
body = marker + "\n" + body;
const existing = comments.filter((c) => c.body.includes(marker));
if (existing.length > 0) {
const last = existing[existing.length - 1];
core.info(`Updating comment ${last.id}`);
await github.rest.issues.updateComment({
owner, repo,
body,
comment_id: last.id,
});
} else {
core.info(`Creating a comment in issue / PR #${issue_number}`);
await github.rest.issues.createComment({issue_number, body, owner, repo});
}
}
const {owner, repo} = context.repo;
const run_id = ${{github.event.workflow_run.id}};
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
if (!pull_requests.length) {
return core.error("This workflow doesn't match any pull requests!");
}
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
if (!artifacts.length) {
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request (nightly.link):\n`;
for (const art of artifacts) {
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
}
core.info("Review thread message body:", body);
for (const pr of pull_requests) {
await upsertComment(owner, repo, pr.number,
"nightly-link", body);
}

103
.github/workflows/beta.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Beta SideStore build
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
jobs:
build:
name: Build and upload SideStore Beta
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- 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.4.1
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
- name: Build SideStore
run: make build | xcpretty && 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: Upload to new beta release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: true
prerelease: 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. -->
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
## Changelog
- TODO
## 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@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

View File

@@ -1,100 +0,0 @@
name: Build and Upload SideStore
on:
push:
branches:
- master
- develop
pull_request:
jobs:
build:
name: Build and upload SideStore
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.0.0'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Cache rust cargo
id: cache-rust-cargo
uses: actions/cache@v3
env:
cache-name: cache-rust-cargo
with:
path: ~/.cargo
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache rust minimuxer
id: cache-rust-minimuxer
uses: actions/cache@v3
env:
cache-name: cache-rust-minimuxer
with:
path: ./Dependencies/minimuxer/target
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache rust em_proxy
id: cache-rust-em_proxy
uses: actions/cache@v3
env:
cache-name: cache-rust-em_proxy
with:
path: ./Dependencies/em_proxy/target
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install dependencies
run: brew install ldid
- name: Install rustup
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
target: aarch64-apple-ios
- name: Create emotional damage
run: cd Dependencies/em_proxy && cargo build --release --target aarch64-apple-ios
- name: Build minimuxer
run: cd Dependencies/minimuxer && cargo build --release --target aarch64-apple-ios
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1
with:
xcode-version: ${{ matrix.version }}
- name: Build SideStore
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
rm ./AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild -project AltStore.xcodeproj -scheme AltStore -sdk iphoneos archive -archivePath ./archive CODE_SIGNING_REQUIRED=NO AD_HOC_CODE_SIGNING_ALLOWED=YES CODE_SIGNING_ALLOWED=NO DEVELOPMENT_TEAM=XYZ0123456 ORG_IDENTIFIER=com.SideStore | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: |
rm -rf archive.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
ldid -SAltStore/Resources/tempEnt.plist archive.xcarchive/Products/Applications/SideStore.app/SideStore
- name: Convert to IPA
run: |
mkdir Payload
mkdir Payload/SideStore.app
cp -R archive.xcarchive/Products/Applications/SideStore.app/ Payload/SideStore.app/
zip -r SideStore.ipa Payload
- name: Upload Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa

View File

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

109
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: Nightly SideStore build
on:
push:
branches:
- develop
jobs:
build:
name: Build and upload SideStore Nightly
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Cache .nightly-build-num
uses: actions/cache@v3
with:
path: .nightly-build-num
key: nightly-build-num
- 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.4.1
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-
- name: Build SideStore
run: make build | xcpretty && 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: 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
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 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 Beta](https://github.com/${{ github.repository }}/releases?q=beta).
## 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@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

70
.github/workflows/pr.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Pull Request SideStore build
on:
pull_request:
jobs:
build:
name: Build and upload SideStore
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Add PR suffix to version
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
env:
COMMIT: ${{ github.event.pull_request.head.sha }}
- 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.4.1
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-
- name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

99
.github/workflows/stable.yml vendored Normal file
View File

@@ -0,0 +1,99 @@
name: Stable SideStore build
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
jobs:
build:
name: Build and upload SideStore
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- 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.4.1
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-
- name: Build SideStore
run: make build | xcpretty && 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: Upload to new stable release
uses: softprops/action-gh-release@v1
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 }}`
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

14
.gitignore vendored
View File

@@ -19,7 +19,7 @@ archive.xcarchive
*.perspectivev3
!default.perspectivev3
xcuserdata
AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm
## Other
*.xccheckout
*.moved-aside
@@ -33,4 +33,14 @@ xcuserdata
/.vscode
## AppCode specific
.idea/
.idea/
Payload/
SideStore.ipa
*.dSYM
Dependencies/.*-prebuilt-fetch-*
Dependencies/minimuxer/*
Dependencies/em_proxy/*
!Dependencies/**/.gitkeep
.nightly-build-num

11
.gitmodules vendored
View File

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

View File

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

View File

@@ -5,7 +5,7 @@
<key>ALTAppGroups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<string>group.com.rileytestut.AltStore</string>
<string>group.com.SideStore.SideStore</string>
</array>
<key>ALTBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

View File

@@ -77,7 +77,7 @@ extension XPCConnectionHandler: NSXPCListenerDelegate
guard
let codeSigningInfo = signingInfo as? [String: Any],
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
bundleIdentifier.contains("com.rileytestut.AltStore")
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
else { return false }
let connection = XPCConnection(newConnection)

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,86 +0,0 @@
{
"pins" : [
{
"identity" : "altsign",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/AltSign",
"state" : {
"branch" : "master",
"revision" : "7e0e7edcf8fbc44ac1e35da3e9030a297aa18b84"
}
},
{
"identity" : "appcenter-sdk-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
"state" : {
"revision" : "8354a50fe01a7e54e196d3b5493b5ab53dd5866a",
"version" : "4.4.2"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "launchatlogin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/LaunchAtLogin.git",
"state" : {
"revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
"version" : "4.2.0"
}
},
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke.git",
"state" : {
"revision" : "9318d02a8a6d20af56505c9673261c1fd3b3aebe",
"version" : "7.6.3"
}
},
{
"identity" : "openssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/OpenSSL",
"state" : {
"revision" : "033fcb41dac96b1b6effa945ca1f9ade002370b2",
"version" : "1.1.1501"
}
},
{
"identity" : "plcrashreporter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/PLCrashReporter.git",
"state" : {
"revision" : "6b27393cad517c067dceea85fadf050e70c4ceaa",
"version" : "1.10.1"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : {
"revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2",
"version" : "2.1.0"
}
},
{
"identity" : "stprivilegedtask",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JoeMatt/STPrivilegedTask.git",
"state" : {
"branch" : "master",
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
}
}
],
"version" : 2
}

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app"
BlueprintName = "SideStore"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app"
BlueprintName = "SideStore"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app"
BlueprintName = "SideStore"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Release">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key>

View File

@@ -14,13 +14,7 @@ import AppCenter
import AppCenterAnalytics
import AppCenterCrashes
#if DEBUG
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
#elseif RELEASE
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
#else
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
#endif
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
extension AnalyticsManager
{
@@ -77,7 +71,7 @@ extension AnalyticsManager
}
}
class AnalyticsManager
final class AnalyticsManager
{
static let shared = AnalyticsManager()

View File

@@ -25,7 +25,7 @@ extension AppContentViewController
}
}
class AppContentViewController: UITableViewController
final class AppContentViewController: UITableViewController
{
var app: StoreApp!
@@ -80,10 +80,21 @@ class AppContentViewController: UITableViewController
self.subtitleLabel.text = self.app.subtitle
self.descriptionTextView.text = self.app.localizedDescription
self.versionDescriptionTextView.text = self.app.versionDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
if let version = self.app.latestAvailableVersion
{
self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
}
else
{
self.versionDescriptionTextView.text = nil
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
}
self.descriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
@@ -173,7 +184,8 @@ private extension AppContentViewController
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
let cell = cell as! PermissionCollectionViewCell
cell.button.setImage(permission.type.icon, for: .normal)
cell.textLabel.text = permission.type.localizedShortName
cell.button.tintColor = .label
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
}
return dataSource

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,49 +18,51 @@ import EmotionalDamage
extension AppDelegate
{
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
static let addSourceDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL"
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
@available(iOS 14, *)
private var intentHandler: IntentHandler {
get { _intentHandler as! IntentHandler }
set { _intentHandler = newValue }
}
@available(iOS 14, *)
private var viewAppIntentHandler: ViewAppIntentHandler {
get { _viewAppIntentHandler as! ViewAppIntentHandler }
set { _viewAppIntentHandler = newValue }
}
private lazy var _intentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() }
return IntentHandler()
}()
private lazy var _viewAppIntentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() }
return ViewAppIntentHandler()
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Register default settings before doing anything else.
UserDefaults.registerDefaults()
DatabaseManager.shared.start { (error) in
if let error = error
{
@@ -71,50 +73,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
print("Started DatabaseManager.")
}
}
AnalyticsManager.shared.start()
self.setTintColor()
SecureValueTransformer.register()
SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil
{
Keychain.shared.reset()
UserDefaults.standard.firstLaunch = Date()
}
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
#if DEBUG || BETA
UserDefaults.standard.isDebugModeEnabled = true
#endif
self.prepareForBackgroundFetch()
return true
}
func applicationDidEnterBackground(_ application: UIApplication)
{
{
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
switch result
{
case .success: break
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
}
}
}
}
func applicationWillEnterForeground(_ application: UIApplication)
{
AppManager.shared.update()
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
{
return self.open(url)
}
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
{
guard #available(iOS 14, *) else { return nil }
switch intent
{
case is RefreshAllIntent: return self.intentHandler
@@ -133,7 +147,7 @@ extension AppDelegate
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{
// Called when the user discards a scene session.
@@ -148,36 +162,36 @@ private extension AppDelegate
{
self.window?.tintColor = .altPrimary
}
func open(_ url: URL) -> Bool
{
if url.isFileURL
{
guard url.pathExtension.lowercased() == "ipa" else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
}
return true
}
else
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host?.lowercased() else { return false }
switch host
{
case "patreon":
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
}
return true
case "appbackupresponse":
let result: Result<Void, Error>
switch url.path.lowercased()
{
case "/success": result = .success(())
@@ -188,37 +202,37 @@ private extension AppDelegate
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
let errorDescription = queryItems["errorDescription"]
else { return false }
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
result = .failure(error)
default: return false
}
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
return true
case "install":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
}
return true
case "source":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
}
return true
default: return false
}
}
@@ -231,47 +245,47 @@ extension AppDelegate
{
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
}
#if DEBUG
UIApplication.shared.registerForRemoteNotifications()
#endif
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
{
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
print("Push Token:", token)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
self.application(application, performFetchWithCompletionHandler: completionHandler)
}
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
{
let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("App Refresh Tip", comment: "")
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.presentedLaunchReminderNotification = true
}
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
if let error = taskResult.error
{
@@ -280,7 +294,7 @@ extension AppDelegate
taskCompletionHandler()
return
}
if !DatabaseManager.shared.isStarted
{
DatabaseManager.shared.start() { (error) in
@@ -309,7 +323,7 @@ extension AppDelegate
}
}
}
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
{
@@ -319,15 +333,15 @@ extension AppDelegate
case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData)
}
if !UserDefaults.standard.isBackgroundRefreshEnabled
{
refreshAppsCompletionHandler(.success([:]))
}
}
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
@@ -343,49 +357,49 @@ private extension AppDelegate
do
{
let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false
previousUpdatesFetchRequest.resultType = .dictionaryResultType
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
previousNewsItemsFetchRequest.includesPendingChanges = false
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
try context.save()
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
let updates = try context.fetch(updatesFetchRequest)
let newsItems = try context.fetch(newsItemsFetchRequest)
for update in updates
{
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp else { continue }
guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue }
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "")
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
for newsItem in newsItems
{
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
guard !newsItem.isSilent else { continue }
let content = UNMutableNotificationContent()
if let app = newsItem.storeApp
{
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
@@ -394,10 +408,10 @@ private extension AppDelegate
{
content.title = NSLocalizedString("SideStore News", comment: "")
}
content.body = newsItem.title
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
@@ -405,7 +419,7 @@ private extension AppDelegate
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count
}
completionHandler(.success(sources))
}
catch

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="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -13,8 +13,8 @@
<scene sceneID="lNR-II-WoW">
<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="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/>
@@ -36,19 +36,19 @@
<!--Authentication View Controller-->
<scene sceneID="OCd-xc-Ms7">
<objects>
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<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,7 +57,7 @@
<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="332" 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"/>
@@ -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>
@@ -258,13 +258,13 @@
<!--How it works-->
<scene sceneID="dMt-EA-SGy">
<objects>
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<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="17" width="264" height="61.5"/>
<rect key="frame" x="79" y="17.5" width="264" height="60.5"/>
<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"/>
@@ -319,7 +319,7 @@
<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">
<rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
<rect key="frame" x="0.0" y="25.5" width="264" height="35"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -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"/>
@@ -434,7 +434,7 @@
<!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX">
<objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -445,7 +445,7 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
<rect key="frame" x="16" y="570" width="343" height="89"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
@@ -493,12 +493,12 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2967" y="736"/>
<point key="canvasLocation" x="3025" y="734"/>
</scene>
<!--Select a Team-->
<scene sceneID="ioQ-WB-CLJ">
<objects>
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -506,11 +506,11 @@
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
<rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
<rect key="frame" x="0.0" y="0.0" width="334.5" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
@@ -550,20 +550,19 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1401" y="734"/>
<point key="canvasLocation" x="2114" y="734"/>
</scene>
</scenes>
<color key="tintColor" name="Primary"/>
<resources>
<namedColor name="Primary">
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</scenes>
<color key="tintColor" name="Primary"/>
<resources>
<namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsBackground">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsHighlighted">
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -8,6 +8,7 @@
import UIKit
import minimuxer
import AltStoreCore
import Roxas
@@ -94,7 +95,7 @@ private extension BrowseViewController
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
cell.bannerView.button.activityIndicatorView.style = .white
cell.bannerView.button.activityIndicatorView.style = .medium
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
// Otherwise, cell reuse can mess up some cached values.
@@ -113,9 +114,9 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress
if Date() < app.versionDate
if let versionDate = app.latestSupportedVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = app.versionDate
cell.bannerView.button.countdownDate = versionDate
}
else
{
@@ -167,14 +168,7 @@ private extension BrowseViewController
func updateDataSource()
{
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
self.dataSource.predicate = nil
}
else
{
self.dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta))
}
}
func fetchSource()
@@ -271,14 +265,20 @@ private extension BrowseViewController
previousProgress?.cancel()
return
}
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(error: error)
let toastView = ToastView(error: error, opensLog: true)
toastView.show(in: self)
case .success: print("Installed app:", app.bundleIdentifier)

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
import UIKit
class CollapsingTextView: UITextView
final class CollapsingTextView: UITextView
{
var isCollapsed = true {
didSet {
@@ -22,7 +22,7 @@ class CollapsingTextView: UITextView
}
}
var lineSpacing: CGFloat = 2 {
var lineSpacing: Double = 2 {
didSet {
self.setNeedsLayout()
}
@@ -34,7 +34,19 @@ class CollapsingTextView: UITextView
{
super.awakeFromNib()
self.layoutManager.delegate = self
self.initialize()
}
private func initialize()
{
if #available(iOS 16, *)
{
self.updateText()
}
else
{
self.layoutManager.delegate = self
}
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0
@@ -71,8 +83,10 @@ class CollapsingTextView: UITextView
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
if self.intrinsicContentSize.height > maximumCollapsedHeight
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)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
@@ -106,6 +120,25 @@ private extension CollapsingTextView
{
self.isCollapsed.toggle()
}
@available(iOS 16, *)
func updateText()
{
do
{
let style = NSMutableParagraphStyle()
style.lineSpacing = self.lineSpacing
var attributedText = try AttributedString(self.attributedText, including: \.uiKit)
attributedText[AttributeScopes.UIKitAttributes.ParagraphStyleAttribute.self] = style
self.attributedText = NSAttributedString(attributedText)
}
catch
{
print("[ALTLog] Failed to update CollapsingTextView line spacing:", error)
}
}
}
extension CollapsingTextView: NSLayoutManagerDelegate

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,19 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>ALTAppGroups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<string>group.com.rileytestut.AltStore</string>
<string>group.com.SideStore.SideStore</string>
</array>
<key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key>
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
@@ -56,6 +56,7 @@
<key>CFBundleURLSchemes</key>
<array>
<string>altstore</string>
<string>sidestore</string>
</array>
</dict>
<dict>
@@ -66,6 +67,7 @@
<key>CFBundleURLSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>sidestore-com.SideStore.SideStore</string>
</array>
</dict>
</array>
@@ -89,6 +91,13 @@
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
@@ -127,13 +136,10 @@
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>

View File

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

View File

@@ -14,7 +14,9 @@ import minimuxer
import AltStoreCore
import UniformTypeIdentifiers
class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{
private var didFinishLaunching = false
@@ -47,6 +49,44 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
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")
}
}
}
}
}
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
DispatchQueue.global().async {
self.askfornetwork()
}
print("SideJITServer Enabled")
}
#if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else {
@@ -54,6 +94,47 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
return
}
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? {
@@ -68,33 +149,55 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty {
!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"){
} 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 for your device. For more information, go to https://youtu.be/dQw4w9WgXcQ", preferredStyle: .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: 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 OK button to a dialog message
//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
}
}
@@ -121,14 +224,11 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
}
// Save to a file for next launch
let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
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")
}
@@ -140,16 +240,24 @@ class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
displayError("Choosing a pairing file was cancelled")
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
}
func start_minimuxer_threads(_ pairing_file: String) {
set_usbmuxd_socket()
let res = start_minimuxer(pairing_file: pairing_file)
if res != 0 {
displayError("minimuxer failed to start. Incorrect arguments were passed.")
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
try start(pairing_file, documentsDirectory)
} catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
}
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
}
else {
start_auto_mounter(documentsDirectory)
}
auto_mount_dev_image()
}
}
@@ -165,7 +273,19 @@ extension LaunchViewController
{
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
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()
}))

View File

@@ -0,0 +1,81 @@
//
// AppExtensionView.swift
// SideStore
//
// Created by June P on 8/17/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import SwiftUI
import CAltSign
extension ALTApplication: Identifiable {}
struct AppExtensionView: View {
var extensions: Set<ALTApplication>
@State var selection: [ALTApplication] = []
var completion: (_ selection: [ALTApplication]) -> Any?
var body: some View {
NavigationView {
List {
ForEach(self.extensions.sorted {
$0.bundleIdentifier < $1.bundleIdentifier
}, id: \.self) { item in
MultipleSelectionRow(title: item.bundleIdentifier, isSelected: !selection.contains(item)) {
if self.selection.contains(item) {
self.selection.removeAll(where: { $0 == item })
}
else {
self.selection.append(item)
}
}
}
}
.navigationTitle("App Extensions")
.onDisappear {
_ = completion(selection)
}
}
}
}
struct MultipleSelectionRow: View {
var title: String
var isSelected: Bool
var action: () -> Void
var body: some View {
SwiftUI.Button(action: self.action) {
HStack {
Text(self.title)
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
class AppExtensionViewHostingController: UIHostingController<AppExtensionView> {
var completion: Optional<(_ selection: [ALTApplication]) -> Any?> = nil
required init(extensions: Set<ALTApplication>, completion: @escaping (_ selection: [ALTApplication]) -> Any?) {
self.completion = completion
super.init(rootView: AppExtensionView(extensions: extensions, completion: completion))
}
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
extension AppExtensionViewHostingController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}

View File

@@ -8,12 +8,14 @@
import Foundation
import UIKit
import SwiftUI
import UserNotifications
import MobileCoreServices
import Intents
import Combine
import WidgetKit
import minimuxer
import AltStoreCore
import AltSign
import Roxas
@@ -28,7 +30,7 @@ extension AppManager
}
@available(iOS 13, *)
class AppManagerPublisher: ObservableObject
final class AppManagerPublisher: ObservableObject
{
@Published
fileprivate(set) var installationProgress = [String: Progress]()
@@ -37,17 +39,12 @@ class AppManagerPublisher: ObservableObject
fileprivate(set) var refreshProgress = [String: Progress]()
}
private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
{
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
}
class AppManager
final class AppManager
{
static let shared = AppManager()
private(set) var updatePatronsResult: Result<Void, Error>?
private let operationQueue = OperationQueue()
private let serialOperationQueue = OperationQueue()
@@ -307,6 +304,45 @@ extension AppManager
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
func clearAppCache(completion: @escaping (Result<Void, Error>) -> Void)
{
let clearAppCacheOperation = ClearAppCacheOperation()
clearAppCacheOperation.resultHandler = { result in
completion(result)
}
self.run([clearAppCacheOperation], context: nil)
}
func log(_ error: Error, operation: LoggedError.Operation, app: AppProtocol)
{
switch error {
case ~OperationError.Code.cancelled: return // Don't log cancelled events
default: break
}
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForSerialization()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: operation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
}
extension AppManager
@@ -359,7 +395,7 @@ extension AppManager
case .success(let source): fetchedSources.insert(source)
case .failure(let error):
let source = managedObjectContext.object(with: source.objectID) as! Source
source.error = (error as NSError).sanitizedForCoreData()
source.error = (error as NSError).sanitizedForSerialization()
errors[source] = error
}
@@ -392,7 +428,8 @@ extension AppManager
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
{
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
print("Authenticated for fetching App IDs with result:", result)
// result contains name, email, auth token, OTP and other possibly personal/account specific info. we don't want this logged
//print("Authenticated for fetching App IDs with result:", result)
}
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
@@ -446,7 +483,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw context.error ?? OperationError.unknown }
guard let result = results.values.first else { throw context.error ?? OperationError.unknown() }
completionHandler(result)
}
catch
@@ -465,7 +502,7 @@ extension AppManager
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
guard let storeApp = app.storeApp else {
completionHandler(.failure(OperationError.appNotFound))
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return Progress.discreteProgress(totalUnitCount: 1)
}
@@ -473,7 +510,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown }
guard let result = results.values.first else { throw OperationError.unknown() }
completionHandler(result)
}
catch
@@ -509,8 +546,8 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown }
guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -548,7 +585,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown }
guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -574,8 +611,8 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown }
guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -599,7 +636,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown }
guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -664,18 +701,25 @@ extension AppManager
@available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
class Context: OperationContext, EnableJITContext
final class Context: OperationContext, EnableJITContext
{
var installedApp: InstalledApp?
}
let appName = installedApp.name
let context = Context()
context.installedApp = installedApp
let enableJITOperation = EnableJITOperation(context: context)
enableJITOperation.resultHandler = { (result) in
completionHandler(result)
switch result {
case .success: completionHandler(.success(()))
case .failure(let nsError as NSError):
let localizedTitle = String(format: NSLocalizedString("Failed to enable JIT for %@", comment: ""), appName)
let error = nsError.withLocalizedTitle(localizedTitle)
self.log(error, operation: .enableJIT, app: installedApp)
}
}
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
@@ -684,7 +728,7 @@ extension AppManager
@available(iOS 14.0, *)
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
{
class Context: InstallAppOperationContext, PatchAppContext
final class Context: InstallAppOperationContext, PatchAppContext
{
}
@@ -753,12 +797,18 @@ extension AppManager
let progress = self.refreshProgress[app.bundleIdentifier]
return progress
}
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
{
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
return isActivelyManaging
}
}
extension AppManager
{
@discardableResult
func backgroundRefresh(_ installedApps: [InstalledApp], presentsNotifications: Bool = true, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> BackgroundRefreshAppsOperation
func backgroundRefresh(_ installedApps: [InstalledApp], presentsNotifications: Bool = false, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> BackgroundRefreshAppsOperation
{
let backgroundRefreshAppsOperation = BackgroundRefreshAppsOperation(installedApps: installedApps)
backgroundRefreshAppsOperation.resultHandler = completionHandler
@@ -805,12 +855,18 @@ private extension AppManager
return bundleIdentifier
}
}
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
{
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
return isActivelyManaging
var loggedErrorOperation: LoggedError.Operation {
switch self {
case .install: return .install
case .update: return .update
case .refresh: return .refresh
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}
}
@discardableResult
@@ -875,7 +931,9 @@ private extension AppManager
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
uti != nil ||
app.needsResign
app.needsResign ||
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
app.bundleIdentifier == StoreApp.altstoreAppID
{
// Resign app instead of just refreshing profiles because either:
// * Refreshing using different certificate
@@ -945,12 +1003,100 @@ private extension AppManager
}
else
{
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
}
performAppOperations()
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = false
}
}
return group
}
func removeAppExtensions(from application: ALTApplication, extensions: Set<ALTApplication>, _ presentingViewController: UIViewController, completion: @escaping (Result<Void, Error>) -> Void)
{
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
let firstSentence: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
}
else
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
}
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
completion(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
completion(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
do
{
for appExtension in application.appExtensions
{
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
do
{
for appExtension in selection
{
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
return nil
}
let suiview = popoverContentController.view!
suiview.translatesAutoresizingMaskIntoConstraints = false
popoverContentController.modalPresentationStyle = .popover
if let popoverPresentationController = popoverContentController.popoverPresentationController {
popoverPresentationController.sourceView = presentingViewController.view
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
popoverPresentationController.delegate = popoverContentController
DispatchQueue.main.async {
presentingViewController.present(popoverContentController, animated: true)
}
}
})
DispatchQueue.main.async {
presentingViewController.present(alertController, animated: true)
}
}
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 100)
@@ -1023,7 +1169,73 @@ private extension AppManager
}
verifyOperation.addDependency(downloadOperation)
/* Remove App Extensions */
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do
{
if let error = context.error
{
throw error
}
guard case .install = appOperation else {
operation.finish()
return
}
guard let extensions = context.app?.appExtensions else { throw OperationError.invalidParameters }
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
self?.removeAppExtensions(from: app, extensions: extensions, presentingViewController) { result in
switch result {
case .success(): break
case .failure(let error): context.error = error
}
operation.finish()
}
}
catch
{
group.context.error = error
operation.finish()
}
}
removeAppExtensionsOperation.addDependency(verifyOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(removeAppExtensionsOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles):
context.provisioningProfiles = provisioningProfiles
print("PROVISIONING PROFILES \(context.provisioningProfiles)")
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Deactivate Apps (if necessary) */
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do
@@ -1039,6 +1251,12 @@ private extension AppManager
{
throw error
}
guard let profiles = context.provisioningProfiles else { throw OperationError.invalidParameters }
if !profiles.contains(where: { $1.isFreeProvisioningProfile == true }) {
operation.finish()
return
}
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
@@ -1058,8 +1276,7 @@ private extension AppManager
operation.finish()
}
}
deactivateAppsOperation.addDependency(verifyOperation)
deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation)
/* Patch App */
let patchAppOperation = RSTAsyncBlockOperation { operation in
@@ -1121,7 +1338,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
@@ -1133,32 +1350,6 @@ private extension AppManager
patchAppOperation.addDependency(deactivateAppsOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(patchAppOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Resign */
let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in
@@ -1168,7 +1359,7 @@ private extension AppManager
case .success(let resignedApp): context.resignedApp = resignedApp
}
}
resignAppOperation.addDependency(fetchProvisioningProfilesOperation)
resignAppOperation.addDependency(patchAppOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
@@ -1211,7 +1402,57 @@ private extension AppManager
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
installOperation.addDependency(sendAppOperation)
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
let notificationRegistrationOperation = RSTAsyncBlockOperation { (operation) in
do
{
if let error = context.error
{
throw error
}
guard let app = context.installedApp else { operation.finish(); return }
let content = UNMutableNotificationContent()
content.title = "App Expiring Soon"
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = false
formatter.includesTimeRemainingPhrase = false
formatter.allowedUnits = [.day, .hour, .minute]
formatter.maximumUnitCount = 1
let scheduledDate = DateInterval(start: Date(), duration: 60 * 60 * 24 * 6)
guard let timeLeft = formatter.string(from: scheduledDate.end, to: app.expirationDate) else { operation.finish(); return }
content.body = "App \(app.name) is expiring in \(timeLeft). Open SideStore to refresh now"
var dateComponents = DateComponents()
dateComponents.calendar = Calendar.current
let trigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(identifier: app.bundleIdentifier, content: content, trigger: trigger)
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) {_ in
operation.finish()
}
}
catch
{
operation.finish()
}
}
notificationRegistrationOperation.addDependency(installOperation)
let operations = [downloadOperation, verifyOperation, removeAppExtensionsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, deactivateAppsOperation, patchAppOperation, resignAppOperation, sendAppOperation, installOperation, notificationRegistrationOperation]
group.add(operations)
self.run(operations, context: group.context)
@@ -1223,7 +1464,7 @@ private extension AppManager
let progress = Progress.discreteProgress(totalUnitCount: 100)
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.url)
context.app = ALTApplication(fileURL: app.fileURL)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
@@ -1244,14 +1485,21 @@ private extension AppManager
case .success(let installedApp):
completionHandler(.success(installedApp))
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound):
case .failure(MinimuxerError.ProfileInstall):
completionHandler(.failure(OperationError.noWiFi))
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound(name: app.name)):
// Fall back to installation if AltServer doesn't support newer provisioning profile requests,
// OR if the cached app could not be found and we may need to redownload it.
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
let installProgress = self._install(app, operation: operation, group: group) { (result) in
completionHandler(result)
if minimuxer.ready() {
let installProgress = self._install(app, operation: operation, group: group) { (result) in
completionHandler(result)
}
progress.addChild(installProgress, withPendingUnitCount: 40)
} else {
completionHandler(.failure(OperationError.noWiFi))
}
progress.addChild(installProgress, withPendingUnitCount: 40)
}
case .failure(let error):
@@ -1518,7 +1766,7 @@ private extension AppManager
}
guard let application = ALTApplication(fileURL: app.fileURL) else {
completionHandler(.failure(OperationError.appNotFound))
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return progress
}
@@ -1530,8 +1778,8 @@ private extension AppManager
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: app.name) }
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1662,21 +1910,42 @@ private extension AppManager
}
if #available(iOS 14, *)
{
WidgetCenter.shared.getCurrentConfigurations { (result) in
guard case .success(let widgets) = result else { return }
guard let widget = widgets.first(where: { $0.configuration is ViewAppIntent }) else { return }
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
}
{
WidgetCenter.shared.reloadAllTimelines()
}
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
}
catch
catch let nsError as NSError
{
var appName: String!
if let app = operation.app as? (NSManagedObject & AppProtocol) {
if let context = app.managedObjectContext {
context.performAndWait {
appName = app.name
}
} else {
appName = NSLocalizedString("Unknown App", comment: "")
}
} else {
appName = operation.app.name
}
let localizedTitle: String
switch operation {
case .install: localizedTitle = String(format: NSLocalizedString("Failed to Install %@", comment: ""), appName)
case .refresh: localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@", comment: ""), appName)
case .update: localizedTitle = String(format: NSLocalizedString("Failed to Update %@", comment: ""), appName)
case .activate: localizedTitle = String(format: NSLocalizedString("Failed to Activate %@", comment: ""), appName)
case .deactivate: localizedTitle = String(format: NSLocalizedString("Failed to Deactivate %@", comment: ""), appName)
case .backup: localizedTitle = String(format: NSLocalizedString("Failed to Backup %@", comment: ""), appName)
case .restore: localizedTitle = String(format: NSLocalizedString("Failed to Restore %@ Backup", comment: ""), appName)
}
let error = nsError.withLocalizedTitle(localizedTitle)
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, operation: operation.loggedErrorOperation, app: operation.app)
}
}
@@ -1693,14 +1962,15 @@ private extension AppManager
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("AltStore Expiring Soon", comment: "")
content.body = NSLocalizedString("AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
content.title = NSLocalizedString("SideStore Expiring Soon", comment: "")
content.body = NSLocalizedString("SideStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
content.sound = .default
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
{
// Find "Install AltStore" operation if it already exists in `context`

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,12 @@ import UIKit
import MobileCoreServices
import Intents
import Combine
import UniformTypeIdentifiers
import AltStoreCore
import AltSign
import Roxas
import minimuxer
import Nuke
@@ -30,7 +32,7 @@ extension MyAppsViewController
}
}
class MyAppsViewController: UICollectionViewController
final class MyAppsViewController: UICollectionViewController
{
private let coordinator = NSFileCoordinator()
private let operationQueue = OperationQueue()
@@ -153,6 +155,13 @@ class MyAppsViewController: UICollectionViewController
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
var minimuxerStatus: Bool {
guard minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
return false
}
return true
}
}
private extension MyAppsViewController
@@ -186,7 +195,7 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{
let fetchRequest = InstalledApp.updatesFetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.versionDate, ascending: true),
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestSupportedVersion?.date, ascending: false),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
@@ -195,21 +204,21 @@ private extension MyAppsViewController
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let self = self else { return }
guard let app = installedApp.storeApp else { return }
guard let app = installedApp.storeApp, let latestSupportedVersion = app.latestSupportedVersion else { return }
let cell = cell as! UpdateCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = app.versionDescription
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate
let appName: String
@@ -223,7 +232,7 @@ private extension MyAppsViewController
appName = app.name
}
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, app.version, versionDate)
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.version, versionDate)
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -327,21 +336,25 @@ private extension MyAppsViewController
let currentDate = Date()
let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate)
let numberOfDaysText: String
if numberOfDays == 1
{
numberOfDaysText = NSLocalizedString("1 day", comment: "")
}
else
{
numberOfDaysText = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
}
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = false
formatter.includesTimeRemainingPhrase = false
formatter.allowedUnits = [.day, .hour, .minute]
formatter.maximumUnitCount = 1
cell.bannerView.button.setTitle(formatter.string(from: currentDate, to: installedApp.expirationDate)?.uppercased(), for: .normal)
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText)
formatter.includesTimeRemainingPhrase = true
cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " "
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
@@ -461,17 +474,10 @@ private extension MyAppsViewController
func updateDataSource()
{
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
self.dataSource.predicate = nil
}
else
{
self.dataSource.predicate = NSPredicate(format: "%K == nil OR %K == NO OR %K == %@",
#keyPath(InstalledApp.storeApp),
#keyPath(InstalledApp.storeApp.isBeta),
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
}
}
}
@@ -529,11 +535,9 @@ private extension MyAppsViewController
guard !failures.isEmpty else { return }
let toastView: ToastView
if let failure = failures.first, results.count == 1
{
toastView = ToastView(error: failure.value)
ToastView(error: failure.value).show(in: self)
}
else
{
@@ -551,11 +555,10 @@ private extension MyAppsViewController
let error = failures.first?.value as NSError?
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
toastView = ToastView(text: localizedText, detailText: detailText)
let toastView = ToastView(text: localizedText, detailText: detailText, opensLog: true)
toastView.preferredDuration = 4.0
toastView.show(in: self)
}
toastView.show(in: self)
}
self.refreshGroup = nil
@@ -646,6 +649,8 @@ private extension MyAppsViewController
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
{
guard minimuxerStatus else { return }
self.isRefreshingAllApps = true
self.collectionView.collectionViewLayout.invalidateLayout()
@@ -689,8 +694,7 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath])
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
self.collectionView.reloadItems(at: [indexPath])
@@ -708,18 +712,11 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem)
{
let supportedTypes: [String]
guard minimuxerStatus else { return }
let supportedTypes = UTType.types(tag: "ipa", tagClass: .filenameExtension, conformingTo: nil)
if let types = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "ipa" as CFString, nil)?.takeRetainedValue()
{
supportedTypes = (types as NSArray).map { $0 as! String }
}
else
{
supportedTypes = ["com.apple.itunes.ipa"] // Declared by the system.
}
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
let documentPickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes, asCopy: true)
documentPickerViewController.delegate = self
self.present(documentPickerViewController, animated: true, completion: nil)
}
@@ -786,7 +783,7 @@ private extension MyAppsViewController
}
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
let unzipAppOperation = BlockOperation {
let unzipAppOperation = BlockOperation {
do
{
if let error = context.error
@@ -818,38 +815,7 @@ private extension MyAppsViewController
{
unzipAppOperation.addDependency(downloadOperation)
}
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do
{
if let error = context.error
{
throw error
}
guard let application = context.application else { throw OperationError.invalidParameters }
DispatchQueue.main.async {
self?.removeAppExtensions(from: application) { (result) in
switch result
{
case .success: removeAppExtensionsProgress.completedUnitCount = 1
case .failure(let error): context.error = error
}
operation.finish()
}
}
}
catch
{
context.error = error
operation.finish()
}
}
removeAppExtensionsOperation.addDependency(unzipAppOperation)
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
let installAppOperation = RSTAsyncBlockOperation { (operation) in
do
@@ -898,22 +864,23 @@ private extension MyAppsViewController
completion(.failure((OperationError.cancelled)))
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
completion(.failure(error))
}
}
}
installAppOperation.addDependency(unzipAppOperation)
progress.addChild(installProgress, withPendingUnitCount: 65)
installAppOperation.addDependency(removeAppExtensionsOperation)
self.sideloadingProgress = progress
self.sideloadingProgressView.progress = 0
self.sideloadingProgressView.isHidden = false
self.sideloadingProgressView.observedProgress = self.sideloadingProgress
let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 }
let operations = [downloadOperation, unzipAppOperation, installAppOperation].compactMap { $0 }
self.operationQueue.addOperations(operations, waitUntilFinished: false)
}
@@ -962,49 +929,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(())) }
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
do
{
for appExtension in application.appExtensions
{
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
})
self.present(alertController, animated: true, completion: nil)
}
}
private extension MyAppsViewController
@@ -1014,13 +938,14 @@ private extension MyAppsViewController
UIApplication.shared.open(installedApp.openAppURL) { success in
guard !success else { return }
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name))
toastView.show(in: self)
ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self)
}
}
func refresh(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else {
previousProgress?.cancel()
@@ -1042,6 +967,8 @@ private extension MyAppsViewController
func activate(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
func finish(_ result: Result<InstalledApp, Error>)
{
do
@@ -1062,8 +989,7 @@ private extension MyAppsViewController
DispatchQueue.main.async {
installedApp.isActive = false
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
@@ -1117,7 +1043,8 @@ private extension MyAppsViewController
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
{
guard installedApp.isActive else { return }
guard installedApp.isActive, minimuxerStatus else { return }
installedApp.isActive = false
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
@@ -1130,13 +1057,12 @@ private extension MyAppsViewController
}
catch
{
print("Failed to activate app:", error)
print("Failed to deactivate app:", error)
DispatchQueue.main.async {
installedApp.isActive = true
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
@@ -1158,7 +1084,7 @@ private extension MyAppsViewController
message = NSLocalizedString("This will also erase all backup data for this app.", comment: "")
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
AppManager.shared.remove(installedApp) { (result) in
@@ -1167,8 +1093,7 @@ private extension MyAppsViewController
case .success: break
case .failure(let error):
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
@@ -1179,6 +1104,8 @@ private extension MyAppsViewController
func backup(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
let title = NSLocalizedString("Start Backup?", comment: "")
let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "")
@@ -1200,9 +1127,8 @@ private extension MyAppsViewController
print("Failed to back up app:", error)
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}
@@ -1218,6 +1144,8 @@ private extension MyAppsViewController
func restore(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel)
@@ -1235,8 +1163,7 @@ private extension MyAppsViewController
print("Failed to restore app:", error)
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
@@ -1253,8 +1180,11 @@ private extension MyAppsViewController
{
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return }
let documentPicker = UIDocumentPickerViewController(url: backupURL, in: .exportToService)
documentPicker.delegate = self
let documentPicker = UIDocumentPickerViewController(forExporting: [backupURL], asCopy: true)
// Don't set delegate to avoid conflicting with import callbacks.
// documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil)
}
@@ -1308,8 +1238,7 @@ private extension MyAppsViewController
print("Failed to change app icon.", error)
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
}
}
}
@@ -1318,14 +1247,28 @@ private extension MyAppsViewController
@available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp)
{
let sidejitenabled = UserDefaults.standard.sidejitenable
if #unavailable(iOS 17) {
guard minimuxerStatus else { return }
}
if #available(iOS 17, *), !sidejitenabled {
ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self)
AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
switch result
{
case .success: break
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
ToastView(error: error, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
}
}
}
@@ -1472,7 +1415,7 @@ extension MyAppsViewController
let registeredAppIDs = team.appIDs.count
let maximumAppIDCount = 10
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
let remainingAppIDs = maximumAppIDCount - registeredAppIDs
if remainingAppIDs == 1
{
@@ -1483,7 +1426,7 @@ extension MyAppsViewController
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
}
footerView.textLabel.isHidden = false
footerView.textLabel.isHidden = remainingAppIDs < 0
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
@unknown default: break
@@ -2057,15 +2000,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
{
guard let fileURL = urls.first else { return }
switch controller.documentPickerMode
{
case .import, .open:
self.sideloadApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
}
case .exportToService, .moveToService: break
@unknown default: break
self.sideloadApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
}
}
}

View File

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

View File

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

View File

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

View File

@@ -12,8 +12,10 @@ import Network
import AltStoreCore
import AltSign
import minimuxer
enum AuthenticationError: LocalizedError
typealias AuthenticationError = AuthenticationErrorCode.Error
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
{
case noTeam
case noCertificate
@@ -22,11 +24,11 @@ enum AuthenticationError: LocalizedError
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
var errorFailureReason: String {
switch self {
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "")
case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
@@ -34,7 +36,7 @@ enum AuthenticationError: LocalizedError
}
@objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
{
let context: AuthenticatedOperationContext
@@ -212,8 +214,8 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
else { throw AuthenticationError.noTeam }
else { throw AuthenticationError(.noTeam) }
// Account
account.isActiveAccount = true
@@ -431,7 +433,7 @@ private extension AuthenticationOperation
}
else
{
completionHandler(.failure(error ?? OperationError.unknown))
completionHandler(.failure(error ?? OperationError.unknown()))
}
}
}
@@ -448,7 +450,7 @@ private extension AuthenticationOperation
if let team = teams.first {
return completionHandler(.success(team))
} else {
return completionHandler(.failure(AuthenticationError.noTeam))
return completionHandler(.failure(AuthenticationError(.noTeam)))
}
} else {
DispatchQueue.main.async {
@@ -459,7 +461,7 @@ private extension AuthenticationOperation
if !self.present(selectTeamViewController)
{
return completionHandler(.failure(AuthenticationError.noTeam))
return completionHandler(.failure(AuthenticationError(.noTeam)))
}
}
}
@@ -488,20 +490,20 @@ private extension AuthenticationOperation
{
func requestCertificate()
{
let machineName = "AltStore - " + UIDevice.current.name
let machineName: String = "SideStore - \(team.account.firstName)'s \(UIDevice.current.name)"
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw AuthenticationError.missingCertificate
throw AuthenticationError(.missingCertificate)
}
certificate.privateKey = privateKey
@@ -522,16 +524,50 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate])
{
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
let ourCertificates = certificates.filter { a in
a.machineName?.starts(with: "SideStore") == true || a.machineName?.starts(with: "AltStore") == true
}
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success
if ourCertificates.isEmpty {
return requestCertificate()
}
// We don't have private keys for any of the certificates,
// so we need to revoke one and create a new one.
var certsText = ""
for certificate in ourCertificates {
if let name = certificate.machineName {
certsText.append("\(name)\n")
}
}
let alertController = UIAlertController(title: NSLocalizedString("Would you like to revoke your previous certificates?\n\(certsText)", comment: ""), message: nil, preferredStyle: .alert)
let noAction = UIAlertAction(title: NSLocalizedString("No", comment: ""), style: .default) { (action) in
requestCertificate()
}
let yesAction = UIAlertAction(title: NSLocalizedString("Yes", comment: ""), style: .default) { (action) in
for certificate in ourCertificates {
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success
{
completionHandler(.failure(error))
}
}
}
requestCertificate()
}
alertController.addAction(noAction)
alertController.addAction(yesAction)
DispatchQueue.main.async {
if self.navigationController.presentingViewController != nil
{
completionHandler(.failure(error))
self.navigationController.present(alertController, animated: true, completion: nil)
}
else
{
requestCertificate()
self.presentingViewController?.present(alertController, animated: true, completion: nil)
}
}
}
@@ -579,8 +615,6 @@ private extension AuthenticationOperation
}
else
{
// We don't have private keys for any of the certificates,
// so we need to revoke one and create a new one.
replaceCertificate(from: certificates)
}
}
@@ -593,7 +627,7 @@ private extension AuthenticationOperation
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
guard let udid = fetch_udid()?.toString() else {
return completionHandler(.failure(OperationError.unknownUDID))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,9 +9,17 @@
import UIKit
import Combine
import minimuxer
import UniformTypeIdentifiers
import AltStoreCore
enum SideJITServerErrorType: Error {
case invalidURL
case errorConnecting
case deviceNotFound
case other(String)
}
@available(iOS 14, *)
protocol EnableJITContext
{
@@ -21,7 +29,7 @@ protocol EnableJITContext
}
@available(iOS 14, *)
class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
{
let context: Context
@@ -43,25 +51,105 @@ class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
}
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
installedApp.managedObjectContext?.perform {
let v = minimuxer_to_operation(code: 1)
do {
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier)
switch x {
case .Good:
self.finish(.success(()))
case .Bad(let code):
self.finish(.failure(minimuxer_to_operation(code: code)))
if #available(iOS 17, *) {
let sideJITenabled = UserDefaults.standard.sidejitenable
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
if sideJITenabled {
installedApp.managedObjectContext?.perform {
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
switch result {
case .failure(let error):
switch error {
case .invalidURL, .errorConnecting:
self.finish(.failure(OperationError.unableToConnectSideJIT))
case .deviceNotFound:
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
case .other(let message):
if let startRange = message.range(of: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
let pContent = message[startRange.upperBound..<endRange.lowerBound]
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
print(message + " + " + String(pContent))
} else {
print(message)
self.finish(.failure(OperationError.SideJITIssue(error: message)))
}
}
case .success():
self.finish(.success(()))
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
}
}
return
}
}
} else {
installedApp.managedObjectContext?.perform {
var retries = 3
while (retries > 0){
do {
try debug_app(installedApp.resignedBundleIdentifier)
self.finish(.success(()))
retries = 0
} catch {
retries -= 1
if (retries <= 0){
self.finish(.failure(error))
}
}
}
} catch Uhoh.Bad(let code) {
self.finish(.failure(minimuxer_to_operation(code: code)))
} catch {
self.finish(.failure(OperationError.unknown))
}
}
}
}
@available(iOS 17, *)
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
guard let udid = fetch_udid()?.toString() else {
completion(.failure(.other("Unable to get UDID")))
return
}
var SJSURL = serverurl
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
if !SJSURL.hasPrefix("http") {
completion(.failure(.invalidURL))
return
}
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
let url = URL(string: fullurl)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
if let error = error {
completion(.failure(.errorConnecting))
return
}
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
let content = UNMutableNotificationContent()
content.title = "JIT Successfully Enabled"
content.subtitle = "JIT Enabled For \(installedapp.name)"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
completion(.success(()))
} else {
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
completion(.failure(errorType))
}
}
task.resume()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,79 +8,186 @@
import Foundation
import AltSign
import AltStoreCore
import minimuxer
enum OperationError: LocalizedError
extension OperationError
{
case unknown
case unknownResult
case cancelled
case timedOut
enum Code: Int, ALTErrorCode, CaseIterable {
typealias Error = OperationError
// General
case unknown = 1000
case unknownResult
case cancelled
case timedOut
case unableToConnectSideJIT
case unableToRespondSideJITDevice
case wrongSideJITIP
case SideJITIssue // (error: String)
case refreshsidejit
case notAuthenticated
case appNotFound
case unknownUDID
case invalidApp
case invalidParameters
case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
case openAppFailed//(name: String)
case missingAppGroup
// Connection
case noWiFi = 1200
case tooNewError
case anisetteV1Error//(message: String)
case provisioningError//(result: String, message: String?)
case anisetteV3Error//(message: String)
case cacheClearError//(errors: [String])
}
static let unknownResult: OperationError = .init(code: .unknownResult)
static let cancelled: OperationError = .init(code: .cancelled)
static let timedOut: OperationError = .init(code: .timedOut)
static let unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
static let unknownUDID: OperationError = .init(code: .unknownUDID)
static let invalidApp: OperationError = .init(code: .invalidApp)
static let invalidParameters: OperationError = .init(code: .invalidParameters)
static let noSources: OperationError = .init(code: .noSources)
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
static let noWiFi: OperationError = .init(code: .noWiFi)
static let tooNewError: OperationError = .init(code: .tooNewError)
static let provisioningError: OperationError = .init(code: .provisioningError)
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
static let cacheClearError: OperationError = .init(code: .cacheClearError)
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
static func appNotFound(name: String?) -> OperationError {
OperationError(code: .appNotFound, appName: name)
}
static func openAppFailed(name: String?) -> OperationError {
OperationError(code: .openAppFailed, appName: name)
}
case notAuthenticated
case appNotFound
static func SideJITIssue(error: String?) -> OperationError {
var o = OperationError(code: .SideJITIssue)
o.errorFailure = error
return o
}
case unknownUDID
case invalidApp
case invalidParameters
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
case openAppFailed(name: String)
case missingAppGroup
case noDevice
case createService(name: String)
case getFromDevice(name: String)
case setArgument(name: String)
case afc
case install
case uninstall
case lookupApps
case detach
case functionArguments
case profileInstall
case noConnection
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
static func provisioningError(result: String, message: String?) -> OperationError {
var o = OperationError(code: .provisioningError, failureReason: result)
o.errorTitle = message
return o
}
static func cacheClearError(errors: [String]) -> OperationError {
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
}
static func anisetteV1Error(message: String) -> OperationError {
OperationError(code: .anisetteV1Error, failureReason: message)
}
static func anisetteV3Error(message: String) -> OperationError {
OperationError(code: .anisetteV3Error, failureReason: message)
}
}
struct OperationError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
var appName: String?
var requiredAppIDs: Int?
var availableAppIDs: Int?
var expirationDate: Date?
var sourceFile: String?
var sourceLine: UInt?
private var _failureReason: String?
private init(code: Code, failureReason: String? = nil,
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
self.code = code
self._failureReason = failureReason
self.appName = appName
self.requiredAppIDs = requiredAppIDs
self.availableAppIDs = availableAppIDs
self.expirationDate = expirationDate
self.sourceFile = sourceFile
self.sourceLine = sourceLine
}
var errorFailureReason: String {
switch self.code {
case .unknown:
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "")
guard let sourceFile, let sourceLine else { return failureReason }
failureReason += " (\(sourceFile) line \(sourceLine)"
return failureReason
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
case .noDevice: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
case .createService(let name): return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
case .getFromDevice(let name): return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
case .setArgument(let name): return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
case .afc: return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
case .install: return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
case .uninstall: return NSLocalizedString("Unable to uninstall the app", comment: "")
case .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", comment: "")
case .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "")
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "")
case .appNotFound:
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
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 .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
}
}
var recoverySuggestion: String? {
switch self
switch self.code
{
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
case .maximumAppIDLimitReached:
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
let message: String
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage }
var message: String
if requiredAppIDs > 1
{
let availableText: String
@@ -92,23 +199,23 @@ enum OperationError: LocalizedError
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
}
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage + "\n\n"
}
else
{
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.maximumUnitCount = 1
dateComponentsFormatter.unitsStyle = .full
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
message = baseMessage + " " + remainingTimeMessage
message = baseMessage + " "
}
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
let dateFormatter = DateComponentsFormatter()
dateFormatter.maximumUnitCount = 1
dateFormatter.unitsStyle = .full
let remainingTime = dateFormatter.string(from: dateComponents)!
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
return message
default: return nil
@@ -116,49 +223,66 @@ enum OperationError: LocalizedError
}
}
func minimuxer_to_operation(code: Int32) -> OperationError {
switch code {
case -1:
return OperationError.noDevice
case -2:
return OperationError.createService(name: "debug")
case -3:
return OperationError.createService(name: "instproxy")
case -4:
return OperationError.getFromDevice(name: "installed apps")
case -5:
return OperationError.getFromDevice(name: "path to the app")
case -6:
return OperationError.getFromDevice(name: "bundle path")
case -7:
return OperationError.setArgument(name: "max packet")
case -8:
return OperationError.setArgument(name: "working directory")
case -9:
return OperationError.setArgument(name: "argv")
case -10:
return OperationError.getFromDevice(name: "launch success")
case -11:
return OperationError.detach
case -12:
return OperationError.functionArguments
case -13:
return OperationError.createService(name: "AFC")
case -14:
return OperationError.afc
case -15:
return OperationError.install
case -16:
return OperationError.uninstall
case -17:
return OperationError.createService(name: "misagent")
case -18:
return OperationError.profileInstall
case -19:
return OperationError.profileInstall
case -20:
return OperationError.noConnection
default:
return OperationError.unknown
extension MinimuxerError: LocalizedError {
public var failureReason: String? {
switch self {
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: "")
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: "")
case .CreateDebug:
return self.createService(name: "debug")
case .LookupApps:
return self.getFromDevice(name: "installed apps")
case .FindApp:
return self.getFromDevice(name: "path to the app")
case .BundlePath:
return self.getFromDevice(name: "bundle path")
case .MaxPacket:
return self.setArgument(name: "max packet")
case .WorkingDirectory:
return self.setArgument(name: "working directory")
case .Argv:
return self.setArgument(name: "argv")
case .LaunchSuccess:
return self.getFromDevice(name: "launch success")
case .Detach:
return NSLocalizedString("Unable to detach from the app's process", comment: "")
case .Attach:
return NSLocalizedString("Unable to attach to the app's process", comment: "")
case .CreateInstproxy:
return self.createService(name: "instproxy")
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: "")
case .InstallApp(let message):
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
case .UninstallApp:
return NSLocalizedString("Unable to uninstall the app", comment: "")
case .CreateMisagent:
return self.createService(name: "misagent")
case .ProfileInstall:
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .ProfileRemove:
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
}
}
fileprivate func createService(name: String) -> String {
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
}
fileprivate func getFromDevice(name: String) -> String {
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
}
fileprivate func setArgument(name: String) -> String {
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

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