Compare commits

...

213 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
130 changed files with 4996 additions and 2197 deletions

2
.github/CODEOWNERS vendored
View File

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

View File

@@ -2,15 +2,14 @@ name: Bug Report
description: Report a bug description: Report a bug
title: "[BUG] " title: "[BUG] "
labels: ["bug"] labels: ["bug"]
assignees: assignees: []
- naturecodevoid
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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. 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/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.** **Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea - type: textarea
id: description id: description
attributes: attributes:

View File

@@ -3,7 +3,7 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Discord - name: Discord
url: https://discord.gg/RgpFBX3Q3k url: https://discord.gg/sidestore-949183273383395328
about: If you need support, please go here first instead of making an issue! about: If you need support, please go here first instead of making an issue!
- name: GitHub Discussions - name: GitHub Discussions
url: https://github.com/SideStore/SideStore/discussions url: https://github.com/SideStore/SideStore/discussions

View File

@@ -2,15 +2,14 @@ name: Feature Request
description: Suggest a feature description: Suggest a feature
title: "[FEATURE REQUEST] " title: "[FEATURE REQUEST] "
labels: ["enhancement"] labels: ["enhancement"]
assignees: assignees: []
- naturecodevoid
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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. 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/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.** **Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea - type: textarea
id: description id: description
attributes: attributes:

View File

@@ -10,6 +10,3 @@
<!-- Example: --> <!-- Example: -->
- [x] Finish UI changes - [x] Finish UI changes
- [ ] Test - [ ] Test
<!-- If your PR doesn't close an issue, you can remove the next line. -->
Closes #1234

View File

@@ -20,3 +20,58 @@ jobs:
format: name format: name
addTo: pull addTo: pull
# addTo: pullandissues # 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);
}

View File

@@ -27,11 +27,25 @@ jobs:
- name: Change version to tag - name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: 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 - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} 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 - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make build | xcpretty && exit ${PIPESTATUS[0]}
@@ -41,22 +55,6 @@ jobs:
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: make ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
@@ -88,3 +86,18 @@ jobs:
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

View File

@@ -7,7 +7,7 @@ DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1 BUILD_NUM=1
write() { write() {
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM/" -i '' Build.xcconfig sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > .nightly-build-num echo "$DATE,$BUILD_NUM" > .nightly-build-num
} }

View File

@@ -36,11 +36,24 @@ jobs:
- name: Increase nightly build number and set as version - name: Increase nightly build number and set as version
run: bash .github/workflows/increase-nightly-build-num.sh 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 - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} 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 - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make build | xcpretty && exit ${PIPESTATUS[0]}
@@ -50,22 +63,6 @@ jobs:
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: make ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
@@ -96,5 +93,17 @@ jobs:
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`
- name: Reset cache for apps.sidestore.io/nightly - name: Add version to IPA file name
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }} 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

@@ -23,13 +23,28 @@ jobs:
run: brew install ldid run: brew install ldid
- name: Add PR suffix to version - name: Add PR suffix to version
run: sed -e '/MARKETING_VERSION = .*/s/$/-pr.${{ github.event.pull_request.number }}/' -i '' Build.xcconfig 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 - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} 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 - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make build | xcpretty && exit ${PIPESTATUS[0]}
@@ -39,14 +54,17 @@ jobs:
- name: Convert to IPA - name: Convert to IPA
run: make 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 - name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore.ipa name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact - name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/ path: ./*.dSYM/

View File

@@ -27,11 +27,24 @@ jobs:
- name: Change version to tag - name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: 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 - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} 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 - name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make build | xcpretty && exit ${PIPESTATUS[0]}
@@ -41,22 +54,6 @@ jobs:
- name: Convert to IPA - name: Convert to IPA
run: make ipa run: make ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore.ipa
path: SideStore.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-dSYM
path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
@@ -85,3 +82,18 @@ jobs:
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

2
.gitignore vendored
View File

@@ -19,7 +19,7 @@ archive.xcarchive
*.perspectivev3 *.perspectivev3
!default.perspectivev3 !default.perspectivev3
xcuserdata xcuserdata
AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm
## Other ## Other
*.xccheckout *.xccheckout
*.moved-aside *.moved-aside

2
.gitmodules vendored
View File

@@ -9,7 +9,7 @@
url = https://github.com/libimobiledevice/libusbmuxd.git url = https://github.com/libimobiledevice/libusbmuxd.git
[submodule "Dependencies/libplist"] [submodule "Dependencies/libplist"]
path = Dependencies/libplist path = Dependencies/libplist
url = https://github.com/libimobiledevice/libplist.git url = https://github.com/SideStore/libplist.git
[submodule "Dependencies/MarkdownAttributedString"] [submodule "Dependencies/MarkdownAttributedString"]
path = Dependencies/MarkdownAttributedString path = Dependencies/MarkdownAttributedString
url = https://github.com/chockenberry/MarkdownAttributedString.git url = https://github.com/chockenberry/MarkdownAttributedString.git

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,95 +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" : "semanticversion",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
"state" : {
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
"version" : "0.3.5"
}
},
{
"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

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

View File

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

View File

@@ -217,8 +217,8 @@ final class AppViewController: UIViewController
self._shouldResetLayout = false 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 cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
let inset = 12 as CGFloat let inset = 12 as CGFloat
@@ -323,7 +323,7 @@ final class AppViewController: UIViewController
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
self.scrollView.scrollIndicatorInsets.top = statusBarHeight self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
// Adjust content offset + size. // Adjust content offset + size.
let contentOffset = self.scrollView.contentOffset let contentOffset = self.scrollView.contentOffset
@@ -384,7 +384,7 @@ private extension AppViewController
button.progress = progress button.progress = progress
} }
if let versionDate = self.app.latestVersion?.date, versionDate > Date() if let versionDate = self.app.latestAvailableVersion?.date, versionDate > Date()
{ {
self.bannerView.button.countdownDate = versionDate self.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = versionDate self.navigationBarDownloadButton.countdownDate = versionDate
@@ -510,7 +510,7 @@ extension AppViewController
catch catch
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) let toastView = ToastView(error: error, opensLog: true)
toastView.show(in: self) toastView.show(in: self)
} }
} }

View File

@@ -90,14 +90,21 @@ private extension AppIDsViewController
cell.bannerView.button.isUserInteractionEnabled = false cell.bannerView.button.isUserInteractionEnabled = false
cell.bannerView.buttonLabel.isHidden = false cell.bannerView.buttonLabel.isHidden = false
let currentDate = Date() let currentDate = Date()
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate) let formatter = DateComponentsFormatter()
let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays)) formatter.unitsStyle = .full
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal) 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 else
{ {

View File

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

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -13,8 +13,8 @@
<scene sceneID="lNR-II-WoW"> <scene sceneID="lNR-II-WoW">
<objects> <objects>
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController"> <navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/> <rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/> <color key="barTintColor" name="SettingsBackground"/>
@@ -36,19 +36,19 @@
<!--Authentication View Controller--> <!--Authentication View Controller-->
<scene sceneID="OCd-xc-Ms7"> <scene sceneID="OCd-xc-Ms7">
<objects> <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"> <view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View"> <view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="44" width="375" height="623"/> <rect key="frame" x="0.0" y="64" width="375" height="603"/>
</view> </view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv"> <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z"> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/> <rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="343" height="359.5"/> <rect key="frame" x="16" y="6" width="343" height="359.5"/>
@@ -57,7 +57,7 @@
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/> <rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
<rect key="frame" x="0.0" y="0.0" width="332" height="41"/> <rect key="frame" x="0.0" y="0.0" width="343" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -179,7 +179,7 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8"> <stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="518.5" width="343" height="96.5"/> <rect key="frame" x="16" y="498.5" width="343" height="96.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
@@ -198,6 +198,10 @@
</stackView> </stackView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/> <constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
</constraints> </constraints>
</view> </view>
@@ -215,19 +219,15 @@
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/> <constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/> <constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/> <constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/> <constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/> <constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/> <constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/> <constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/> <constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/> <constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/> <constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/> <constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/> <constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
</constraints> </constraints>
</view> </view>
@@ -258,13 +258,13 @@
<!--How it works--> <!--How it works-->
<scene sceneID="dMt-EA-SGy"> <scene sceneID="dMt-EA-SGy">
<objects> <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"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="44" width="375" height="564"/> <rect key="frame" x="0.0" y="64" width="375" height="544"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35" width="343" height="95.5"/> <rect key="frame" x="16" y="35" width="343" height="95.5"/>
@@ -298,7 +298,7 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="168" width="343" height="95.5"/> <rect key="frame" x="16" y="161" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -310,7 +310,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="17" width="264" height="61.5"/> <rect key="frame" x="79" y="17.5" width="264" height="60.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -319,7 +319,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable 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"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -329,7 +329,7 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="300.5" width="343" height="95.5"/> <rect key="frame" x="16" y="287.5" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -341,7 +341,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="16" width="264" height="64"/> <rect key="frame" x="79" y="15.5" width="264" height="64"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -360,7 +360,7 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="433.5" width="343" height="95.5"/> <rect key="frame" x="16" y="413.5" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -434,7 +434,7 @@
<!--Refresh AltStore--> <!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX"> <scene sceneID="9Vh-dM-OqX">
<objects> <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"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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"> <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"/> <rect key="frame" x="16" y="570" width="343" height="89"/>
<subviews> <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"/> <rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
@@ -493,12 +493,12 @@
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="2967" y="736"/> <point key="canvasLocation" x="3025" y="734"/>
</scene> </scene>
<!--Select a Team--> <!--Select a Team-->
<scene sceneID="ioQ-WB-CLJ"> <scene sceneID="ioQ-WB-CLJ">
<objects> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -506,11 +506,11 @@
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes> <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"/> <rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <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> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1401" y="734"/> <point key="canvasLocation" x="2114" y="734"/>
</scene> </scene>
</scenes> </scenes>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
<resources> <resources>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="SettingsBackground"> <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>
<namedColor name="SettingsHighlighted"> <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> </namedColor>
</resources> </resources>
</document> </document>

View File

@@ -108,11 +108,9 @@ private extension AuthenticationViewController
case .failure(let error as NSError): case .failure(let error as NSError):
DispatchQueue.main.async { 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) let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self) toastView.show(in: self)
self.toastView = toastView self.toastView = toastView

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>ALTAppGroups</key> <key>ALTAppGroups</key>
<array> <array>
<string>group.$(APP_GROUP_IDENTIFIER)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
@@ -9,12 +11,10 @@
</array> </array>
<key>ALTDeviceID</key> <key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string> <string>00008101-000129D63698001E</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key> <key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string> <string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key> <key>ALTServerID</key>
<string>https://ani.sidestore.io</string> <string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
@@ -44,8 +44,6 @@
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -93,6 +91,13 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_altserver._tcp</string> <string>_altserver._tcp</string>
@@ -131,13 +136,10 @@
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
@@ -204,7 +206,5 @@
</dict> </dict>
</dict> </dict>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

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

View File

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

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 Foundation
import UIKit import UIKit
import SwiftUI
import UserNotifications import UserNotifications
import MobileCoreServices import MobileCoreServices
import Intents import Intents
import Combine import Combine
import WidgetKit import WidgetKit
import minimuxer
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
@@ -37,17 +39,12 @@ final class AppManagerPublisher: ObservableObject
fileprivate(set) var refreshProgress = [String: Progress]() 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)
}
final class AppManager final class AppManager
{ {
static let shared = AppManager() static let shared = AppManager()
private(set) var updatePatronsResult: Result<Void, Error>? private(set) var updatePatronsResult: Result<Void, Error>?
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
private let serialOperationQueue = OperationQueue() private let serialOperationQueue = OperationQueue()
@@ -307,6 +304,45 @@ extension AppManager
presentingViewController.present(alertController, animated: true, completion: nil) presentingViewController.present(alertController, animated: true, completion: nil)
} }
} }
func clearAppCache(completion: @escaping (Result<Void, Error>) -> Void)
{
let clearAppCacheOperation = ClearAppCacheOperation()
clearAppCacheOperation.resultHandler = { result in
completion(result)
}
self.run([clearAppCacheOperation], context: nil)
}
func log(_ error: Error, operation: LoggedError.Operation, app: AppProtocol)
{
switch error {
case ~OperationError.Code.cancelled: return // Don't log cancelled events
default: break
}
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForSerialization()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: operation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
} }
extension AppManager extension AppManager
@@ -359,7 +395,7 @@ extension AppManager
case .success(let source): fetchedSources.insert(source) case .success(let source): fetchedSources.insert(source)
case .failure(let error): case .failure(let error):
let source = managedObjectContext.object(with: source.objectID) as! Source let source = managedObjectContext.object(with: source.objectID) as! Source
source.error = (error as NSError).sanitizedForCoreData() source.error = (error as NSError).sanitizedForSerialization()
errors[source] = error errors[source] = error
} }
@@ -447,7 +483,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw context.error ?? OperationError.unknown } guard let result = results.values.first else { throw context.error ?? OperationError.unknown() }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -466,7 +502,7 @@ extension AppManager
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
guard let storeApp = app.storeApp else { guard let storeApp = app.storeApp else {
completionHandler(.failure(OperationError.appNotFound)) completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return Progress.discreteProgress(totalUnitCount: 1) return Progress.discreteProgress(totalUnitCount: 1)
} }
@@ -474,7 +510,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown } guard let result = results.values.first else { throw OperationError.unknown() }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -510,8 +546,8 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown } guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -549,7 +585,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown } guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -575,8 +611,8 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown } guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -600,7 +636,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown } guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -670,13 +706,20 @@ extension AppManager
var installedApp: InstalledApp? var installedApp: InstalledApp?
} }
let appName = installedApp.name
let context = Context() let context = Context()
context.installedApp = installedApp context.installedApp = installedApp
let enableJITOperation = EnableJITOperation(context: context) let enableJITOperation = EnableJITOperation(context: context)
enableJITOperation.resultHandler = { (result) in enableJITOperation.resultHandler = { (result) in
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) self.run([enableJITOperation], context: context, requiresSerialQueue: true)
@@ -754,6 +797,12 @@ extension AppManager
let progress = self.refreshProgress[app.bundleIdentifier] let progress = self.refreshProgress[app.bundleIdentifier]
return progress return progress
} }
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
{
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
return isActivelyManaging
}
} }
extension AppManager extension AppManager
@@ -806,12 +855,18 @@ private extension AppManager
return bundleIdentifier return bundleIdentifier
} }
}
var loggedErrorOperation: LoggedError.Operation {
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool switch self {
{ case .install: return .install
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID) case .update: return .update
return isActivelyManaging case .refresh: return .refresh
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}
} }
@discardableResult @discardableResult
@@ -876,7 +931,9 @@ private extension AppManager
if app.certificateSerialNumber != group.context.certificate?.serialNumber || if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
uti != nil || uti != nil ||
app.needsResign app.needsResign ||
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
app.bundleIdentifier == StoreApp.altstoreAppID
{ {
// Resign app instead of just refreshing profiles because either: // Resign app instead of just refreshing profiles because either:
// * Refreshing using different certificate // * Refreshing using different certificate
@@ -946,12 +1003,100 @@ private extension AppManager
} }
else else
{ {
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
}
performAppOperations() performAppOperations()
DispatchQueue.main.schedule {
UIApplication.shared.isIdleTimerDisabled = false
}
} }
return group return group
} }
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 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) let progress = Progress.discreteProgress(totalUnitCount: 100)
@@ -1024,7 +1169,73 @@ private extension AppManager
} }
verifyOperation.addDependency(downloadOperation) 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) */ /* Deactivate Apps (if necessary) */
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do do
@@ -1040,6 +1251,12 @@ private extension AppManager
{ {
throw error throw error
} }
guard let profiles = context.provisioningProfiles else { throw OperationError.invalidParameters }
if !profiles.contains(where: { $1.isFreeProvisioningProfile == true }) {
operation.finish()
return
}
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters } guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
@@ -1059,8 +1276,7 @@ private extension AppManager
operation.finish() operation.finish()
} }
} }
deactivateAppsOperation.addDependency(verifyOperation) deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation)
/* Patch App */ /* Patch App */
let patchAppOperation = RSTAsyncBlockOperation { operation in let patchAppOperation = RSTAsyncBlockOperation { operation in
@@ -1134,32 +1350,6 @@ private extension AppManager
patchAppOperation.addDependency(deactivateAppsOperation) patchAppOperation.addDependency(deactivateAppsOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(patchAppOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Resign */ /* Resign */
let resignAppOperation = ResignAppOperation(context: context) let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in resignAppOperation.resultHandler = { (result) in
@@ -1169,7 +1359,7 @@ private extension AppManager
case .success(let resignedApp): context.resignedApp = resignedApp case .success(let resignedApp): context.resignedApp = resignedApp
} }
} }
resignAppOperation.addDependency(fetchProvisioningProfilesOperation) resignAppOperation.addDependency(patchAppOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
@@ -1212,7 +1402,57 @@ private extension AppManager
progress.addChild(installOperation.progress, withPendingUnitCount: 30) progress.addChild(installOperation.progress, withPendingUnitCount: 30)
installOperation.addDependency(sendAppOperation) installOperation.addDependency(sendAppOperation)
let operations = [downloadOperation, verifyOperation, 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) group.add(operations)
self.run(operations, context: group.context) self.run(operations, context: group.context)
@@ -1245,14 +1485,21 @@ private extension AppManager
case .success(let installedApp): case .success(let installedApp):
completionHandler(.success(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, // Fall back to installation if AltServer doesn't support newer provisioning profile requests,
// OR if the cached app could not be found and we may need to redownload it. // OR if the cached app could not be found and we may need to redownload it.
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return. app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
let installProgress = self._install(app, operation: operation, group: group) { (result) in if minimuxer.ready() {
completionHandler(result) 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): case .failure(let error):
@@ -1519,7 +1766,7 @@ private extension AppManager
} }
guard let application = ALTApplication(fileURL: app.fileURL) else { guard let application = ALTApplication(fileURL: app.fileURL) else {
completionHandler(.failure(OperationError.appNotFound)) completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return progress return progress
} }
@@ -1531,8 +1778,8 @@ private extension AppManager
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString) let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound } 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) let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp } guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1670,11 +1917,35 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() } do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) } catch { print("Error saving installed app.", error) }
} }
catch 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) group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, for: operation) self.log(error, operation: operation.loggedErrorOperation, app: operation.app)
} }
} }
@@ -1691,51 +1962,15 @@ private extension AppManager
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("AltStore Expiring Soon", comment: "") content.title = NSLocalizedString("SideStore 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.body = NSLocalizedString("SideStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
content.sound = .default content.sound = .default
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger) let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
func log(_ error: Error, for operation: AppOperation)
{
// Sanitize NSError on same thread before performing background task.
let sanitizedError = (error as NSError).sanitizedForCoreData()
let loggedErrorOperation: LoggedError.Operation = {
switch operation
{
case .install: return .install
case .update: return .update
case .refresh: return .refresh
case .activate: return .activate
case .deactivate: return .deactivate
case .backup: return .backup
case .restore: return .restore
}
}()
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
var app = operation.app
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context)
try context.save()
}
catch let saveError
{
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
}
}
}
func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false) func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false)
{ {
// Find "Install AltStore" operation if it already exists in `context` // Find "Install AltStore" operation if it already exists in `context`

View File

@@ -22,13 +22,27 @@ extension AppManager
var managedObjectContext: NSManagedObjectContext? var managedObjectContext: NSManagedObjectContext?
var errorDescription: String? { var localizedTitle: String? {
if let error = self.primaryError var localizedTitle: String?
{ self.managedObjectContext?.performAndWait {
return error.localizedDescription 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? var localizedDescription: String?
self.managedObjectContext?.performAndWait { self.managedObjectContext?.performAndWait {
@@ -67,8 +81,14 @@ extension AppManager
} }
var errorUserInfo: [String : Any] { var errorUserInfo: [String : Any] {
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] } let errors = Array(self.errors.values)
return [NSUnderlyingErrorKey: error] 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) init(_ error: Error)

View File

@@ -10,10 +10,12 @@ import UIKit
import MobileCoreServices import MobileCoreServices
import Intents import Intents
import Combine import Combine
import UniformTypeIdentifiers
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
import minimuxer
import Nuke import Nuke
@@ -153,6 +155,13 @@ final class MyAppsViewController: UICollectionViewController
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue) @IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{ {
} }
var minimuxerStatus: Bool {
guard minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
return false
}
return true
}
} }
private extension MyAppsViewController private extension MyAppsViewController
@@ -186,7 +195,7 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage> func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{ {
let fetchRequest = InstalledApp.updatesFetchRequest() let fetchRequest = InstalledApp.updatesFetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true), fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestSupportedVersion?.date, ascending: false),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)] NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
@@ -195,21 +204,21 @@ private extension MyAppsViewController
dataSource.cellIdentifierHandler = { _ in "UpdateCell" } dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let self = self else { return } guard let self = self else { return }
guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return } guard let app = installedApp.storeApp, let latestSupportedVersion = app.latestSupportedVersion else { return }
let cell = cell as! UpdateCollectionViewCell let cell = cell as! UpdateCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = app.versionDescription cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.bannerView.iconImageView.image = nil cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.configure(for: app) cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter) let versionDate = Date().relativeDateString(since: latestSupportedVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate cell.bannerView.subtitleLabel.text = versionDate
let appName: String let appName: String
@@ -223,7 +232,7 @@ private extension MyAppsViewController
appName = app.name appName = app.name
} }
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate) cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.version, versionDate)
cell.bannerView.button.isIndicatingActivity = false cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -327,21 +336,25 @@ private extension MyAppsViewController
let currentDate = Date() let currentDate = Date()
let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate) let numberOfDays = installedApp.expirationDate.numberOfCalendarDays(since: currentDate)
let numberOfDaysText: String
if numberOfDays == 1 let formatter = DateComponentsFormatter()
{ formatter.unitsStyle = .full
numberOfDaysText = NSLocalizedString("1 day", comment: "") formatter.includesApproximationPhrase = false
} formatter.includesTimeRemainingPhrase = false
else
{ formatter.allowedUnits = [.day, .hour, .minute]
numberOfDaysText = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
} formatter.maximumUnitCount = 1
cell.bannerView.button.setTitle(formatter.string(from: currentDate, to: installedApp.expirationDate)?.uppercased(), for: .normal)
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
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. // Make sure refresh button is correct size.
cell.layoutIfNeeded() cell.layoutIfNeeded()
@@ -522,11 +535,9 @@ private extension MyAppsViewController
guard !failures.isEmpty else { return } guard !failures.isEmpty else { return }
let toastView: ToastView
if let failure = failures.first, results.count == 1 if let failure = failures.first, results.count == 1
{ {
toastView = ToastView(error: failure.value) ToastView(error: failure.value).show(in: self)
} }
else else
{ {
@@ -544,11 +555,10 @@ private extension MyAppsViewController
let error = failures.first?.value as NSError? let error = failures.first?.value as NSError?
let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription let detailText = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
toastView = ToastView(text: localizedText, detailText: detailText) let toastView = ToastView(text: localizedText, detailText: detailText, opensLog: true)
toastView.preferredDuration = 4.0 toastView.preferredDuration = 4.0
toastView.show(in: self)
} }
toastView.show(in: self)
} }
self.refreshGroup = nil self.refreshGroup = nil
@@ -639,6 +649,8 @@ private extension MyAppsViewController
@IBAction func refreshAllApps(_ sender: UIBarButtonItem) @IBAction func refreshAllApps(_ sender: UIBarButtonItem)
{ {
guard minimuxerStatus else { return }
self.isRefreshingAllApps = true self.isRefreshingAllApps = true
self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.collectionViewLayout.invalidateLayout()
@@ -682,8 +694,7 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
@@ -701,18 +712,11 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem) @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() let documentPickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes, asCopy: true)
{
supportedTypes = (types as NSArray).map { $0 as! String }
}
else
{
supportedTypes = ["com.apple.itunes.ipa"] // Declared by the system.
}
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
documentPickerViewController.delegate = self documentPickerViewController.delegate = self
self.present(documentPickerViewController, animated: true, completion: nil) self.present(documentPickerViewController, animated: true, completion: nil)
} }
@@ -779,7 +783,7 @@ private extension MyAppsViewController
} }
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1) let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
let unzipAppOperation = BlockOperation { let unzipAppOperation = BlockOperation {
do do
{ {
if let error = context.error if let error = context.error
@@ -811,38 +815,7 @@ private extension MyAppsViewController
{ {
unzipAppOperation.addDependency(downloadOperation) 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 installProgress = Progress.discreteProgress(totalUnitCount: 100)
let installAppOperation = RSTAsyncBlockOperation { (operation) in let installAppOperation = RSTAsyncBlockOperation { (operation) in
do do
@@ -891,22 +864,23 @@ private extension MyAppsViewController
completion(.failure((OperationError.cancelled))) completion(.failure((OperationError.cancelled)))
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
completion(.failure(error)) completion(.failure(error))
} }
} }
} }
installAppOperation.addDependency(unzipAppOperation)
progress.addChild(installProgress, withPendingUnitCount: 65) progress.addChild(installProgress, withPendingUnitCount: 65)
installAppOperation.addDependency(removeAppExtensionsOperation)
self.sideloadingProgress = progress self.sideloadingProgress = progress
self.sideloadingProgressView.progress = 0 self.sideloadingProgressView.progress = 0
self.sideloadingProgressView.isHidden = false self.sideloadingProgressView.isHidden = false
self.sideloadingProgressView.observedProgress = self.sideloadingProgress 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) self.operationQueue.addOperations(operations, waitUntilFinished: false)
} }
@@ -955,49 +929,6 @@ private extension MyAppsViewController
cell.bannerView.iconImageView.isIndicatingActivity = false cell.bannerView.iconImageView.isIndicatingActivity = false
} }
func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result<Void, Error>) -> Void)
{
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
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 private extension MyAppsViewController
@@ -1007,13 +938,14 @@ private extension MyAppsViewController
UIApplication.shared.open(installedApp.openAppURL) { success in UIApplication.shared.open(installedApp.openAppURL) { success in
guard !success else { return } guard !success else { return }
let toastView = ToastView(error: OperationError.openAppFailed(name: installedApp.name)) ToastView(error: OperationError.openAppFailed(name: installedApp.name), opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
func refresh(_ installedApp: InstalledApp) func refresh(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
let previousProgress = AppManager.shared.refreshProgress(for: installedApp) let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else { guard previousProgress == nil else {
previousProgress?.cancel() previousProgress?.cancel()
@@ -1035,6 +967,8 @@ private extension MyAppsViewController
func activate(_ installedApp: InstalledApp) func activate(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
func finish(_ result: Result<InstalledApp, Error>) func finish(_ result: Result<InstalledApp, Error>)
{ {
do do
@@ -1055,8 +989,7 @@ private extension MyAppsViewController
DispatchQueue.main.async { DispatchQueue.main.async {
installedApp.isActive = false installedApp.isActive = false
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1110,7 +1043,8 @@ private extension MyAppsViewController
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil) func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
{ {
guard installedApp.isActive else { return } guard installedApp.isActive, minimuxerStatus else { return }
installedApp.isActive = false installedApp.isActive = false
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
@@ -1123,13 +1057,12 @@ private extension MyAppsViewController
} }
catch catch
{ {
print("Failed to activate app:", error) print("Failed to deactivate app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
installedApp.isActive = true installedApp.isActive = true
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
@@ -1151,7 +1084,7 @@ private extension MyAppsViewController
message = NSLocalizedString("This will also erase all backup data for this app.", comment: "") 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(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
AppManager.shared.remove(installedApp) { (result) in AppManager.shared.remove(installedApp) { (result) in
@@ -1160,8 +1093,7 @@ private extension MyAppsViewController
case .success: break case .success: break
case .failure(let error): case .failure(let error):
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1172,6 +1104,8 @@ private extension MyAppsViewController
func backup(_ installedApp: InstalledApp) func backup(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
let title = NSLocalizedString("Start Backup?", comment: "") let title = NSLocalizedString("Start Backup?", comment: "")
let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "") let message = NSLocalizedString("This will replace any previous backups. Please leave SideStore open until the backup is complete.", comment: "")
@@ -1193,9 +1127,8 @@ private extension MyAppsViewController
print("Failed to back up app:", error) print("Failed to back up app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue]) self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
} }
} }
@@ -1211,6 +1144,8 @@ private extension MyAppsViewController
func restore(_ installedApp: InstalledApp) func restore(_ installedApp: InstalledApp)
{ {
guard minimuxerStatus else { return }
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name) let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet) let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel) alertController.addAction(.cancel)
@@ -1228,8 +1163,7 @@ private extension MyAppsViewController
print("Failed to restore app:", error) print("Failed to restore app:", error)
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1246,8 +1180,11 @@ private extension MyAppsViewController
{ {
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return } guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return }
let documentPicker = UIDocumentPickerViewController(url: backupURL, in: .exportToService) let documentPicker = UIDocumentPickerViewController(forExporting: [backupURL], asCopy: true)
documentPicker.delegate = self
// Don't set delegate to avoid conflicting with import callbacks.
// documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil) self.present(documentPicker, animated: true, completion: nil)
} }
@@ -1301,8 +1238,7 @@ private extension MyAppsViewController
print("Failed to change app icon.", error) print("Failed to change app icon.", error)
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
} }
} }
} }
@@ -1311,14 +1247,28 @@ private extension MyAppsViewController
@available(iOS 14, *) @available(iOS 14, *)
func enableJIT(for installedApp: InstalledApp) 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 AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result switch result
{ {
case .success: break case .success: break
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self) AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
} }
} }
} }
@@ -1465,7 +1415,7 @@ extension MyAppsViewController
let registeredAppIDs = team.appIDs.count let registeredAppIDs = team.appIDs.count
let maximumAppIDCount = 10 let maximumAppIDCount = 10
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0) let remainingAppIDs = maximumAppIDCount - registeredAppIDs
if remainingAppIDs == 1 if remainingAppIDs == 1
{ {
@@ -1476,7 +1426,7 @@ extension MyAppsViewController
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs)) footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
} }
footerView.textLabel.isHidden = false footerView.textLabel.isHidden = remainingAppIDs < 0
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
@unknown default: break @unknown default: break
@@ -2050,15 +2000,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
{ {
guard let fileURL = urls.first else { return } guard let fileURL = urls.first else { return }
switch controller.documentPickerMode self.sideloadApp(at: fileURL) { (result) in
{ print("Sideloaded app at \(fileURL) with result:", result)
case .import, .open:
self.sideloadApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
}
case .exportToService, .moveToService: break
@unknown default: break
} }
} }
} }

View File

@@ -313,9 +313,8 @@ private extension NewsViewController
{ {
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case .failure(let error): case .failure(let error):
let toastView = ToastView(error: error) ToastView(error: error, opensLog: true).show(in: self)
toastView.show(in: self)
case .success: print("Installed app:", storeApp.bundleIdentifier) case .success: print("Installed app:", storeApp.bundleIdentifier)
} }
@@ -391,9 +390,9 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp) let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress footerView.bannerView.button.progress = progress
if let versionDate = storeApp.latestVersion?.date, versionDate > Date() if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date()
{ {
footerView.bannerView.button.countdownDate = storeApp.versionDate footerView.bannerView.button.countdownDate = versionDate
} }
else else
{ {
@@ -426,6 +425,10 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
return previousSize return previousSize
} }
// Take layout margins into account.
self.prototypeCell.layoutMargins.left = self.view.layoutMargins.left
self.prototypeCell.layoutMargins.right = self.view.layoutMargins.right
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width) let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
NSLayoutConstraint.activate([widthConstraint]) NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) } defer { NSLayoutConstraint.deactivate([widthConstraint]) }

View File

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

View File

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

View File

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

View File

@@ -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

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

View File

@@ -12,66 +12,108 @@ import Roxas
import AltStoreCore import AltStoreCore
import AltSign import AltSign
private extension DownloadAppOperation
{
struct DependencyError: ALTLocalizedError
{
let dependency: Dependency
let error: Error
var failure: String? {
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
}
var underlyingError: Error? {
return self.error
}
}
}
@objc(DownloadAppOperation) @objc(DownloadAppOperation)
final class DownloadAppOperation: ResultOperation<ALTApplication> final class DownloadAppOperation: ResultOperation<ALTApplication>
{ {
let app: AppProtocol let app: AppProtocol
let context: AppOperationContext let context: AppOperationContext
private let appName: String
private let bundleIdentifier: String private let bundleIdentifier: String
private var sourceURL: URL?
private let destinationURL: URL private let destinationURL: URL
private let session = URLSession(configuration: .default) private let session = URLSession(configuration: .default)
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL() private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext) init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
{ {
self.app = app self.app = app
self.context = context self.context = context
self.appName = app.name
self.bundleIdentifier = app.bundleIdentifier self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = destinationURL self.destinationURL = destinationURL
super.init() super.init()
// App = 3, Dependencies = 1 // App = 3, Dependencies = 1
self.progress.totalUnitCount = 4 self.progress.totalUnitCount = 4
} }
override func main() override func main()
{ {
super.main() super.main()
if let error = self.context.error if let error = self.context.error
{ {
self.finish(.failure(error)) self.finish(.failure(error))
return return
} }
print("Downloading App:", self.bundleIdentifier) print("Downloading App:", self.bundleIdentifier)
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) } self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
self.downloadApp(from: sourceURL) { result in 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 do
{ {
let application = try result.get() let application = try result.get()
@@ -112,24 +154,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
} }
} }
override func finish(_ result: Result<ALTApplication, Error>) func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectory)
}
catch
{
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
}
super.finish(result)
}
}
private extension DownloadAppOperation
{
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
{ {
func finishOperation(_ result: Result<URL, Error>) func finishOperation(_ result: Result<URL, Error>)
{ {
@@ -138,8 +163,8 @@ private extension DownloadAppOperation
let fileURL = try result.get() let fileURL = try result.get()
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound } 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) try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL: URL let appBundleURL: URL
@@ -178,6 +203,9 @@ private extension DownloadAppOperation
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
do do
{ {
if let response = response as? HTTPURLResponse {
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
}
let (fileURL, _) = try Result((fileURL, response), error).get() let (fileURL, _) = try Result((fileURL, response), error).get()
finishOperation(.success(fileURL)) finishOperation(.success(fileURL))
@@ -252,7 +280,7 @@ private extension DownloadAppOperation
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data) let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set<URL>() var dependencyURLs = Set<URL>()
var dependencyError: DependencyError? var dependencyError: Error?
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1) let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
@@ -285,7 +313,7 @@ private extension DownloadAppOperation
} }
catch let error as DecodingError catch let error as DecodingError
{ {
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not 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)) completionHandler(.failure(nsError))
} }
catch catch
@@ -294,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 let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do do
@@ -315,9 +343,10 @@ private extension DownloadAppOperation
completionHandler(.success(destinationURL)) 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) progress.addChild(downloadTask.progress, withPendingUnitCount: 1)

View File

@@ -9,9 +9,17 @@
import UIKit import UIKit
import Combine import Combine
import minimuxer import minimuxer
import UniformTypeIdentifiers
import AltStoreCore import AltStoreCore
enum SideJITServerErrorType: Error {
case invalidURL
case errorConnecting
case deviceNotFound
case other(String)
}
@available(iOS 14, *) @available(iOS 14, *)
protocol EnableJITContext protocol EnableJITContext
{ {
@@ -43,25 +51,105 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
} }
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) } guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
if #available(iOS 17, *) {
installedApp.managedObjectContext?.perform { let sideJITenabled = UserDefaults.standard.sidejitenable
let v = minimuxer_to_operation(code: 1) let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
do { if sideJITenabled {
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier) installedApp.managedObjectContext?.perform {
switch x { EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
case .Good: switch result {
self.finish(.success(())) case .failure(let error):
case .Bad(let code): switch error {
self.finish(.failure(minimuxer_to_operation(code: code))) 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 Foundation
import CommonCrypto
import Starscream
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
@objc(FetchAnisetteDataOperation) @objc(FetchAnisetteDataOperation)
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData> final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
{ {
let context: OperationContext let context: OperationContext
var socket: WebSocket!
var url: URL?
var startProvisioningURL: URL?
var endProvisioningURL: URL?
var clientInfo: String?
var userAgent: String?
var mdLu: String?
var deviceId: String?
init(context: OperationContext) init(context: OperationContext)
{ {
@@ -32,32 +45,413 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
return return
} }
let url = AnisetteManager.currentURL self.url = URL(string: UserDefaults.standard.menuAnisetteURL)
DLOG("Anisette URL: %@", url.absoluteString) print("Anisette URL: \(self.url!.absoluteString)")
let task = URLSession.shared.dataTask(with: url) { data, response, error in if let identifier = Keychain.shared.identifier,
guard let data = data, error == nil else { return } let adiPb = Keychain.shared.adiPb {
fetchAnisetteV3(identifier, adiPb)
do { } else {
// make sure this JSON is in the format we expect provision()
// 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 // MARK: - COMMON
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"]
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
if let anisette = ALTAnisetteData(json: formattedJSON) { // make sure this JSON is in the format we expect
DLOG("Anisette used: %@", formattedJSON) // convert data to json
self.finish(.success(anisette)) 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 { } catch let error as NSError {
print("Failed to load: \(error.localizedDescription)") print("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error)) self.finish(.failure(error))
} }
}.resume()
}
// MARK: - V3: PROVISIONING
func provision() {
fetchClientInfo {
print("Getting provisioning URLs")
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let startProvisioningString = plist["urls"]?["midStartProvisioning"] as? String,
let startProvisioningURL = URL(string: startProvisioningString),
let endProvisioningString = plist["urls"]?["midFinishProvisioning"] as? String,
let endProvisioningURL = URL(string: endProvisioningString) {
self.startProvisioningURL = startProvisioningURL
self.endProvisioningURL = endProvisioningURL
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
print("Starting a provisioning session")
self.startProvisioningSession()
} else {
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
}
}.resume()
}
}
func startProvisioningSession() {
let provisioningSessionURL = self.url!.appendingPathComponent("v3").appendingPathComponent("provisioning_session")
var wsRequest = URLRequest(url: provisioningSessionURL)
wsRequest.timeoutInterval = 5
self.socket = WebSocket(request: wsRequest)
self.socket.delegate = self
self.socket.connect()
}
func didReceive(event: WebSocketEvent, client: WebSocketClient) {
switch event {
case .text(let string):
do {
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
guard let result = json["result"] as? String else {
print("The server didn't give us a result")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
return
}
print("Received result: \(result)")
switch result {
case "GiveIdentifier":
print("Giving identifier")
client.json(["identifier": Keychain.shared.identifier!])
case "GiveStartProvisioningData":
print("Getting start provisioning data")
let body = [
"Header": [String: Any](),
"Request": [String: Any](),
]
var request = self.buildAppleRequest(url: self.startProvisioningURL!)
request.httpMethod = "POST"
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let spim = plist["Response"]?["spim"] as? String {
print("Giving start provisioning data")
client.json(["spim": spim])
} else {
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
}
}.resume()
case "GiveEndProvisioningData":
print("Getting end provisioning data")
guard let cpim = json["cpim"] as? String else {
print("The server didn't give us a cpim")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
return
}
let body = [
"Header": [String: Any](),
"Request": [
"cpim": cpim,
],
]
var request = self.buildAppleRequest(url: self.endProvisioningURL!)
request.httpMethod = "POST"
request.httpBody = try! PropertyListSerialization.data(fromPropertyList: body, format: .xml, options: 0)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let ptm = plist["Response"]?["ptm"] as? String,
let tk = plist["Response"]?["tk"] as? String {
print("Giving end provisioning data")
client.json(["ptm": ptm, "tk": tk])
} else {
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
}
}.resume()
case "ProvisioningSuccess":
print("Provisioning succeeded!")
client.disconnect(closeCode: 0)
guard let adiPb = json["adi_pb"] as? String else {
print("The server didn't give us an adi.pb file")
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
return
}
Keychain.shared.adiPb = adiPb
self.fetchAnisetteV3(Keychain.shared.identifier!, Keychain.shared.adiPb!)
default:
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
print("Failing because of \(result)")
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
}
}
}
} catch let error as NSError {
print("Failed to handle text: \(error.localizedDescription)")
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
}
case .connected:
print("Connected")
case .disconnected(let string, let code):
print("Disconnected: \(code); \(string)")
case .error(let error):
print("Got error: \(String(describing: error))")
default:
print("Unknown event: \(event)")
}
}
func buildAppleRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.setValue(self.clientInfo!, forHTTPHeaderField: "X-Mme-Client-Info")
request.setValue(self.userAgent!, forHTTPHeaderField: "User-Agent")
request.setValue("text/x-xml-plist", forHTTPHeaderField: "Content-Type")
request.setValue("*/*", forHTTPHeaderField: "Accept")
request.setValue(self.mdLu!, forHTTPHeaderField: "X-Apple-I-MD-LU")
request.setValue(self.deviceId!, forHTTPHeaderField: "X-Mme-Device-Id")
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
let dateString = formatter.string(from: Date())
request.setValue(dateString, forHTTPHeaderField: "X-Apple-I-Client-Time")
request.setValue(Locale.current.identifier, forHTTPHeaderField: "X-Apple-Locale")
request.setValue(TimeZone.current.abbreviation(), forHTTPHeaderField: "X-Apple-I-TimeZone")
return request
}
// MARK: - V3: FETCHING
func fetchClientInfo(_ callback: @escaping () -> Void) {
if self.clientInfo != nil &&
self.userAgent != nil &&
self.mdLu != nil &&
self.deviceId != nil &&
Keychain.shared.identifier != nil {
print("Skipping client_info fetch since all the properties we need aren't nil")
return callback()
}
print("Trying to get client_info")
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
do {
guard let data = data, error == nil else {
return self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The server may be down\(error != nil ? " (\(error!.localizedDescription))" : "")")))
}
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
if let clientInfo = json["client_info"] {
print("Server is V3")
self.clientInfo = clientInfo
self.userAgent = json["user_agent"]!
print("Client-Info: \(self.clientInfo!)")
print("User-Agent: \(self.userAgent!)")
if Keychain.shared.identifier == nil {
print("Generating identifier")
var bytes = [Int8](repeating: 0, count: 16)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
if status != errSecSuccess {
print("ERROR GENERATING IDENTIFIER!!! \(status)")
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
}
Keychain.shared.identifier = Data(bytes: &bytes, count: bytes.count).base64EncodedString()
}
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
self.mdLu = decoded.sha256().hexEncodedString()
print("X-Apple-I-MD-LU: \(self.mdLu!)")
let uuid: UUID = decoded.object()
self.deviceId = uuid.uuidString.uppercased()
print("X-Mme-Device-Id: \(self.deviceId!)")
callback()
} else { self.handleV1() }
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.handleV1()
}
}.resume()
}
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
fetchClientInfo {
print("Fetching anisette V3")
let url = UserDefaults.standard.menuAnisetteURL
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
request.httpMethod = "POST"
request.httpBody = try! JSONSerialization.data(withJSONObject: [
"identifier": identifier,
"adi_pb": adiPb
], options: [])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { data, response, error in
do {
guard let data = data, error == nil else { throw OperationError.anisetteV3Error(message: "Couldn't fetch anisette") }
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
} }
task.resume()
} }
} }
extension WebSocketClient {
func json(_ dictionary: [String: String]) {
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
self.write(string: String(data: data, encoding: .utf8)!)
}
}
extension Data {
// https://stackoverflow.com/a/25391020
func sha256() -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
self.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
}
return Data(hash)
}
// https://stackoverflow.com/a/40089462
func hexEncodedString() -> String {
return self.map { String(format: "%02hhX", $0) }.joined()
}
// https://stackoverflow.com/a/59127761
func object<T>() -> T { self.withUnsafeBytes { $0.load(as: T.self) } }
}

View File

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

View File

@@ -11,6 +11,7 @@ import Network
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
import minimuxer
@objc(InstallAppOperation) @objc(InstallAppOperation)
final class InstallAppOperation: ResultOperation<InstalledApp> final class InstallAppOperation: ResultOperation<InstalledApp>
@@ -40,12 +41,14 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
guard guard
let certificate = self.context.certificate, let certificate = self.context.certificate,
let resignedApp = self.context.resignedApp let resignedApp = self.context.resignedApp,
let provisioningProfiles = self.context.provisioningProfiles
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform { backgroundContext.perform {
/* App */ /* App */
let installedApp: InstalledApp let installedApp: InstalledApp
@@ -115,8 +118,7 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to. // Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
self.cleanUp() self.cleanUp()
var activeProfiles: Set<String>? if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit, provisioningProfiles.contains(where: { $1.isFreeProvisioningProfile == true })
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
{ {
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit. // When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
@@ -141,23 +143,70 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
installedApp.isActive = false installedApp.isActive = false
} }
} }
}
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in else
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier } {
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles installedApp.isActive = true
})
} }
let ns_bundle = NSString(string: installedApp.bundleIdentifier) var installing = true
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String) 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) do {
if res == 0 { try install_ipa(installedApp.bundleIdentifier)
installing = false
installedApp.refreshedDate = Date() installedApp.refreshedDate = Date()
self.finish(.success(installedApp)) self.finish(.success(installedApp))
} catch let error {
} else { installing = false
self.finish(.failure(minimuxer_to_operation(code: res))) self.finish(.failure(error))
} }
} }
} }
@@ -174,10 +223,11 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
do do
{ {
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
print("Removed refreshed IPA")
} }
catch catch
{ {
print("Failed to remove refreshed .ipa:", error) print("Failed to remove refreshed .ipa: \(error)")
} }
} }

View File

@@ -12,7 +12,10 @@ import Roxas
class ResultOperation<ResultType>: Operation class ResultOperation<ResultType>: Operation
{ {
var resultHandler: ((Result<ResultType, Error>) -> Void)? var resultHandler: ((Result<ResultType, Error>) -> Void)?
// Should only be set by subclasses
var localizedFailure: String?
@available(*, unavailable) @available(*, unavailable)
override func finish() override func finish()
{ {
@@ -22,16 +25,20 @@ class ResultOperation<ResultType>: Operation
func finish(_ result: Result<ResultType, Error>) func finish(_ result: Result<ResultType, Error>)
{ {
guard !self.isFinished else { return } guard !self.isFinished else { return }
var result = result
if self.isCancelled if self.isCancelled
{ {
self.resultHandler?(.failure(OperationError.cancelled)) result = .failure(OperationError.cancelled)
} }
else 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)
self.resultHandler?(result) let error = nsError.withLocalizedFailure(localizedFailure)
result = .failure(error)
} }
self.resultHandler?(result)
super.finish() super.finish()
} }
} }

View File

@@ -8,81 +8,186 @@
import Foundation import Foundation
import AltSign import AltSign
import AltStoreCore
import minimuxer
enum OperationError: LocalizedError extension OperationError
{ {
static let domain = OperationError.unknown._domain 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 unknown static func SideJITIssue(error: String?) -> OperationError {
case unknownResult var o = OperationError(code: .SideJITIssue)
case cancelled o.errorFailure = error
case timedOut return o
}
case notAuthenticated static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
case appNotFound OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
case unknownUDID
static func provisioningError(result: String, message: String?) -> OperationError {
case invalidApp var o = OperationError(code: .provisioningError, failureReason: result)
case invalidParameters o.errorTitle = message
return o
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date) }
case noSources static func cacheClearError(errors: [String]) -> OperationError {
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
case openAppFailed(name: String) }
case missingAppGroup
static func anisetteV1Error(message: String) -> OperationError {
case noDevice OperationError(code: .anisetteV1Error, failureReason: message)
case createService(name: String) }
case getFromDevice(name: String)
case setArgument(name: String) static func anisetteV3Error(message: String) -> OperationError {
case afc OperationError(code: .anisetteV3Error, failureReason: message)
case install }
case uninstall
case lookupApps }
case detach
case functionArguments
case profileInstall struct OperationError: ALTLocalizedError {
case noConnection
let code: Code
var failureReason: String? {
switch self { var errorTitle: String?
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "") 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 .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "") case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "") case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "") case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "") case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "") case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "") case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
case .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 accessed.", comment: "")
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "") case .appNotFound:
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "") let appName = self.appName ?? NSLocalizedString("The app", comment: "")
case .noDevice: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "") return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
case .createService(let name): return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name) case .openAppFailed:
case .getFromDevice(let name): return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name) let appName = self.appName ?? NSLocalizedString("The app", comment: "")
case .setArgument(let name): return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name) return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
case .afc: return NSLocalizedString("AFC was unable to manage files on the device", comment: "") case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
case .install: return NSLocalizedString("Unable to install the app from the staging directory", 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 .uninstall: return NSLocalizedString("Unable to uninstall the app", 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 .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", 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 .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "") case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "") case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "") case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", 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? { 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 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 if requiredAppIDs > 1
{ {
let availableText: String let availableText: String
@@ -94,23 +199,23 @@ enum OperationError: LocalizedError
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs)) default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
} }
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText) let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage message = prefixMessage + " " + baseMessage + "\n\n"
} }
else else
{ {
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date) message = baseMessage + " "
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.maximumUnitCount = 1
dateComponentsFormatter.unitsStyle = .full
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
message = baseMessage + " " + remainingTimeMessage
} }
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
let dateFormatter = DateComponentsFormatter()
dateFormatter.maximumUnitCount = 1
dateFormatter.unitsStyle = .full
let remainingTime = dateFormatter.string(from: dateComponents)!
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
return message return message
default: return nil default: return nil
@@ -118,49 +223,66 @@ enum OperationError: LocalizedError
} }
} }
func minimuxer_to_operation(code: Int32) -> OperationError { extension MinimuxerError: LocalizedError {
switch code { public var failureReason: String? {
case 1: switch self {
return OperationError.noDevice case .NoDevice:
case 2: return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
return OperationError.createService(name: "debug") case .NoConnection:
case 3: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi. This could mean an invalid pairing.", comment: "")
return OperationError.createService(name: "instproxy") case .PairingFile:
case 4: return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
return OperationError.getFromDevice(name: "installed apps")
case 5: case .CreateDebug:
return OperationError.getFromDevice(name: "path to the app") return self.createService(name: "debug")
case 6: case .LookupApps:
return OperationError.getFromDevice(name: "bundle path") return self.getFromDevice(name: "installed apps")
case 7: case .FindApp:
return OperationError.setArgument(name: "max packet") return self.getFromDevice(name: "path to the app")
case 8: case .BundlePath:
return OperationError.setArgument(name: "working directory") return self.getFromDevice(name: "bundle path")
case 9: case .MaxPacket:
return OperationError.setArgument(name: "argv") return self.setArgument(name: "max packet")
case 10: case .WorkingDirectory:
return OperationError.getFromDevice(name: "launch success") return self.setArgument(name: "working directory")
case 11: case .Argv:
return OperationError.detach return self.setArgument(name: "argv")
case 12: case .LaunchSuccess:
return OperationError.functionArguments return self.getFromDevice(name: "launch success")
case 13: case .Detach:
return OperationError.createService(name: "AFC") return NSLocalizedString("Unable to detach from the app's process", comment: "")
case 14: case .Attach:
return OperationError.afc return NSLocalizedString("Unable to attach to the app's process", comment: "")
case 15:
return OperationError.install case .CreateInstproxy:
case 16: return self.createService(name: "instproxy")
return OperationError.uninstall case .CreateAfc:
case 17: return self.createService(name: "AFC")
return OperationError.createService(name: "misagent") case .RwAfc:
case 18: return NSLocalizedString("AFC was unable to manage files on the device. This usually means an invalid pairing.", comment: "")
return OperationError.profileInstall case .InstallApp(let message):
case 19: return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
return OperationError.profileInstall case .UninstallApp:
case 20: return NSLocalizedString("Unable to uninstall the app", comment: "")
return OperationError.noConnection
default: case .CreateMisagent:
return OperationError.unknown 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 } var error: Error? { get }
} }
enum PatchAppError: LocalizedError extension PatchAppError
{ {
case unsupportedOperatingSystemVersion(OperatingSystemVersion) enum Code: Int, ALTErrorCode, CaseIterable {
typealias Error = PatchAppError
var errorDescription: String? {
switch self case unsupportedOperatingSystemVersion
{ }
case .unsupportedOperatingSystemVersion(let osVersion):
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)" static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
if osVersion.patchVersion != 0 PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
{ }
osVersionString += ".\(osVersion.patchVersion)" }
struct PatchAppError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
var osVersion: OperatingSystemVersion?
var errorFailureReason: String {
switch self.code {
case .unsupportedOperatingSystemVersion:
let osVersionString: String
if let osVersion = self.osVersion?.stringValue {
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
} else {
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
} }
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
} }
} }
} }

View File

@@ -439,7 +439,7 @@ private extension PatchViewController
do do
{ {
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown } guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() }
_ = try result.get() _ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier) if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,190 @@
//
// AnisetteServerList.swift
// SideStore
//
// Created by ny on 6/18/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import UIKit
import SwiftUI
import AltStoreCore
typealias SUIButton = SwiftUI.Button
// MARK: - AnisetteServerData
struct AnisetteServerData: Codable {
let servers: [Server]
}
// MARK: - Server
struct Server: Codable {
var name: String
var address: String
}
class AnisetteViewModel: ObservableObject {
@Published var selected: String = ""
@Published var source: String = "https://servers.sidestore.io/servers.json"
@Published var servers: [Server] = []
func getListOfServers() {
guard let url = URL(string: source) else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
return
}
if let data = data {
do {
let decoder = Foundation.JSONDecoder()
let servers = try decoder.decode(AnisetteServerData.self, from: data)
DispatchQueue.main.async {
self.servers = servers.servers
}
} catch {
// Handle decoding error
print("Failed to decode JSON: \(error)")
}
}
}.resume()
}
}
struct AnisetteServers: View {
@Environment(\.presentationMode) var presentationMode
@StateObject var viewModel: AnisetteViewModel = AnisetteViewModel()
@State var selected: String? = nil
var errorCallback: () -> ()
var body: some View {
ZStack {
Color(UIColor.systemBackground)
.ignoresSafeArea()
.onAppear {
viewModel.getListOfServers()
}
VStack {
if #available(iOS 16.0, *) {
SwiftUI.List($viewModel.servers, id: \.address, selection: $selected) { server in
HStack {
VStack(alignment: .leading) {
Text("\(server.name.wrappedValue)")
.font(.headline)
.foregroundColor(.primary)
Text("\(server.address.wrappedValue)")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
if selected != nil {
if server.address.wrappedValue == selected {
Spacer()
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.accentColor)
.onAppear {
UserDefaults.standard.menuAnisetteURL = server.address.wrappedValue
print(UserDefaults.synchronize(.standard)())
print(UserDefaults.standard.menuAnisetteURL)
print(server.address.wrappedValue)
}
}
}
}
.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemBackground)))
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.listRowBackground(Color(UIColor.systemBackground))
} else {
List(selection: $selected) {
ForEach($viewModel.servers, id: \.name) { server in
VStack {
HStack {
Text("\(server.name.wrappedValue)")
.foregroundColor(.primary)
.frame(alignment: .center)
Text("\(server.address.wrappedValue)")
.foregroundColor(.secondary)
.frame(alignment: .center)
}
}
Spacer()
}
.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemBackground)))
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
.listStyle(.plain)
}
VStack(spacing: 16) {
TextField("Anisette Server List", text: $viewModel.source)
.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(Color(UIColor.secondarySystemFill)))
.foregroundColor(.primary)
.frame(height: 60)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
.onChange(of: viewModel.source) { newValue in
UserDefaults.standard.menuAnisetteList = newValue
viewModel.getListOfServers()
}
HStack(spacing: 16) {
SUIButton(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
}
.buttonStyle(PlainButtonStyle())
.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor))
.foregroundColor(.white)
.shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5)
SUIButton(action: {
viewModel.getListOfServers()
}) {
Text("Refresh Servers")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
}
.buttonStyle(PlainButtonStyle())
.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor))
.foregroundColor(.white)
.shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5)
}
SUIButton(action: {
#if !DEBUG
if Keychain.shared.adiPb != nil {
Keychain.shared.adiPb = nil
}
#endif
print("Cleared adi.pb from keychain")
errorCallback()
presentationMode.wrappedValue.dismiss()
}) {
Text("Reset adi.pb")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
}
.buttonStyle(PlainButtonStyle())
.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.red))
.foregroundColor(.white)
.shadow(color: Color.red.opacity(0.4), radius: 10, x: 0, y: 5)
}
.padding(.horizontal)
.padding(.bottom)
}
}
.navigationBarHidden(true)
.navigationTitle("")
}
}

View File

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

View File

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

View File

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

View File

@@ -56,6 +56,8 @@ final class PatronsFooterView: UICollectionReusableView
final class AboutPatreonHeaderView: UICollectionReusableView final class AboutPatreonHeaderView: UICollectionReusableView
{ {
@IBOutlet var supportButton: UIButton! @IBOutlet var supportButton: UIButton!
@IBOutlet var twitterButton: UIButton!
@IBOutlet var instagramButton: UIButton!
@IBOutlet var accountButton: UIButton! @IBOutlet var accountButton: UIButton!
@IBOutlet var textView: UITextView! @IBOutlet var textView: UITextView!
@@ -79,12 +81,12 @@ final class AboutPatreonHeaderView: UICollectionReusableView
imageView.layer.cornerRadius = imageView.bounds.midY imageView.layer.cornerRadius = imageView.bounds.midY
} }
for button in [self.supportButton, self.accountButton].compactMap({ $0 }) for button in [self.supportButton, self.accountButton, self.twitterButton, self.instagramButton].compactMap({ $0 })
{ {
button.clipsToBounds = true button.clipsToBounds = true
button.layer.cornerRadius = 16 button.layer.cornerRadius = 16
} }
} }
override func layoutMarginsDidChange() override func layoutMarginsDidChange()
{ {

View File

@@ -111,7 +111,9 @@ private extension PatreonViewController
headerView.layoutMargins = self.view.layoutMargins headerView.layoutMargins = self.view.layoutMargins
headerView.supportButton.addTarget(self, action: #selector(PatreonViewController.openPatreonURL(_:)), for: .primaryActionTriggered) headerView.supportButton.addTarget(self, action: #selector(PatreonViewController.openPatreonURL(_:)), for: .primaryActionTriggered)
headerView.twitterButton.addTarget(self, action: #selector(PatreonViewController.openTwitterURL(_:)), for: .primaryActionTriggered)
headerView.instagramButton.addTarget(self, action: #selector(PatreonViewController.openInstagramURL(_:)), for: .primaryActionTriggered)
let defaultSupportButtonTitle = NSLocalizedString("Become a patron", comment: "") let defaultSupportButtonTitle = NSLocalizedString("Become a patron", comment: "")
let isPatronSupportButtonTitle = NSLocalizedString("View Patreon", comment: "") let isPatronSupportButtonTitle = NSLocalizedString("View Patreon", comment: "")
@@ -180,6 +182,24 @@ private extension PatreonViewController
self.present(safariViewController, animated: true, completion: nil) self.present(safariViewController, animated: true, completion: nil)
} }
@objc func openTwitterURL(_ sender: UIButton)
{
let twitterURL = URL(string: "https://twitter.com/SideStore_io")!
let safariViewController = SFSafariViewController(url: twitterURL)
safariViewController.preferredControlTintColor = self.view.tintColor
self.present(safariViewController, animated: true, completion: nil)
}
@objc func openInstagramURL(_ sender: UIButton)
{
let twitterURL = URL(string: "https://instagram.com/sidestore.io")!
let safariViewController = SFSafariViewController(url: twitterURL)
safariViewController.preferredControlTintColor = self.view.tintColor
self.present(safariViewController, animated: true, completion: nil)
}
@IBAction func authenticate(_ sender: UIBarButtonItem) @IBAction func authenticate(_ sender: UIBarButtonItem)
{ {
PatreonAPI.shared.authenticate { (result) in PatreonAPI.shared.authenticate { (result) in

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23090" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23079"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -20,8 +20,8 @@
<color key="backgroundColor" name="SettingsBackground"/> <color key="backgroundColor" name="SettingsBackground"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2"> <label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore 1.0" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
<rect key="frame" x="0.0" y="1082" width="375" height="25"/> <rect key="frame" x="0.0" y="1296" width="375" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -167,8 +167,8 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Join the beta" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Il-5a-5Zp"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Support the team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Il-5a-5Zp">
<rect key="frame" x="30" y="15.5" width="106" height="20.5"/> <rect key="frame" x="30" y="15.5" width="142.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -236,15 +236,51 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/> <userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="GYp-O0-pse" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="444" width="375" height="51"/> <rect key="frame" x="0.0" y="444" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GYp-O0-pse" id="vDG-ZV-xRS">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Disable Idle Timeout" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PCh-Up-aJJ">
<rect key="frame" x="30" y="15.5" width="166" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="iQA-wm-5ag">
<rect key="frame" x="296" y="10" width="51" height="31"/>
<connections>
<action selector="toggleNoIdleTimeoutEnabled:" destination="aMk-Xp-UL8" eventType="valueChanged" id="WSl-Jc-g5J"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="iQA-wm-5ag" secondAttribute="trailing" id="MJ1-HF-Dln"/>
<constraint firstItem="PCh-Up-aJJ" firstAttribute="leading" secondItem="vDG-ZV-xRS" secondAttribute="leadingMargin" id="Ocu-jn-RAQ"/>
<constraint firstItem="iQA-wm-5ag" firstAttribute="centerY" secondItem="vDG-ZV-xRS" secondAttribute="centerY" id="c6W-bN-VAb"/>
<constraint firstItem="PCh-Up-aJJ" firstAttribute="centerY" secondItem="vDG-ZV-xRS" secondAttribute="centerY" id="mL6-LB-cjn"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="495" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="amC-sE-8O0" id="GEO-2e-E4k"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="amC-sE-8O0" id="GEO-2e-E4k">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add to Siri…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Allow Siri To Refresh Apps…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
<rect key="frame" x="30" y="15.5" width="100.5" height="20.5"/> <rect key="frame" x="30" y="15.5" width="228.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -269,7 +305,7 @@
<tableViewSection headerTitle="" id="eHy-qI-w5w"> <tableViewSection headerTitle="" id="eHy-qI-w5w">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="30h-59-88f" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="30h-59-88f" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="535" width="375" height="51"/> <rect key="frame" x="0.0" y="586" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="30h-59-88f" id="7qD-DW-Jls"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="30h-59-88f" id="7qD-DW-Jls">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -309,7 +345,7 @@
<tableViewSection headerTitle="" id="J90-vn-u2O"> <tableViewSection headerTitle="" id="J90-vn-u2O">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="626" width="375" height="51"/> <rect key="frame" x="0.0" y="677" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -353,28 +389,28 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="oHX-oR-nwJ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="oHX-oR-nwJ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="677" width="375" height="51"/> <rect key="frame" x="0.0" y="728" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oHX-oR-nwJ" id="hN4-i5-igu"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oHX-oR-nwJ" id="hN4-i5-igu">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/> <rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X"> <stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/> <rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
<rect key="frame" x="129" y="0.0" width="18" height="20.5"/> <rect key="frame" x="129" y="0.0" width="18" height="20.5"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -397,28 +433,28 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="728" width="375" height="51"/> <rect key="frame" x="0.0" y="779" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/> <rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY"> <stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
<rect key="frame" x="206" y="15.5" width="139" height="20.5"/> <rect key="frame" x="206" y="15.5" width="139" height="20.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
<rect key="frame" x="121" y="0.0" width="18" height="20.5"/> <rect key="frame" x="121" y="0.0" width="18" height="20.5"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -441,19 +477,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="779" width="375" height="51"/> <rect key="frame" x="0.0" y="830" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
<rect key="frame" x="30" y="15.5" width="67.5" height="20.5"/> <rect key="frame" x="30" y="15.5" width="67.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
<rect key="frame" x="327" y="16.5" width="18" height="18"/> <rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -481,19 +517,19 @@
<tableViewSection headerTitle="" id="OMa-EK-hRI"> <tableViewSection headerTitle="" id="OMa-EK-hRI">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="870" width="375" height="51"/> <rect key="frame" x="0.0" y="921" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
<rect key="frame" x="30" y="15.5" width="125.5" height="20.5"/> <rect key="frame" x="30" y="15.5" width="125.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
<rect key="frame" x="327" y="16.5" width="18" height="18"/> <rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -514,19 +550,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="921" width="375" height="51"/> <rect key="frame" x="0.0" y="972" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
<rect key="frame" x="30" y="15.5" width="187.5" height="20.5"/> <rect key="frame" x="30" y="15.5" width="187.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
<rect key="frame" x="327" y="16.5" width="18" height="18"/> <rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -550,19 +586,19 @@
</connections> </connections>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="rE2-P4-OaE" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="972" width="375" height="51"/> <rect key="frame" x="0.0" y="1023" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rE2-P4-OaE" id="qIT-rz-ZUb">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PWC-OG-5jx"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PWC-OG-5jx">
<rect key="frame" x="30" y="15.5" width="119" height="20.5"/> <rect key="frame" x="30" y="15.5" width="119" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="VfB-c5-5wG"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="VfB-c5-5wG">
<rect key="frame" x="327" y="16.5" width="18" height="18"/> <rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -582,23 +618,89 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/> <userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
<connections> <connections>
<segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="SSW-vL-86I"/> <segue destination="g8a-Rf-zWa" kind="show" identifier="showErrorLog" id="vFC-Id-Ww6"/>
</connections> </connections>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1074" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VrV-qI-zXF" id="w1r-uY-4pD">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
<rect key="frame" x="30" y="15.5" width="183" height="21"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="wvD-eZ-nQI" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="O6Y-Y1-yxv"/>
<constraint firstItem="46q-DB-5nc" firstAttribute="centerY" secondItem="w1r-uY-4pD" secondAttribute="centerY" id="ROS-YF-6jb"/>
<constraint firstItem="46q-DB-5nc" firstAttribute="leading" secondItem="w1r-uY-4pD" secondAttribute="leadingMargin" id="acd-O8-WTI"/>
<constraint firstAttribute="trailingMargin" secondItem="wvD-eZ-nQI" secondAttribute="trailing" id="taB-EP-QMM"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eZ3-BT-q4D" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1125" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eZ3-BT-q4D" id="17m-VV-hzf">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Clear Cache" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IbH-V1-ce3">
<rect key="frame" x="30" y="15.5" width="98.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="FZe-BJ-fOm">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="FZe-BJ-fOm" firstAttribute="centerY" secondItem="17m-VV-hzf" secondAttribute="centerY" id="bGv-Np-5aO"/>
<constraint firstAttribute="trailingMargin" secondItem="FZe-BJ-fOm" secondAttribute="trailing" id="ccb-JP-Eqi"/>
<constraint firstItem="IbH-V1-ce3" firstAttribute="centerY" secondItem="17m-VV-hzf" secondAttribute="centerY" id="iQJ-gN-sRF"/>
<constraint firstItem="IbH-V1-ce3" firstAttribute="leading" secondItem="17m-VV-hzf" secondAttribute="leadingMargin" id="m1g-Y6-aT5"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1023" width="375" height="51"/> <rect key="frame" x="0.0" y="1176" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
<rect key="frame" x="30" y="15.5" width="140" height="20.5"/> <rect key="frame" x="30" y="15.5" width="140" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
<rect key="frame" x="327" y="16.5" width="18" height="18"/> <rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -618,28 +720,28 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/> <userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="fj2-EJ-Z98" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1074" width="375" height="51"/> <rect key="frame" x="0.0" y="1227" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fj2-EJ-Z98" id="BcT-Fs-KNg"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Advanced Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OcM-OM-uDE"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
<rect key="frame" x="30" y="15.5" width="154" height="20.5"/> <rect key="frame" x="30" y="15.5" width="135.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Pcu-Sy-yfZ"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
<rect key="frame" x="327" y="16.5" width="18" height="18"/> <rect key="frame" x="327" y="16.5" width="18" height="18"/>
</imageView> </imageView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="trailingMargin" secondItem="Pcu-Sy-yfZ" secondAttribute="trailing" id="CFy-IO-4eb"/> <constraint firstItem="0dh-yd-7i9" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="8OI-PI-weT"/>
<constraint firstItem="OcM-OM-uDE" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="OGl-h4-FPx"/> <constraint firstItem="eds-Dj-36y" firstAttribute="leading" secondItem="yjL-Mu-HTk" secondAttribute="leadingMargin" id="BqG-Ef-xQo"/>
<constraint firstItem="Pcu-Sy-yfZ" firstAttribute="centerY" secondItem="BcT-Fs-KNg" secondAttribute="centerY" id="R7L-4O-lTn"/> <constraint firstAttribute="trailingMargin" secondItem="0dh-yd-7i9" secondAttribute="trailing" id="TFW-nu-jo4"/>
<constraint firstItem="OcM-OM-uDE" firstAttribute="leading" secondItem="BcT-Fs-KNg" secondAttribute="leadingMargin" id="yoh-C6-UC5"/> <constraint firstItem="eds-Dj-36y" firstAttribute="centerY" secondItem="yjL-Mu-HTk" secondAttribute="centerY" id="YiJ-OF-FXE"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -656,7 +758,6 @@
</sections> </sections>
<connections> <connections>
<outlet property="dataSource" destination="aMk-Xp-UL8" id="c6c-fR-8C4"/> <outlet property="dataSource" destination="aMk-Xp-UL8" id="c6c-fR-8C4"/>
<outlet property="delegate" destination="aMk-Xp-UL8" id="moP-1B-lRq"/>
</connections> </connections>
</tableView> </tableView>
<navigationItem key="navigationItem" title="Settings" id="Ddg-UQ-KJ8"/> <navigationItem key="navigationItem" title="Settings" id="Ddg-UQ-KJ8"/>
@@ -665,6 +766,7 @@
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/> <outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/> <outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/> <outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
<outlet property="noIdleTimeoutSwitch" destination="iQA-wm-5ag" id="jHC-js-q0Y"/>
<outlet property="versionLabel" destination="bUR-rp-Nw2" id="85I-5R-hqz"/> <outlet property="versionLabel" destination="bUR-rp-Nw2" id="85I-5R-hqz"/>
</connections> </connections>
</tableViewController> </tableViewController>
@@ -680,7 +782,7 @@
<toolbarItems/> <toolbarItems/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/> <simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Jtn-cs-Tvp" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Jtn-cs-Tvp" 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"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/> <color key="barTintColor" name="SettingsBackground"/>
@@ -781,7 +883,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" layoutMarginsFollowReadableWidth="YES" contentInsetAdjustmentBehavior="never" indicatorStyle="white" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oQQ-pR-oKc"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" layoutMarginsFollowReadableWidth="YES" contentInsetAdjustmentBehavior="never" indicatorStyle="white" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oQQ-pR-oKc">
<rect key="frame" x="0.0" y="44" width="375" height="574"/> <rect key="frame" x="0.0" y="64" width="375" height="554"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/> <edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<string key="text">Jay Freeman (ldid) <string key="text">Jay Freeman (ldid)
Copyright (C) 2007-2012 Jay Freeman (saurik) Copyright (C) 2007-2012 Jay Freeman (saurik)
@@ -884,7 +986,7 @@ Settings by i cons from the Noun Project</string>
</objects> </objects>
<point key="canvasLocation" x="1697" y="313"/> <point key="canvasLocation" x="1697" y="313"/>
</scene> </scene>
<!--Patreon--> <!--Support us-->
<scene sceneID="Lnh-9P-HnL"> <scene sceneID="Lnh-9P-HnL">
<objects> <objects>
<collectionViewController id="dp8-8j-vt9" customClass="PatreonViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController id="dp8-8j-vt9" customClass="PatreonViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
@@ -931,7 +1033,7 @@ Settings by i cons from the Noun Project</string>
<outlet property="delegate" destination="dp8-8j-vt9" id="790-Kr-6l7"/> <outlet property="delegate" destination="dp8-8j-vt9" id="790-Kr-6l7"/>
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="Patreon" largeTitleDisplayMode="always" id="uUV-1f-xEq"/> <navigationItem key="navigationItem" title="Support us" largeTitleDisplayMode="always" id="uUV-1f-xEq"/>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="qq3-Hj-S9f" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="qq3-Hj-S9f" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
@@ -1015,7 +1117,7 @@ Settings by i cons from the Noun Project</string>
</textView> </textView>
</subviews> </subviews>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5"> <button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ba2-EY-tf5" customClass="ErrorLogMenuButton">
<rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/> <rect key="frame" x="0.0" y="0.0" width="343" height="107.5"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="NO"/> <bool key="isElement" value="NO"/>
@@ -1064,11 +1166,73 @@ Settings by i cons from the Noun Project</string>
</barButtonItem> </barButtonItem>
</rightBarButtonItems> </rightBarButtonItems>
</navigationItem> </navigationItem>
<connections>
<segue destination="7gm-d1-zWK" kind="presentation" identifier="showErrorDetails" id="9vz-y6-evp"/>
</connections>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1697" y="1774"/> <point key="canvasLocation" x="1697" y="1774"/>
</scene> </scene>
<!--Error Details View Controller-->
<scene sceneID="XNO-Yg-I7t">
<objects>
<viewController id="xB2-Se-VVg" customClass="ErrorDetailsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eBQ-se-VIy">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ctd-NB-4ov">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="Nm8-69-Ngi"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="ctd-NB-4ov" firstAttribute="leading" secondItem="eBQ-se-VIy" secondAttribute="leading" id="Cv1-Te-gBH"/>
<constraint firstItem="ctd-NB-4ov" firstAttribute="top" secondItem="eBQ-se-VIy" secondAttribute="top" id="HRY-Rg-iMI"/>
<constraint firstAttribute="trailing" secondItem="ctd-NB-4ov" secondAttribute="trailing" id="Lc1-K7-iuq"/>
<constraint firstAttribute="bottom" secondItem="ctd-NB-4ov" secondAttribute="bottom" id="zCz-Cy-Y5z"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="XpE-V9-EaY">
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="rnr-dX-4Ev">
<connections>
<segue destination="ZSp-1n-UJ9" kind="unwind" unwindAction="unwindFromErrorDetails:" id="TFu-zD-QyF"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="textView" destination="ctd-NB-4ov" id="x2C-9R-Xz1"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8AM-Vx-XTN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="ZSp-1n-UJ9" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="3389.5999999999999" y="1772.5637181409297"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="4LJ-Od-dCK">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7gm-d1-zWK" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="xB2-Se-VVg" kind="relationship" relationship="rootViewController" id="RpP-UM-JfJ"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="OXW-bf-HIj" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2554" y="1773"/>
</scene>
</scenes> </scenes>
<resources> <resources>
<image name="Next" width="18" height="18"/> <image name="Next" width="18" height="18"/>
@@ -1081,7 +1245,10 @@ Settings by i cons from the Noun Project</string>
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>
<systemColor name="labelColor"> <systemColor name="labelColor">
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>
</resources> </resources>
</document> </document>

View File

@@ -7,6 +7,7 @@
// //
import UIKit import UIKit
import SwiftUI
import SafariServices import SafariServices
import MessageUI import MessageUI
import Intents import Intents
@@ -30,13 +31,14 @@ extension SettingsViewController
fileprivate enum AppRefreshRow: Int, CaseIterable fileprivate enum AppRefreshRow: Int, CaseIterable
{ {
case backgroundRefresh case backgroundRefresh
case noIdleTimeout
@available(iOS 14, *) @available(iOS 14, *)
case addToSiri case addToSiri
static var allCases: [AppRefreshRow] { static var allCases: [AppRefreshRow] {
guard #available(iOS 14, *) else { return [.backgroundRefresh] } guard #available(iOS 14, *) else { return [.backgroundRefresh, .noIdleTimeout] }
return [.backgroundRefresh, .addToSiri] return [.backgroundRefresh, .noIdleTimeout, .addToSiri]
} }
} }
@@ -53,8 +55,12 @@ extension SettingsViewController
case sendFeedback case sendFeedback
case refreshAttempts case refreshAttempts
case errorLog case errorLog
case refreshSideJITServer
case clearCache
case resetPairingFile case resetPairingFile
case anisetteServers
case advancedSettings case advancedSettings
} }
} }
@@ -72,6 +78,9 @@ final class SettingsViewController: UITableViewController
@IBOutlet private var accountTypeLabel: UILabel! @IBOutlet private var accountTypeLabel: UILabel!
@IBOutlet private var backgroundRefreshSwitch: UISwitch! @IBOutlet private var backgroundRefreshSwitch: UISwitch!
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
@IBOutlet private var refreshSideJITServer: UILabel!
@IBOutlet private var versionLabel: UILabel! @IBOutlet private var versionLabel: UILabel!
@@ -84,6 +93,7 @@ final class SettingsViewController: UITableViewController
super.init(coder: aDecoder) super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
} }
override func viewDidLoad() override func viewDidLoad()
@@ -101,16 +111,36 @@ final class SettingsViewController: UITableViewController
debugModeGestureRecognizer.numberOfTouchesRequired = 3 debugModeGestureRecognizer.numberOfTouchesRequired = 3
self.tableView.addGestureRecognizer(debugModeGestureRecognizer) self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
var versionString: String = ""
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
{ {
self.versionLabel.text = NSLocalizedString(String(format: "SideStore %@", version), comment: "SideStore Version") versionString += "SideStore \(version)"
if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
versionString += " - Xcode \(xcode) - "
if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
versionString += "\(build)"
}
}
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String {
let pair_test = pairing == "<insert pairing file here>"
if !pair_test {
versionString += " - \(!pair_test)"
}
}
} }
else else
{ {
self.versionLabel.text = NSLocalizedString("SideStore", comment: "") versionString += "SideStore\t"
} }
versionString += "\n\(Bundle.Info.appbundleIdentifier)"
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
self.tableView.contentInset.bottom = 20 self.versionLabel.numberOfLines = 0
self.versionLabel.lineBreakMode = .byWordWrapping
self.versionLabel.setNeedsUpdateConstraints()
self.tableView.contentInset.bottom = 40
self.update() self.update()
@@ -127,6 +157,18 @@ final class SettingsViewController: UITableViewController
self.update() self.update()
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "anisetteServers" {
let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: {
ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self)
}))
self.show(controller, sender: nil)
} else {
super.prepare(for: segue, sender: sender)
}
}
} }
private extension SettingsViewController private extension SettingsViewController
@@ -147,6 +189,7 @@ private extension SettingsViewController
} }
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
if self.isViewLoaded if self.isViewLoaded
{ {
@@ -177,11 +220,11 @@ private extension SettingsViewController
case .patreon: case .patreon:
if isHeader if isHeader
{ {
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("PATREON", comment: "") settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("SUPPORT US", comment: "")
} }
else else
{ {
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Support the SideStore Team by becoming a patron!", comment: "") settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Support the SideStore Team by following our socials or becoming a patron!", comment: "")
} }
case .account: case .account:
@@ -198,7 +241,7 @@ private extension SettingsViewController
} }
else else
{ {
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi.", comment: "") settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi. \n\nDisable the Idle Timeout toggle to allow SideStore to not let your device go to sleep during a refresh or install of any apps.", comment: "")
} }
case .instructions: case .instructions:
@@ -279,6 +322,11 @@ private extension SettingsViewController
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
} }
@IBAction func toggleNoIdleTimeoutEnabled(_ sender: UISwitch)
{
UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn
}
@available(iOS 14, *) @available(iOS 14, *)
@IBAction func addRefreshAppsShortcut() @IBAction func addRefreshAppsShortcut()
{ {
@@ -290,6 +338,39 @@ private extension SettingsViewController
self.present(viewController, animated: true, completion: nil) self.present(viewController, animated: true, completion: nil)
} }
func clearCache()
{
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear SideStore's cache?", comment: ""),
message: NSLocalizedString("This will remove all temporary files as well as backups for uninstalled apps.", comment: ""),
preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { [weak self] _ in
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Clear Cache", comment: ""), style: .destructive) { [weak self] _ in
AppManager.shared.clearAppCache { result in
DispatchQueue.main.async {
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
switch result
{
case .success: break
case .failure(let error):
let alertController = UIAlertController(title: NSLocalizedString("Unable to Clear Cache", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self?.present(alertController, animated: true)
}
}
}
})
if let popoverController = alertController.popoverPresentationController {
popoverController.sourceView = self.view
popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
}
self.present(alertController, animated: true)
}
@IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer) @IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer)
{ {
self.debugGestureCounter += 1 self.debugGestureCounter += 1
@@ -344,6 +425,15 @@ private extension SettingsViewController
self.performSegue(withIdentifier: "showPatreon", sender: nil) self.performSegue(withIdentifier: "showPatreon", sender: nil)
} }
} }
@objc func openErrorLog(_: Notification) {
guard self.presentedViewController == nil else { return }
self.navigationController?.popViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.performSegue(withIdentifier: "showErrorLog", sender: nil)
}
}
} }
extension SettingsViewController extension SettingsViewController
@@ -385,6 +475,17 @@ extension SettingsViewController
cell.style = .single cell.style = .single
} }
if AppRefreshRow.AllCases().count == 1
{
if let cell = cell as? InsetGroupTableViewCell,
indexPath.section == Section.appRefresh.rawValue,
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
{
cell.style = .single
}
}
return cell return cell
} }
@@ -440,7 +541,7 @@ extension SettingsViewController
switch section switch section
{ {
case .signIn where self.activeTeam != nil: return 1.0 case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0 case .account where self.activeTeam == nil: return 1.0
case .signIn, .patreon, .appRefresh: case .signIn, .patreon, .appRefresh:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false) let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height return height
@@ -464,11 +565,13 @@ extension SettingsViewController
switch row switch row
{ {
case .backgroundRefresh: break case .backgroundRefresh: break
case .noIdleTimeout: break
case .addToSiri: case .addToSiri:
guard #available(iOS 14, *) else { return } guard #available(iOS 14, *) else { return }
self.addRefreshAppsShortcut() self.addRefreshAppsShortcut()
} }
case .credits: case .credits:
let row = CreditsRow.allCases[indexPath.row] let row = CreditsRow.allCases[indexPath.row]
switch row switch row
@@ -484,31 +587,158 @@ extension SettingsViewController
switch row switch row
{ {
case .sendFeedback: case .sendFeedback:
if MFMailComposeViewController.canSendMail() let alertController = UIAlertController(title: "Send Feedback", message: "Choose a method to send feedback:", preferredStyle: .actionSheet)
{
let mailViewController = MFMailComposeViewController() // Option 1: GitHub
mailViewController.mailComposeDelegate = self alertController.addAction(UIAlertAction(title: "GitHub", style: .default) { _ in
mailViewController.setToRecipients(["support@sidestore.io"]) if let githubURL = URL(string: "https://github.com/SideStore/SideStore/issues") {
let safariViewController = SFSafariViewController(url: githubURL)
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String safariViewController.preferredControlTintColor = .altPrimary
{ self.present(safariViewController, animated: true, completion: nil)
mailViewController.setSubject("SideStore Beta \(version) Feedback")
} }
else })
{
mailViewController.setSubject("SideStore Beta Feedback") // Option 2: Discord
alertController.addAction(UIAlertAction(title: "Discord", style: .default) { _ in
if let discordURL = URL(string: "https://discord.gg/sidestore-949183273383395328") {
let safariViewController = SFSafariViewController(url: discordURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true, completion: nil)
}
})
// Option 3: Mail
// alertController.addAction(UIAlertAction(title: "Send Email", style: .default) { _ in
// if MFMailComposeViewController.canSendMail() {
// let mailViewController = MFMailComposeViewController()
// mailViewController.mailComposeDelegate = self
// mailViewController.setToRecipients(["support@sidestore.io"])
//
// if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
// mailViewController.setSubject("SideStore Beta \(version) Feedback")
// } else {
// mailViewController.setSubject("SideStore Beta Feedback")
// }
//
// self.present(mailViewController, animated: true, completion: nil)
// } else {
// let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
// toastView.show(in: self)
// }
// })
// Cancel action
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
// For iPad: Set the source view if presenting on iPad to avoid crashes
if let popoverController = alertController.popoverPresentationController {
popoverController.sourceView = self.view
popoverController.sourceRect = self.view.bounds
}
// Present the action sheet
self.present(alertController, animated: true, completion: nil)
case .refreshSideJITServer:
if #available(iOS 17, *) {
let alertController = UIAlertController(
title: NSLocalizedString("SideJITServer", comment: ""),
message: NSLocalizedString("Settings for SideJITServer", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
if UserDefaults.standard.sidejitenable {
alertController.addAction(UIAlertAction(title: NSLocalizedString("Disable", comment: ""), style: .default){ _ in
UserDefaults.standard.sidejitenable = false
})
} else {
alertController.addAction(UIAlertAction(title: NSLocalizedString("Enable", comment: ""), style: .default){ _ in
UserDefaults.standard.sidejitenable = true
})
} }
self.present(mailViewController, animated: true, completion: nil) alertController.addAction(UIAlertAction(title: NSLocalizedString("Server Address", comment: ""), style: .default){ _ in
} let alertController1 = UIAlertController(title: "SideJITServer Address", message: "Please Enter the SideJITServer Address Below. (this is not needed if SideJITServer has already been detected)", preferredStyle: .alert)
else
{
let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil) alertController1.addTextField { textField in
toastView.show(in: self) textField.placeholder = "SideJITServer Address"
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController1.addAction(cancelAction)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
if let text = alertController1.textFields?.first?.text {
UserDefaults.standard.textInputSideJITServerurl = text
}
}
alertController1.addAction(okAction)
// Present the alert controller
self.present(alertController1, animated: true)
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .destructive){ _ in
if UserDefaults.standard.sidejitenable {
var SJSURL = ""
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
} else {
SJSURL = UserDefaults.standard.textInputSideJITServerurl ?? ""
}
let url = URL(string: SJSURL + "/re/")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("Error: \(error)")
} else {
// Do nothing with data or response
}
}
task.resume()
}
})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
} else {
let alertController = UIAlertController(
title: NSLocalizedString("You are not on iOS 17+ This will not work", comment: ""),
message: NSLocalizedString("This is meant for 'SideJITServer' and it only works on iOS 17+ ", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
} }
case .clearCache: self.clearCache()
case .resetPairingFile: case .resetPairingFile:
let filename = "ALTPairingFile.mobiledevicepairing" let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)") let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
let alertController = UIAlertController( let alertController = UIAlertController(
title: NSLocalizedString("Are you sure to reset the pairing file?", comment: ""), title: NSLocalizedString("Are you sure to reset the pairing file?", comment: ""),
@@ -517,11 +747,12 @@ extension SettingsViewController
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty { if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
UserDefaults.standard.isPairingReset = true
try? fm.removeItem(atPath: documentsPath.path) try? fm.removeItem(atPath: documentsPath.path)
NSLog("Pairing File Reseted") NSLog("Pairing File Reseted")
} }
self.tableView.deselectRow(at: indexPath, animated: true) self.tableView.deselectRow(at: indexPath, animated: true)
let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reseted", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert) let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reset", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert)
self.present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
}) })
alertController.addAction(.cancel) alertController.addAction(.cancel)
@@ -530,6 +761,12 @@ extension SettingsViewController
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath) alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true) self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true) self.tableView.deselectRow(at: indexPath, animated: true)
case .anisetteServers:
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: UIHostingController(rootView: AnisetteServers(selected: "", errorCallback: {
ToastView(text: "Reset adi.pb", detailText: "Buh").show(in: self)
}))), sender: nil)
// self.performSegue(withIdentifier: "anisetteServers", sender: nil)
case .advancedSettings: case .advancedSettings:
// Create the URL that deep links to your app's custom settings. // Create the URL that deep links to your app's custom settings.
if let url = URL(string: UIApplication.openSettingsURLString) { if let url = URL(string: UIApplication.openSettingsURLString) {
@@ -539,6 +776,7 @@ extension SettingsViewController
ELOG("UIApplication.openSettingsURLString invalid") ELOG("UIApplication.openSettingsURLString invalid")
} }
case .refreshAttempts, .errorLog: break case .refreshAttempts, .errorLog: break
} }
default: break default: break

View File

@@ -12,17 +12,22 @@ import CoreData
import AltStoreCore import AltStoreCore
import Roxas import Roxas
struct SourceError: LocalizedError struct SourceError: ALTLocalizedError
{ {
enum Code enum Code: Int, ALTErrorCode
{ {
typealias Error = SourceError
case unsupported case unsupported
} }
var code: Code var code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var source: Source @Managed var source: Source
var errorDescription: String? { var errorFailureReason: String {
switch self.code switch self.code
{ {
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name) case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of SideStore.", comment: ""), self.$source.name)
@@ -197,7 +202,7 @@ private extension SourcesViewController
{ {
let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert) let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in alertController.addTextField { (textField) in
textField.placeholder = "https://apps.altstore.io" textField.placeholder = "https://apps.sidestore.io"
textField.textContentType = .URL textField.textContentType = .URL
} }
alertController.addAction(.cancel) alertController.addAction(.cancel)
@@ -545,19 +550,19 @@ extension SourcesViewController: UICollectionViewDelegateFlowLayout
footerView.textView.delegate = self footerView.textView.delegate = self
let attributedText = NSMutableAttributedString( let attributedText = NSMutableAttributedString(
string: NSLocalizedString("SideStore has reviewed these sources to make sure they meet our safety standards.\n\nSupport for untrusted sources is currently in beta, but you can help test them out by", comment: ""), string: NSLocalizedString("SideStore has reviewed these sources to make sure they meet our safety standards.", comment: ""),
attributes: [.font: font, .foregroundColor: UIColor.gray] attributes: [.font: font, .foregroundColor: UIColor.gray]
) )
attributedText.mutableString.append(" ") //attributedText.mutableString.append(" ")
let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize) //let boldedFont = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
let openPatreonURL = URL(string: "https://SideStore.io/patreon")! //let openPatreonURL = URL(string: "https://SideStore.io/")!
let joinPatreonText = NSAttributedString( // let joinPatreonText = NSAttributedString(
string: NSLocalizedString("joining our Patreon.", comment: ""), // string: NSLocalizedString("", comment: ""),
attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear] // attributes: [.font: boldedFont, .link: openPatreonURL, .underlineColor: UIColor.clear]
) //)
attributedText.append(joinPatreonText) //attributedText.append(joinPatreonText)
footerView.textView.attributedText = attributedText footerView.textView.attributedText = attributedText
footerView.textView.textAlignment = .natural footerView.textView.textAlignment = .natural

View File

@@ -33,6 +33,7 @@ final class TabBarController: UITabBarController
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
} }
override func viewDidAppear(_ animated: Bool) override func viewDidAppear(_ animated: Bool)
@@ -141,4 +142,7 @@ private extension TabBarController
{ {
self.selectedIndex = Tab.myApps.rawValue self.selectedIndex = Tab.myApps.rawValue
} }
@objc func openErrorLog(_: Notification){
self.selectedIndex = Tab.settings.rawValue
}
} }

View File

@@ -10,23 +10,27 @@ import Foundation
import CoreData import CoreData
@propertyWrapper @dynamicMemberLookup @propertyWrapper @dynamicMemberLookup
struct Managed<ManagedObject: NSManagedObject> struct Managed<ManagedObject>
{ {
var wrappedValue: ManagedObject { var wrappedValue: ManagedObject {
didSet { didSet {
self.managedObjectContext = self.wrappedValue.managedObjectContext self.managedObjectContext = self.managedObject?.managedObjectContext
} }
} }
private var managedObjectContext: NSManagedObjectContext?
var projectedValue: Managed<ManagedObject> { var projectedValue: Managed<ManagedObject> {
return self return self
} }
private var managedObjectContext: NSManagedObjectContext?
private var managedObject: NSManagedObject? {
return self.wrappedValue as? NSManagedObject
}
init(wrappedValue: ManagedObject) init(wrappedValue: ManagedObject)
{ {
self.wrappedValue = wrappedValue self.wrappedValue = wrappedValue
self.managedObjectContext = wrappedValue.managedObjectContext self.managedObjectContext = self.managedObject?.managedObjectContext
} }
subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T subscript<T>(dynamicMember keyPath: KeyPath<ManagedObject, T>) -> T
@@ -46,4 +50,18 @@ struct Managed<ManagedObject: NSManagedObject>
return result return result
} }
// Optionals
subscript<Wrapped, T>(dynamicMember keyPath: KeyPath<Wrapped, T>) -> T? where ManagedObject == Optional<Wrapped> {
var result: T?
if let context = self.managedObjectContext {
context.performAndWait {
result = self.wrappedValue?[keyPath: keyPath] as? T
}
} else {
result = self.wrappedValue?[keyPath: keyPath] as? T
}
return result
}
} }

View File

@@ -23,5 +23,6 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
// Shared // Shared
#import <AltStoreCore/ALTConstants.h> #import <AltStoreCore/ALTConstants.h>
#import <AltStoreCore/ALTConnection.h> #import <AltStoreCore/ALTConnection.h>
#import <AltStoreCore/ALTWrappedError.h>
#import <AltStoreCore/NSError+ALTServerError.h> #import <AltStoreCore/NSError+ALTServerError.h>
#import <AltStoreCore/CFNotificationName+AltStore.h> #import <AltStoreCore/CFNotificationName+AltStore.h>

View File

@@ -77,6 +77,12 @@ public class Keychain
@KeychainItem(key: "patreonAccountID") @KeychainItem(key: "patreonAccountID")
public var patreonAccountID: String? public var patreonAccountID: String?
@KeychainItem(key: "identifier")
public var identifier: String?
@KeychainItem(key: "adiPb")
public var adiPb: String?
private init() private init()
{ {
} }

View File

@@ -0,0 +1,18 @@
//
// OperatingSystemVersion+Comparable.swift
// AltStoreCore
//
// Created by nythepegasus on 5/9/24.
//
import Foundation
extension OperatingSystemVersion: Comparable {
public static func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
return lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion
}
public static func <(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool {
return lhs.stringValue.compare(rhs.stringValue, options: .numeric) == .orderedAscending
}
}

View File

@@ -0,0 +1,14 @@
//
// String+SideStore.swift
// AltStoreCore
//
// Created by nythepegasus on 5/9/24.
//
import Foundation
public extension String {
init(formatted: String, comment: String? = nil, _ args: String...) {
self.init(format: NSLocalizedString(formatted, comment: comment ?? ""), args)
}
}

View File

@@ -22,11 +22,17 @@ public extension UserDefaults
@NSManaged var firstLaunch: Date? @NSManaged var firstLaunch: Date?
@NSManaged var requiresAppGroupMigration: Bool @NSManaged var requiresAppGroupMigration: Bool
@NSManaged var textServer: Bool @NSManaged var textServer: Bool
@NSManaged var sidejitenable: Bool
@NSManaged var textInputSideJITServerurl: String?
@NSManaged var textInputAnisetteURL: String? @NSManaged var textInputAnisetteURL: String?
@NSManaged var customAnisetteURL: String? @NSManaged var customAnisetteURL: String?
@NSManaged var menuAnisetteURL: String
@NSManaged var menuAnisetteList: String
@NSManaged var preferredServerID: String? @NSManaged var preferredServerID: String?
@NSManaged var isBackgroundRefreshEnabled: Bool @NSManaged var isBackgroundRefreshEnabled: Bool
@NSManaged var isIdleTimeoutDisableEnabled: Bool
@NSManaged var isPairingReset: Bool
@NSManaged var isDebugModeEnabled: Bool @NSManaged var isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool @NSManaged var presentedLaunchReminderNotification: Bool
@@ -42,6 +48,7 @@ public extension UserDefaults
@NSManaged var patronsRefreshID: String? @NSManaged var patronsRefreshID: String?
@NSManaged var trustedSourceIDs: [String]? @NSManaged var trustedSourceIDs: [String]?
@NSManaged var trustedServerURL: String?
var activeAppsLimit: Int? { var activeAppsLimit: Int? {
get { get {
@@ -71,11 +78,14 @@ public extension UserDefaults
let defaults = [ let defaults = [
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true, #keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,
#keyPath(UserDefaults.isPairingReset): true,
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported, #keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions, #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing, #keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
#keyPath(UserDefaults.requiresAppGroupMigration): true #keyPath(UserDefaults.requiresAppGroupMigration): true,
] #keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io"
] as [String : Any]
UserDefaults.standard.register(defaults: defaults) UserDefaults.standard.register(defaults: defaults)
UserDefaults.shared.register(defaults: defaults) UserDefaults.shared.register(defaults: defaults)

View File

@@ -40,7 +40,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
/* Relationships */ /* Relationships */
@NSManaged public private(set) var app: StoreApp? @NSManaged public private(set) var app: StoreApp?
@NSManaged public private(set) var latestVersionApp: StoreApp? @NSManaged @objc(latestVersionApp) public internal(set) var latestSupportedVersionApp: StoreApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
@@ -54,6 +54,8 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
case localizedDescription case localizedDescription
case downloadURL case downloadURL
case size case size
case minOSVersion
case maxOSVersion
} }
public required init(from decoder: Decoder) throws public required init(from decoder: Decoder) throws
@@ -72,6 +74,9 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL) self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
self.size = try container.decode(Int64.self, forKey: .size) self.size = try container.decode(Int64.self, forKey: .size)
self._minOSVersion = try container.decodeIfPresent(String.self, forKey: .minOSVersion)
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
} }
catch catch
{ {
@@ -113,4 +118,13 @@ public extension AppVersion
return appVersion return appVersion
} }
var isSupported: Bool {
if let minOSVersion = self.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
return false
} else if let maxOSVersion = self.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
return false
}
return true
}
} }

View File

@@ -13,11 +13,11 @@ import Roxas
extension CFNotificationName extension CFNotificationName
{ {
fileprivate static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString) fileprivate static let willMigrateDatabase = CFNotificationName("com.rileytestut.AltStore.WillMigrateDatabase" as CFString)
} }
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in private let ReceivedWillMigrateDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
DatabaseManager.shared.receivedWillAccessDatabaseNotification() DatabaseManager.shared.receivedWillMigrateDatabaseNotification()
} }
fileprivate class PersistentContainer: RSTPersistentContainer fileprivate class PersistentContainer: RSTPersistentContainer
@@ -52,15 +52,15 @@ public class DatabaseManager
private let coordinator = NSFileCoordinator() private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue() private let coordinatorQueue = OperationQueue()
private var ignoreWillAccessDatabaseNotification = false private var ignoreWillMigrateDatabaseNotification = false
private init() private init()
{ {
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self)) self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
self.persistentContainer.preferredMergePolicy = MergePolicy() self.persistentContainer.preferredMergePolicy = MergePolicy()
let observer = Unmanaged.passUnretained(self).toOpaque() let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately) CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillMigrateDatabaseNotification, CFNotificationName.willMigrateDatabase.rawValue, nil, .deliverImmediately)
} }
} }
@@ -87,10 +87,13 @@ public extension DatabaseManager
guard !self.isStarted else { return finish(nil) } guard !self.isStarted else { return finish(nil) }
// Quit any other running AltStore processes to prevent concurrent database access during and after migration. if self.persistentContainer.isMigrationRequired {
self.ignoreWillAccessDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true) // Quit any other running AltStore processes to prevent concurrent database access during and after migration.
self.ignoreWillMigrateDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willMigrateDatabase, nil, nil, true)
}
self.migrateDatabaseToAppGroupIfNeeded { (result) in self.migrateDatabaseToAppGroupIfNeeded { (result) in
switch result switch result
{ {
@@ -229,7 +232,7 @@ private extension DatabaseManager
else else
{ {
storeApp = StoreApp.makeAltStoreApp(in: context) storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.latestVersion?.version = localApp.version storeApp.latestSupportedVersion?.version = localApp.version
storeApp.source = altStoreSource storeApp.source = altStoreSource
} }
@@ -417,13 +420,13 @@ private extension DatabaseManager
} }
} }
func receivedWillAccessDatabaseNotification() func receivedWillMigrateDatabaseNotification()
{ {
defer { self.ignoreWillAccessDatabaseNotification = false } defer { self.ignoreWillMigrateDatabaseNotification = false }
// Ignore notifications sent by the current process. // Ignore notifications sent by the current process.
guard !self.ignoreWillAccessDatabaseNotification else { return } guard !self.ignoreWillMigrateDatabaseNotification else { return }
exit(104) exit(104)
} }
} }

View File

@@ -62,14 +62,14 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
@objc public var hasUpdate: Bool { @objc public var hasUpdate: Bool {
if self.storeApp == nil { return false } if self.storeApp == nil { return false }
if self.storeApp!.latestVersion == nil { return false } if self.storeApp!.latestSupportedVersion == nil { return false }
let currentVersion = SemanticVersion(self.version) let currentVersion = SemanticVersion(self.version)
let latestVersion = SemanticVersion(self.storeApp!.latestVersion!.version) let latestVersion = SemanticVersion(self.storeApp!.latestSupportedVersion!.version)
if currentVersion == nil || latestVersion == nil { if currentVersion == nil || latestVersion == nil {
// One of the versions is not valid SemVer, fall back to comparing the version strings by character // One of the versions is not valid SemVer, fall back to comparing the version strings by character
return self.version < self.storeApp!.latestVersion!.version return self.version < self.storeApp!.latestSupportedVersion!.version
} }
return currentVersion! < latestVersion! return currentVersion! < latestVersion!
@@ -163,8 +163,8 @@ public extension InstalledApp
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp> class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
{ {
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp> let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K == YES", fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.hasUpdate)) #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version))
return fetchRequest return fetchRequest
} }
@@ -192,7 +192,7 @@ public extension InstalledApp
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp] class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
{ {
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID) let predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))") print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated // if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
@@ -223,7 +223,7 @@ public extension InstalledApp
// Date 6 hours before now. // Date 6 hours before now.
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60) let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)", let predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.isActive),
#keyPath(InstalledApp.refreshedDate), date as NSDate, #keyPath(InstalledApp.refreshedDate), date as NSDate,
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID) #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
@@ -275,14 +275,12 @@ public extension InstalledApp
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) } do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
catch { print("Creating App Directory Error: \(error)") } catch { print("Creating App Directory Error: \(error)") }
print("`appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL return appsDirectoryURL
} }
class var legacyAppsDirectoryURL: URL { class var legacyAppsDirectoryURL: URL {
let baseDirectory = FileManager.default.applicationSupportDirectory let baseDirectory = FileManager.default.applicationSupportDirectory
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps") let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
print("legacy `appsDirectoryURL` is set to: \(appsDirectoryURL.absoluteString)")
return appsDirectoryURL return appsDirectoryURL
} }

View File

@@ -19,6 +19,8 @@ extension LoggedError
case deactivate case deactivate
case backup case backup
case restore case restore
case connection
case enableJIT
} }
} }
@@ -66,7 +68,12 @@ public class LoggedError: NSManagedObject, Fetchable
self.date = date self.date = date
self._operation = operation?.rawValue self._operation = operation?.rawValue
let nsError = error as NSError let nsError: NSError
if let error = error as? ALTServerError, error.code == .underlyingError, let underlyingError = error.underlyingError {
nsError = underlyingError as NSError
} else {
nsError = error as NSError
}
self.domain = nsError.domain self.domain = nsError.domain
self.code = Int32(nsError.code) self.code = Int32(nsError.code)
self.userInfo = nsError.userInfo self.userInfo = nsError.userInfo
@@ -91,7 +98,7 @@ public extension LoggedError
return app return app
} }
var error: Error { var error: NSError {
let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo) let nsError = NSError(domain: self.domain, code: Int(self.code), userInfo: self.userInfo)
return nsError return nsError
} }
@@ -113,6 +120,8 @@ public extension LoggedError
case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName) case .deactivate: return String(format: NSLocalizedString("Deactivate %@ Failed", comment: ""), self.appName)
case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName) case .backup: return String(format: NSLocalizedString("Backup %@ Failed", comment: ""), self.appName)
case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName) case .restore: return String(format: NSLocalizedString("Restore %@ Failed", comment: ""), self.appName)
case .connection: return String(format: NSLocalizedString("Connection during %@ Failed", comment: ""), self.appName)
case .enableJIT: return String(format: NSLocalizedString("Enabling JIT for %@ Failed", comment: ""), self.appName)
} }
} }
} }

View File

@@ -44,7 +44,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion } let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself. // Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }), if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestSupportedVersionApp?.latestSupportedVersion == $0 }),
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion }) let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
{ {
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion) secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)

View File

@@ -48,7 +48,7 @@ fileprivate extension NSManagedObject
func setStoreAppLatestVersion(_ appVersion: NSManagedObject) func setStoreAppLatestVersion(_ appVersion: NSManagedObject)
{ {
self.setValue(appVersion, forKey: #keyPath(StoreApp.latestVersion)) self.setValue(appVersion, forKey: #keyPath(StoreApp.latestSupportedVersion))
let versions = NSOrderedSet(array: [appVersion]) let versions = NSOrderedSet(array: [appVersion])
self.setValue(versions, forKey: #keyPath(StoreApp._versions)) self.setValue(versions, forKey: #keyPath(StoreApp._versions))

View File

@@ -146,7 +146,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged @objc(source) public var _source: Source? @NSManaged @objc(source) public var _source: Source?
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet @NSManaged @objc(permissions) public var _permissions: NSOrderedSet
@NSManaged public private(set) var latestVersion: AppVersion? @NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet @NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values. @NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
@@ -169,31 +169,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
return self._versions.array as! [AppVersion] return self._versions.array as! [AppVersion]
} }
@nonobjc public var size: Int64? {
guard let version = self.latestVersion else { return nil }
return version.size
}
@nonobjc public var version: String? {
guard let version = self.latestVersion else { return nil }
return version.version
}
@nonobjc public var versionDescription: String? {
guard let version = self.latestVersion else { return nil }
return version.localizedDescription
}
@nonobjc public var versionDate: Date? {
guard let version = self.latestVersion else { return nil }
return version.date
}
@nonobjc public var downloadURL: URL? {
guard let version = self.latestVersion else { return nil }
return version.downloadURL
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
@@ -314,16 +289,30 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
} }
} }
private extension StoreApp internal extension StoreApp
{ {
func setVersions(_ versions: [AppVersion]) func setVersions(_ versions: [AppVersion])
{ {
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self.latestVersion = latestVersion
self._versions = NSOrderedSet(array: versions) self._versions = NSOrderedSet(array: versions)
let latestSupportedVersion = versions.first(where: { $0.isSupported })
self.latestSupportedVersion = latestSupportedVersion
for case let version as AppVersion in self._versions
{
if version == latestSupportedVersion
{
version.latestSupportedVersionApp = self
}
else
{
// Ensure we replace any previous relationship when merging.
version.latestSupportedVersionApp = nil
}
}
// Preserve backwards compatibility by assigning legacy property values. // Preserve backwards compatibility by assigning legacy property values.
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self._version = latestVersion.version self._version = latestVersion.version
self._versionDate = latestVersion.date self._versionDate = latestVersion.date
self._versionDescription = latestVersion.localizedDescription self._versionDescription = latestVersion.localizedDescription
@@ -334,6 +323,10 @@ private extension StoreApp
public extension StoreApp public extension StoreApp
{ {
var latestAvailableVersion: AppVersion? {
return self._versions.firstObject as? AppVersion
}
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp> @nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
{ {
return NSFetchRequest<StoreApp>(entityName: "StoreApp") return NSFetchRequest<StoreApp>(entityName: "StoreApp")

View File

@@ -40,7 +40,7 @@ extension ALTApplication: AppProtocol
extension StoreApp: AppProtocol extension StoreApp: AppProtocol
{ {
public var url: URL? { public var url: URL? {
return self.downloadURL return self.latestAvailableVersion?.downloadURL
} }
} }
@@ -50,3 +50,17 @@ extension InstalledApp: AppProtocol
return self.fileURL return self.fileURL
} }
} }
extension AppVersion: AppProtocol {
public var name: String {
return self.app?.name ?? self.bundleIdentifier
}
public var bundleIdentifier: String {
return self.appBundleID
}
public var url: URL? {
return self.downloadURL
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

View File

@@ -5,12 +5,12 @@
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "Group 23_120.png", "filename" : "1024.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "Group 23_180.png", "filename" : "1024.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "group16Copy2.pdf", "filename" : "sidestore-logo.svg",
"idiom" : "universal" "idiom" : "universal"
} }
], ],

View File

@@ -0,0 +1,17 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<title>sidestore-logo-svg</title>
<style>
.s0 { fill: none }
.s1 { fill: #bebbb4 }
</style>
<g id="Layer">
<g id="Layer">
<path id="Main" class="s0" />
<g id="Main1">
<g id="Layer">
<path id="Layer" fill-rule="evenodd" class="s1" d="m86 50c0 2.3-1.9 4.2-4.2 4.2h-50.9c-2.3 0-4.2-1.9-4.2-4.2 0-2.3 1.9-4.2 4.2-4.2h50.9c2.3 0 4.2 1.9 4.2 4.2zm-31.8-23.3v2.1c0 3.4-1.3 6.6-3.7 9-2.4 2.4-5.6 3.7-9 3.7h-10.6c-2.2 0-4.4 0.9-5.9 2.5-1.6 1.6-2.5 3.8-2.5 6 0 2.2 0.9 4.4 2.5 6 1.5 1.6 3.7 2.5 5.9 2.5h10.6c3.4 0 6.6 1.3 9 3.7 2.4 2.4 3.7 5.6 3.7 9v10.8c0 1.1-0.4 2.1-1.1 2.8-0.8 0.8-1.8 1.2-2.9 1.2h-0.4c-1.1 0-2.1-0.4-2.9-1.2-0.7-0.7-1.1-1.7-1.1-2.8v-10.8c0-1.1-0.5-2.2-1.3-3-0.8-0.8-1.8-1.3-3-1.3h-10.6c-4.5 0-8.8-1.7-11.9-4.9-3.2-3.2-5-7.5-5-12 0-4.5 1.8-8.8 5-12 3.1-3.2 7.4-4.9 11.9-4.9h10.6c1.2 0 2.2-0.5 3-1.3 0.8-0.8 1.3-1.9 1.3-3v-2.1h-4.3l8.5-12.7 8.5 12.7zm12.7 55.3c0 2.2-1.8 4-4 4h-0.5c-2.2 0-4-1.8-4-4v-19.5c0-2.2 1.8-4 4-4h0.5c2.2 0 4 1.8 4 4z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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