Merge branch 'main' into releases-and-new-betas

This commit is contained in:
Jayson Rhynas 2025-08-25 18:21:53 -04:00 committed by GitHub
commit 8a5591c04a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 7243 additions and 1074 deletions

View file

@ -14,16 +14,16 @@ jobs:
# If you're using actions/checkout@v4 you must set persist-credentials to false in most cases for the deployment to work correctly.
persist-credentials: false
- name: Cache 📦
uses: actions/cache@v4.0.0
with:
path: AppCast/vendor/bundle
key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
restore-keys: |
${{ runner.os }}-gems-
# - name: Cache 📦
# uses: actions/cache@v4.1.1
# with:
# path: AppCast/vendor/bundle
# key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
# restore-keys: |
# ${{ runner.os }}-gems-
- name: Setup Ruby, JRuby and TruffleRuby
uses: ruby/setup-ruby@v1.169.0
uses: ruby/setup-ruby@v1.197.0
with:
ruby-version: '3.0'

View file

@ -8,10 +8,10 @@ on:
jobs:
test:
runs-on: macos-13
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Run tests
env:
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app
DEVELOPER_DIR: /Applications/Xcode_16.4.app
run: xcodebuild test -scheme Xcodes

View file

@ -11,6 +11,6 @@ jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

22
.github/workflows/xcstrings.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: XCStrings Validation
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- name: Clone SwiftPolyglot
run: git clone --branch 0.3.1 -- https://github.com/appdecostudio/SwiftPolyglot.git ../SwiftPolyglot
- name: validate translations
run: |
swift build --package-path ../SwiftPolyglot --configuration release
swift run --package-path ../SwiftPolyglot swiftpolyglot "ca,de,el,es,fi,fr,hi,it,ja,ko,nl,pl,pt-BR,ru,tr,uk,zh-Hans,zh-Hant" --errorOnMissing

View file

@ -8,7 +8,7 @@ source "https://rubygems.org"
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 3.9.0"
gem "jekyll", "~> 4.4.1"
gem "jekyll-github-metadata", group: :jekyll_plugins

View file

@ -1,88 +1,110 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
base64 (0.3.0)
bigdecimal (3.2.2)
colorator (1.1.0)
concurrent-ruby (1.1.7)
em-websocket (0.5.2)
concurrent-ruby (1.3.5)
csv (3.3.5)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
http_parser.rb (~> 0)
eventmachine (1.2.7)
faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
faraday-net_http (1.0.1)
ffi (1.14.2)
ffi (1.17.2)
ffi (1.17.2-x86_64-darwin)
forwardable-extended (2.6.0)
http_parser.rb (0.6.0)
i18n (0.9.5)
google-protobuf (4.31.1)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86_64-darwin)
bigdecimal
rake (>= 13)
http_parser.rb (0.8.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jekyll (3.9.0)
jekyll (4.4.1)
addressable (~> 2.4)
base64 (~> 0.2)
colorator (~> 1.0)
csv (~> 3.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
i18n (~> 1.0)
jekyll-sass-converter (>= 2.0, < 4.0)
jekyll-watch (~> 2.0)
kramdown (>= 1.17, < 3)
json (~> 2.6)
kramdown (~> 2.3, >= 2.3.1)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (~> 0.3.3)
mercenary (~> 0.3, >= 0.3.6)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0)
terminal-table (>= 1.8, < 4.0)
webrick (~> 1.7)
jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-sass-converter (3.1.0)
sass-embedded (~> 1.75)
jekyll-watch (2.2.1)
listen (~> 3.0)
kramdown (2.3.1)
rexml
json (2.12.2)
kramdown (2.5.1)
rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.4.1)
liquid (4.0.4)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
mercenary (0.4.0)
multipart-post (2.1.1)
octokit (4.20.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (4.0.6)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
public_suffix (6.0.2)
rake (13.3.0)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.26.0)
rexml (3.4.1)
rouge (4.5.2)
ruby2_keywords (0.0.2)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-embedded (1.89.2)
google-protobuf (~> 4.31)
rake (>= 13)
sass-embedded (1.89.2-x86_64-darwin)
google-protobuf (~> 4.31)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
thread_safe (0.3.6)
tzinfo (1.2.10)
thread_safe (~> 0.1)
tzinfo-data (1.2020.6)
tzinfo (>= 1.0.0)
unicode-display_width (2.6.0)
wdm (0.1.1)
webrick (1.9.1)
PLATFORMS
ruby
x86_64-darwin-20
DEPENDENCIES
jekyll (~> 3.9.0)
jekyll (~> 4.4.1)
jekyll-github-metadata
kramdown-parser-gfm
tzinfo (~> 1.2)
@ -90,4 +112,4 @@ DEPENDENCIES
wdm (~> 0.1.0)
BUNDLED WITH
2.2.5
2.6.9

View file

@ -7,8 +7,8 @@ We love your input! We want to make contributing to this project as easy and tra
- Proposing new features
- Becoming a maintainer
## We Develop with Github
We use github to host code, to track issues and feature requests, as well as accept pull requests.
## We Develop with GitHub
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
## All Code Changes Happen Through Pull Requests
Pull requests are the best way to propose changes to the codebase We actively welcome your pull requests:
@ -23,7 +23,7 @@ Pull requests are the best way to propose changes to the codebase We actively w
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](https://github.com/robotsandpencils/xcodesapp/issues)
## Report bugs using GitHub [issues](https://github.com/robotsandpencils/xcodesapp/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
## Write bug reports with detail, background, and sample code

View file

@ -22,11 +22,14 @@ XcodesApp is now part of the `XcodesOrg` - [read more here](nextstep.md)
- Just click a button to make a version active with `xcode-select`.
- View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com).
- Dark/Light Mode supported
- Security Key Authentication supported
## Platforms/Runtimes
- Xcodes supports downloading the Apple runtimes via the app. Simply click on the Platform, and Xcodes will install automatically for you.
**Note: iOS 18+, tvOS 18+, watchOS 11+, visionOS 2+ requires that Xcode 16.1 Beta 3+ be installed and active.**
## Experiments
- Thanks to the wonderful work of [https://github.com/saagarjha/unxip](https://github.com/saagarjha/unxip), turn on the experiment to increase your unxipping time by up to 70%! More can be found on his repo, but bugs, high memory may occur if used.
@ -50,13 +53,13 @@ The following languages are supported because of the following community users!
|Ukranian 🇺🇦 |[@gelosi](https://github.com/gelosi)|Japanese 🇯🇵|[@tatsuz0u](https://github.com/tatsuz0u)|
|German 🇩🇪|[@drct](https://github.com/drct)|Dutch 🇳🇱|[@jfversluis](https://github/com/jfversluis)|
|Brazilian Portuguese 🇧🇷|[@brunomunizaf](https://github.com/brunomunizaf)|Polish 🇵🇱|[@jakex7](https://github.com/jakex7)|
|Catalan|[@ferranabello](https://github.com/ferranabello)|
|Catalan|[@ferranabello](https://github.com/ferranabello)|Greek 🇬🇷|[@alladinian](https://github.com/alladinian)
Want to add more languages? Simply create a PR with the updated strings file.
## Installation
v1.X - requires MacOS 11 or newer
v2.X - requires MacOS 13
v1.X - requires macOS 11 or newer
v2.X - requires macOS 13
### Install with Homebrew
@ -160,7 +163,8 @@ popd
# Attach the zip that was created in the Product directory to the release
# Publish the release
# Update the [Homebrew Cask](https://github.com/RobotsAndPencils/homebrew-cask/blob/master/Casks/xcodes.rb).
shasum -a 256 xcodes.zip
# Update the [Homebrew Cask](https://github.com/XcodesOrg/homebrew-cask/blob/master/Casks/x/xcodes.rb).
```
</details>

View file

@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; };
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; };
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
@ -25,6 +29,7 @@
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */; };
B0C6AD0B2AD9178E00E64698 /* IdenticalBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */; };
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD0C2AD91D7900E64698 /* IconView.swift */; };
BDBAB7452B9FF55800694B0B /* TrailingIconLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAB7442B9FF55800694B0B /* TrailingIconLabelStyle.swift */; };
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; };
CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; };
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25192925A9644800F08414 /* XcodeInstallState.swift */; };
@ -110,17 +115,19 @@
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4AB325B7D3AF0064FE51 /* AdvancedPreferencePane.swift */; };
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
D93F95C12E0C8C1A00238FB5 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93F95C02E0C8C1A00238FB5 /* TagView.swift */; };
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; };
E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; };
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; };
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E83FDC432CBB649100679C6B /* Sparkle */; };
E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; };
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; };
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */ = {isa = PBXBuildFile; productRef = E862D43A2CC8B26F00BAA376 /* SRP */; };
E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; };
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E891A1C32B43ACF900A1B9D1 /* Sparkle */; };
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */; };
@ -133,6 +140,7 @@
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
E8EE58C02E1CC2A50003FA9F /* RuntimeArchitecture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */; };
E8F44A1E296B4CD7002D6592 /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = E8F44A1D296B4CD7002D6592 /* Path */; };
E8FA00542B5B109800769CE0 /* com.xcodesorg.xcodesapp.Helper in Copy Helper */ = {isa = PBXBuildFile; fileRef = CA9FF8AE2595967A00E47BAF /* com.xcodesorg.xcodesapp.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */ = {isa = PBXBuildFile; productRef = E8FD5726291EE4AC001E004C /* AsyncNetworkService */; };
@ -191,6 +199,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = "<group>"; };
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = "<group>"; };
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
@ -209,6 +220,7 @@
B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseDateView.swift; sourceTree = "<group>"; };
B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdenticalBuildView.swift; sourceTree = "<group>"; };
B0C6AD0C2AD91D7900E64698 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
BDBAB7442B9FF55800694B0B /* TrailingIconLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingIconLabelStyle.swift; sourceTree = "<group>"; };
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = "<group>"; };
CA25192925A9644800F08414 /* XcodeInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeInstallState.swift; sourceTree = "<group>"; };
@ -311,6 +323,7 @@
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesPreferencePane.swift; sourceTree = "<group>"; };
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
D93F95C02E0C8C1A00238FB5 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = "<group>"; };
E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = "<group>"; };
E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitViewWrapper.swift; sourceTree = "<group>"; };
@ -330,6 +343,7 @@
E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = "<group>"; };
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeArchitecture.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -344,12 +358,15 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */,
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */,
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */,
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */,
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */,
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
@ -374,12 +391,14 @@
63EAA4E9259944340046AB8F /* Common */ = {
isa = PBXGroup;
children = (
D93F95C02E0C8C1A00238FB5 /* TagView.swift */,
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */,
53CBAB2B263DCC9100410495 /* XcodesAlert.swift */,
E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */,
BDBAB7442B9FF55800694B0B /* TrailingIconLabelStyle.swift */,
);
path = Common;
sourceTree = "<group>";
@ -405,6 +424,7 @@
CA538A12255A4F7C00E64DD7 /* Frameworks */ = {
isa = PBXGroup;
children = (
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -451,6 +471,8 @@
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */,
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */,
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
@ -640,6 +662,7 @@
E8E98A9425D863B100EC89A0 /* InfoPane */ = {
isa = PBXGroup;
children = (
E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */,
B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */,
B0403CF32AD9381D00137C09 /* SDKsView.swift */,
B0403CF52AD9849E00137C09 /* CompilersView.swift */,
@ -710,7 +733,9 @@
E8C0EB19291EF43E0081528A /* XcodesKit */,
E8F44A1D296B4CD7002D6592 /* Path */,
E84E4F562B335094003F3959 /* OrderedCollections */,
E891A1C32B43ACF900A1B9D1 /* Sparkle */,
E83FDC432CBB649100679C6B /* Sparkle */,
334A932B2CA885A400A5E079 /* LibFido2Swift */,
E862D43A2CC8B26F00BAA376 /* SRP */,
);
productName = XcodesMac;
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
@ -783,6 +808,7 @@
"pt-BR",
nl,
pl,
ar,
);
mainGroup = CAD2E7952449574E00113D76;
packageReferences = (
@ -797,7 +823,8 @@
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */,
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */,
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
);
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
projectDirPath = "";
@ -885,6 +912,7 @@
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */,
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
@ -910,8 +938,11 @@
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */,
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
D93F95C12E0C8C1A00238FB5 /* TagView.swift in Sources */,
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */,
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
E8EE58C02E1CC2A50003FA9F /* RuntimeArchitecture.swift in Sources */,
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */,
@ -919,6 +950,7 @@
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */,
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
BDBAB7452B9FF55800694B0B /* TrailingIconLabelStyle.swift in Sources */,
E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */,
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */,
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */,
@ -1054,7 +1086,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1074,7 +1106,7 @@
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
@ -1086,7 +1118,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.0.2;
MARKETING_VERSION = 2.4.2;
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
PRODUCT_NAME = Xcodes;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1249,7 +1281,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1307,7 +1339,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
@ -1326,7 +1358,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = ZU6GR6B2FY;
ENABLE_HARDENED_RUNTIME = YES;
@ -1338,7 +1370,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.0.2;
MARKETING_VERSION = 2.4.2;
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
PRODUCT_NAME = Xcodes;
SWIFT_VERSION = 5.0;
@ -1354,7 +1386,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = ZU6GR6B2FY;
ENABLE_HARDENED_RUNTIME = YES;
@ -1366,7 +1398,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.0.2;
MARKETING_VERSION = 2.4.2;
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
PRODUCT_NAME = Xcodes;
SWIFT_VERSION = 5.0;
@ -1464,6 +1496,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kinoroy/LibFido2Swift.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.1.4;
};
};
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/xcodereleases/data";
@ -1525,7 +1565,15 @@
repositoryURL = "https://github.com/sindresorhus/DockProgress";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 3.2.0;
minimumVersion = 4.3.1;
};
};
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.6.4;
};
};
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
@ -1536,14 +1584,6 @@
minimumVersion = 1.0.5;
};
};
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.5.2;
};
};
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mxcl/Path.swift";
@ -1563,6 +1603,10 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
334A932B2CA885A400A5E079 /* LibFido2Swift */ = {
isa = XCSwiftPackageProductDependency;
productName = LibFido2Swift;
};
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
isa = XCSwiftPackageProductDependency;
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
@ -1607,15 +1651,19 @@
package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */;
productName = DockProgress;
};
E83FDC432CBB649100679C6B /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
E84E4F562B335094003F3959 /* OrderedCollections */ = {
isa = XCSwiftPackageProductDependency;
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
productName = OrderedCollections;
};
E891A1C32B43ACF900A1B9D1 /* Sparkle */ = {
E862D43A2CC8B26F00BAA376 /* SRP */ = {
isa = XCSwiftPackageProductDependency;
package = E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
productName = SRP;
};
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
isa = XCSwiftPackageProductDependency;

View file

@ -10,6 +10,15 @@
"version": null
}
},
{
"package": "big-num",
"repositoryURL": "https://github.com/adam-fowler/big-num",
"state": {
"branch": null,
"revision": "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7",
"version": "2.0.2"
}
},
{
"package": "CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations",
@ -33,8 +42,8 @@
"repositoryURL": "https://github.com/sindresorhus/DockProgress",
"state": {
"branch": null,
"revision": "7100b68571e2dafe3a06ad5905b80fc3b0107b4b",
"version": "3.2.0"
"revision": "d4f23b5a8f5ca0fac393eb7ba78c2fe3e32e52da",
"version": "4.3.1"
}
},
{
@ -64,6 +73,15 @@
"version": "1.0.4"
}
},
{
"package": "LibFido2Swift",
"repositoryURL": "https://github.com/kinoroy/LibFido2Swift.git",
"state": {
"branch": null,
"revision": "94d496d6f850dcbb3e8c4a27cd7eeabfad9f14e3",
"version": "0.1.4"
}
},
{
"package": "Path.swift",
"repositoryURL": "https://github.com/mxcl/Path.swift",
@ -78,8 +96,8 @@
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
"state": {
"branch": null,
"revision": "47d3d90aee3c52b6f61d04ceae426e607df62347",
"version": "2.5.2"
"revision": "0ef1ee0220239b3776f433314515fd849025673f",
"version": "2.6.4"
}
},
{
@ -91,6 +109,24 @@
"version": "1.0.5"
}
},
{
"package": "swift-crypto",
"repositoryURL": "https://github.com/apple/swift-crypto",
"state": {
"branch": null,
"revision": "ddb07e896a2a8af79512543b1c7eb9797f8898a5",
"version": "1.1.7"
}
},
{
"package": "swift-srp",
"repositoryURL": "https://github.com/xcodesOrg/swift-srp",
"state": {
"branch": "main",
"revision": "543aa0122a0257b992f6c7d62d18a26e3dffb8fe",
"version": null
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup",

View file

@ -1,24 +1,26 @@
// swift-tools-version:5.3
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "AppleAPI",
platforms: [.macOS(.v10_15)],
platforms: [.macOS(.v11)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AppleAPI",
targets: ["AppleAPI"]),
],
dependencies: [],
dependencies: [
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "AppleAPI",
dependencies: []),
dependencies: [.product(name: "SRP", package: "swift-srp")]),
.testTarget(
name: "AppleAPITests",
dependencies: ["AppleAPI"]),

View file

@ -1,5 +1,9 @@
import Foundation
import Combine
import SRP
import Crypto
import CommonCrypto
public class Client {
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
@ -8,8 +12,12 @@ public class Client {
// MARK: - Login
public func login(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
public func srpLogin(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
var serviceKey: String!
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
let clientKeys = client.generateKeys()
let a = clientKeys.public
return Current.network.dataTask(with: URLRequest.itcServiceKey)
.map(\.data)
@ -24,11 +32,45 @@ public class Client {
.map { return (serviceKey, $0)}
.eraseToAnyPublisher()
}
.flatMap { (serviceKey, hashcash) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
.flatMap { (serviceKey, hashcash) -> AnyPublisher<(String, String, ServerSRPInitResponse), Swift.Error> in
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName))
.map(\.data)
.decode(type: ServerSRPInitResponse.self, decoder: JSONDecoder())
.map { return (serviceKey, hashcash, $0) }
.eraseToAnyPublisher()
}
.flatMap { (serviceKey, hashcash, srpInit) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
guard let decodedB = Data(base64Encoded: srpInit.b) else {
return Fail(error: AuthenticationError.srpInvalidPublicKey)
.eraseToAnyPublisher()
}
guard let decodedSalt = Data(base64Encoded: srpInit.salt) else {
return Fail(error: AuthenticationError.srpInvalidPublicKey)
.eraseToAnyPublisher()
}
let iterations = srpInit.iteration
do {
guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations, protocol: srpInit.protocol) else {
return Fail(error: AuthenticationError.srpInvalidPublicKey)
.eraseToAnyPublisher()
}
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password, hashcash: hashcash))
let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
.mapError { $0 as Swift.Error }
.eraseToAnyPublisher()
} catch {
return Fail(error: AuthenticationError.srpInvalidPublicKey)
.eraseToAnyPublisher()
}
}
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
let (data, response) = result
@ -118,7 +160,7 @@ public class Client {
case .twoStep:
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
.eraseToAnyPublisher()
case .twoFactor:
case .twoFactor, .securityKey:
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
.eraseToAnyPublisher()
case .unknown:
@ -139,7 +181,10 @@ public class Client {
// SMS wasn't sent automatically because user needs to choose a phone to send to
} else if authOptions.canFallBackToSMS {
option = .smsPendingChoice
// Code is shown on trusted devices
// Code is shown on trusted devices
} else if authOptions.fsaChallenge != nil {
option = .securityKey
// User needs to use a physical security key to respond to the challenge
} else {
option = .codeSent
}
@ -193,6 +238,33 @@ public class Client {
.eraseToAnyPublisher()
}
public func submitChallenge(response: Data, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
Result {
URLRequest.respondToChallenge(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, response: response)
}
.publisher
.flatMap { request in
Current.network.dataTask(with: request)
.mapError { $0 as Error }
.tryMap { (data, response) throws -> (Data, URLResponse) in
guard let urlResponse = response as? HTTPURLResponse else { return (data, response) }
switch urlResponse.statusCode {
case 200..<300:
return (data, urlResponse)
case 400, 401:
throw AuthenticationError.incorrectSecurityCode
case 412:
throw AuthenticationError.appleIDAndPrivacyAcknowledgementRequired
case let code:
throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse)
}
}
.flatMap { (data, response) -> AnyPublisher<AuthenticationState, Error> in
self.updateSession(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
}
}.eraseToAnyPublisher()
}
// MARK: - Session
/// Use the olympus session endpoint to see if the existing session is still valid
@ -227,6 +299,49 @@ public class Client {
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
func sha256(data : Data) -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
return Data(hash)
}
private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int, protocol srpProtocol: SRPProtocol) -> Data? {
guard let passwordData = password.data(using: .utf8) else { return nil }
let hashedPasswordDataRaw = sha256(data: passwordData)
let hashedPasswordData = switch srpProtocol {
case .s2k: hashedPasswordDataRaw
// the legacy s2k_fo protocol requires hex-encoding the digest before performing PBKDF2.
case .s2k_fo: Data(hashedPasswordDataRaw.hexEncodedString().lowercased().utf8)
}
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
let derivedCount = derivedKeyData.count
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
let keyBuffer: UnsafeMutablePointer<UInt8> =
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return saltData.withUnsafeBytes { saltBytes -> Int32 in
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in
let passwordBuffer: UnsafePointer<UInt8> = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
passwordBuffer,
hashedPasswordData.count,
saltBuffer,
saltData.count,
prf,
UInt32(rounds),
keyBuffer,
derivedCount)
}
}
}
return derivationStatus == kCCSuccess ? derivedKeyData : nil
}
}
// MARK: - Types
@ -252,6 +367,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
case notDeveloperAppleId
case notAuthorized
case invalidResult(resultString: String?)
case srpInvalidPublicKey
public var errorDescription: String? {
switch self {
@ -286,6 +402,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
return "You are not authorized. Please Sign in with your Apple ID first."
case let .invalidResult(resultString):
return resultString ?? "If you continue to have problems, please submit a bug report in the Help menu."
case .srpInvalidPublicKey:
return "Invalid Key"
}
}
}
@ -326,27 +444,37 @@ public enum TwoFactorOption: Equatable {
case smsSent(AuthOptionsResponse.TrustedPhoneNumber)
case codeSent
case smsPendingChoice
case securityKey
}
public struct FSAChallenge: Equatable, Decodable {
public let challenge: String
public let keyHandles: [String]
public let allowedCredentials: String
}
public struct AuthOptionsResponse: Equatable, Decodable {
public let trustedPhoneNumbers: [TrustedPhoneNumber]?
public let trustedDevices: [TrustedDevice]?
public let securityCode: SecurityCodeInfo
public let securityCode: SecurityCodeInfo?
public let noTrustedDevices: Bool?
public let serviceErrors: [ServiceError]?
public let fsaChallenge: FSAChallenge?
public init(
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
securityCode: AuthOptionsResponse.SecurityCodeInfo,
noTrustedDevices: Bool? = nil,
serviceErrors: [ServiceError]? = nil
serviceErrors: [ServiceError]? = nil,
fsaChallenge: FSAChallenge? = nil
) {
self.trustedPhoneNumbers = trustedPhoneNumbers
self.trustedDevices = trustedDevices
self.securityCode = securityCode
self.noTrustedDevices = noTrustedDevices
self.serviceErrors = serviceErrors
self.fsaChallenge = fsaChallenge
}
public var kind: Kind {
@ -354,6 +482,8 @@ public struct AuthOptionsResponse: Equatable, Decodable {
return .twoStep
} else if trustedPhoneNumbers != nil {
return .twoFactor
} else if fsaChallenge != nil {
return .securityKey
} else {
return .unknown
}
@ -416,7 +546,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
}
public enum Kind: Equatable {
case twoStep, twoFactor, unknown
case twoStep, twoFactor, securityKey, unknown
}
}
@ -453,3 +583,24 @@ public struct AppleProvider: Decodable, Equatable {
public struct AppleUser: Decodable, Equatable {
public let fullName: String
}
public struct ServerSRPInitResponse: Decodable {
let iteration: Int
let salt: String
let b: String
let c: String
let `protocol`: SRPProtocol
}
extension String {
func base64ToU8Array() -> Data {
return Data(base64Encoded: self) ?? Data()
}
}
extension Data {
func hexEncodedString() -> String {
return map { String(format: "%02hhx", $0) }.joined()
}
}

View file

@ -9,6 +9,11 @@ public extension URL {
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
static let keyAuth = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/security/key")!
static let srpInit = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/init")!
static let srpComplete = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false")!
}
public extension URLRequest {
@ -105,6 +110,19 @@ public extension URLRequest {
}
return request
}
static func respondToChallenge(serviceKey: String, sessionID: String, scnt: String, response: Data) -> URLRequest {
var request = URLRequest(url: .keyAuth)
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
request.allHTTPHeaderFields?["scnt"] = scnt
request.allHTTPHeaderFields?["Accept"] = "application/json"
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
request.httpMethod = "POST"
request.httpBody = response
return request
}
static func trust(serviceKey: String, sessionID: String, scnt: String) -> URLRequest {
var request = URLRequest(url: .trust)
@ -136,4 +154,51 @@ public extension URLRequest {
return request
}
static func SRPInit(serviceKey: String, a: String, accountName: String) -> URLRequest {
struct ServerSRPInitRequest: Encodable {
public let a: String
public let accountName: String
public let protocols: [SRPProtocol]
}
var request = URLRequest(url: .srpInit)
request.httpMethod = "POST"
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["Accept"] = "application/json"
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
request.httpBody = try? JSONEncoder().encode(ServerSRPInitRequest(a: a, accountName: accountName, protocols: [.s2k, .s2k_fo]))
return request
}
static func SRPComplete(serviceKey: String, hashcash: String, accountName: String, c: String, m1: String, m2: String) -> URLRequest {
struct ServerSRPCompleteRequest: Encodable {
let accountName: String
let c: String
let m1: String
let m2: String
let rememberMe: Bool
}
var request = URLRequest(url: .srpComplete)
request.httpMethod = "POST"
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["Accept"] = "application/json"
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash
request.httpBody = try? JSONEncoder().encode(ServerSRPCompleteRequest(accountName: accountName, c: c, m1: m1, m2: m2, rememberMe: false))
return request
}
}
public enum SRPProtocol: String, Codable {
case s2k, s2k_fo
}

View file

@ -13,8 +13,8 @@ extension AppState {
// check to see if we should auto install for the user
public func autoInstallIfNeeded() {
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
if autoInstallType == .none { return }
// get newest xcode version
@ -227,6 +227,7 @@ extension AppState {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
}
resetDockProgressTracking()
})
.catch { _ in
Just(installedXcode)
@ -473,19 +474,24 @@ extension AppState {
// MARK: - Dock Progress Tracking
private func setupDockProgress() {
DockProgress.progressInstance = nil
DockProgress.style = .bar
Task { @MainActor in
DockProgress.progressInstance = nil
DockProgress.style = .bar
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
progress.kind = .file
progress.fileOperationKind = .downloading
overallProgress = progress
DockProgress.progressInstance = overallProgress
}
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
progress.kind = .file
progress.fileOperationKind = .downloading
overallProgress = progress
DockProgress.progressInstance = overallProgress
}
func resetDockProgressTracking() {
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
Task { @MainActor in
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
}
}
// MARK: -

View file

@ -4,6 +4,7 @@ import OSLog
import Combine
import Path
import AppleAPI
import Version
extension AppState {
func updateDownloadableRuntimes() {
@ -15,10 +16,10 @@ extension AppState {
var updatedRuntime = runtime
// This loops through and matches up the simulatorVersion to the mappings
let simulatorBuildUpdate = downloadableRuntimes.sdkToSimulatorMappings.first { SDKToSimulatorMapping in
let simulatorBuildUpdate = downloadableRuntimes.sdkToSimulatorMappings.filter { SDKToSimulatorMapping in
SDKToSimulatorMapping.simulatorBuildUpdate == runtime.simulatorVersion.buildUpdate
}
updatedRuntime.sdkBuildUpdate = simulatorBuildUpdate?.sdkBuildUpdate
updatedRuntime.sdkBuildUpdate = simulatorBuildUpdate.map { $0.sdkBuildUpdate }
return updatedRuntime
}
@ -48,6 +49,73 @@ extension AppState {
}
func downloadRuntime(runtime: DownloadableRuntime) {
guard let selectedXcode = self.allXcodes.first(where: { $0.selected }) else {
Logger.appState.error("No selected Xcode")
DispatchQueue.main.async {
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: "No selected Xcode. Please make an Xcode active")
}
return
}
// new runtimes
if runtime.contentType == .cryptexDiskImage {
// only selected xcodes > 16.1 beta 3 can download runtimes via a xcodebuild -downloadPlatform version
// only Runtimes coming from cryptexDiskImage can be downloaded via xcodebuild
if selectedXcode.version > Version(major: 16, minor: 0, patch: 0) {
downloadRuntimeViaXcodeBuild(runtime: runtime)
} else {
// not supported
Logger.appState.error("Trying to download a runtime we can't download")
DispatchQueue.main.async {
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: localizeString("Alert.Install.Error.Need.Xcode16.1"))
}
return
}
} else {
downloadRuntimeObseleteWay(runtime: runtime)
}
}
func downloadRuntimeViaXcodeBuild(runtime: DownloadableRuntime) {
let downloadRuntimeTask = Current.shell.downloadRuntime(runtime.platform.shortName, runtime.simulatorVersion.buildUpdate)
runtimePublishers[runtime.identifier] = Task { [weak self] in
guard let self = self else { return }
do {
for try await progress in downloadRuntimeTask {
if progress.isIndeterminate {
DispatchQueue.main.async {
self.setInstallationStep(of: runtime, to: .installing, postNotification: false)
}
} else {
DispatchQueue.main.async {
self.setInstallationStep(of: runtime, to: .downloading(progress: progress), postNotification: false)
}
}
}
Logger.appState.debug("Done downloading runtime - \(runtime.name)")
DispatchQueue.main.async {
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
self.downloadableRuntimes[index].installState = .installed
self.update()
}
} catch {
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
DispatchQueue.main.async {
self.error = error
if let error = error as? String {
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error)
} else {
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
}
}
}
}
func downloadRuntimeObseleteWay(runtime: DownloadableRuntime) {
runtimePublishers[runtime.identifier] = Task {
do {
let downloadedURL = try await downloadRunTimeFull(runtime: runtime)
@ -57,6 +125,9 @@ extension AppState {
self.setInstallationStep(of: runtime, to: .installing)
}
switch runtime.contentType {
case .cryptexDiskImage:
// not supported yet (do we need to for old packages?)
throw "Installing via cryptexDiskImage not support - please install manually from \(downloadedURL.description)"
case .package:
// not supported yet (do we need to for old packages?)
throw "Installing via package not support - please install manually from \(downloadedURL.description)"
@ -80,19 +151,31 @@ extension AppState {
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
DispatchQueue.main.async {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
if let error = error as? String {
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error)
} else {
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
}
}
}
}
func downloadRunTimeFull(runtime: DownloadableRuntime) async throws -> URL {
guard let source = runtime.source else {
throw "Invalid runtime source"
}
guard let downloadPath = runtime.downloadPath else {
throw "Invalid runtime downloadPath"
}
// sets a proper cookie for runtimes
try await validateADCSession(path: runtime.downloadPath)
try await validateADCSession(path: downloadPath)
let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2
let downloader = Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2
let url = URL(string: runtime.source)!
let url = URL(string: source)!
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
// aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
@ -123,9 +206,15 @@ extension AppState {
}
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path) -> AsyncThrowingStream<Progress, Error> {
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: runtime.url) ?? []
guard let url = runtime.url else {
return AsyncThrowingStream<Progress, Error> { continuation in
continuation.finish(throwing: "Invalid or non existant runtime url")
}
}
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: url) ?? []
return Current.shell.downloadWithAria2Async(aria2Path, runtime.url, destination, cookies)
return Current.shell.downloadWithAria2Async(aria2Path, url, destination, cookies)
}
@ -140,7 +229,10 @@ extension AppState {
runtimePublishers[runtime.identifier] = nil
// If the download is cancelled by the user, clean up the download files that aria2 creates.
let url = URL(string: runtime.source)!
guard let source = runtime.source else {
return
}
let url = URL(string: source)!
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")

View file

@ -9,6 +9,26 @@ import Version
import os.log
import DockProgress
import XcodesKit
import LibFido2Swift
enum PreferenceKey: String {
case installPath
case localPath
case unxipExperiment
case createSymLinkOnSelect
case onSelectActionType
case showOpenInRosettaOption
case autoInstallation
case SUEnableAutomaticChecks
case includePrereleaseVersions
case downloader
case dataSource
case xcodeListCategory
case allowedMajorVersions
case hideSupportXcodes
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
}
class AppState: ObservableObject {
private let client = AppleAPI.Client()
@ -66,18 +86,24 @@ class AppState: ObservableObject {
}
}
var disableLocalPathChange: Bool { PreferenceKey.localPath.isManaged() }
@Published var installPath = "" {
didSet {
Current.defaults.set(installPath, forKey: "installPath")
}
}
var disableInstallPathChange: Bool { PreferenceKey.installPath.isManaged() }
@Published var unxipExperiment = false {
didSet {
Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
}
}
var disableUnxipExperiment: Bool { PreferenceKey.unxipExperiment.isManaged() }
@Published var createSymLinkOnSelect = false {
didSet {
Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect")
@ -85,7 +111,7 @@ class AppState: ObservableObject {
}
var createSymLinkOnSelectDisabled: Bool {
return onSelectActionType == .rename
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
}
@Published var onSelectActionType = SelectedActionType.none {
@ -98,12 +124,20 @@ class AppState: ObservableObject {
}
}
var onSelectActionTypeDisabled: Bool { PreferenceKey.onSelectActionType.isManaged() }
@Published var showOpenInRosettaOption = false {
didSet {
Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
}
}
@Published var terminateAfterLastWindowClosed = false {
didSet {
Current.defaults.set(terminateAfterLastWindowClosed, forKey: "terminateAfterLastWindowClosed")
}
}
// MARK: - Runtimes
@Published var downloadableRuntimes: [DownloadableRuntime] = []
@ -173,13 +207,14 @@ class AppState: ObservableObject {
onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none
installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string
showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false
terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
}
// MARK: Timer
/// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes
func setupAutoInstallTimer() {
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
if autoInstallType == .none { return }
autoInstallTimer = Timer.scheduledTimer(withTimeInterval: 60*60*6, repeats: true) { [weak self] _ in
@ -242,7 +277,7 @@ class AppState: ObservableObject {
func signIn(username: String, password: String) {
authError = nil
signIn(username: username, password: password)
signIn(username: username.lowercased(), password: password)
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in }
@ -255,7 +290,7 @@ class AppState: ObservableObject {
Current.defaults.set(username, forKey: "username")
isProcessingAuthRequest = true
return client.login(accountName: username, password: password)
return client.srpLogin(accountName: username, password: password)
.receive(on: DispatchQueue.main)
.handleEvents(
receiveOutput: { authenticationState in
@ -270,11 +305,17 @@ class AppState: ObservableObject {
}
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
self.presentedSheet = .twoFactor(.init(
option: option,
authOptions: authOptions,
sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
))
let sessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
if option == .securityKey, fido2DeviceIsPresent() && !fido2DeviceNeedsPin() {
createAndSubmitSecurityKeyAssertationWithPinCode(nil, sessionData: sessionData, authOptions: authOptions)
} else {
self.presentedSheet = .twoFactor(.init(
option: option,
authOptions: authOptions,
sessionData: sessionData
))
}
}
func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
@ -320,6 +361,83 @@ class AppState: ObservableObject {
.store(in: &cancellables)
}
private lazy var fido2 = FIDO2()
func createAndSubmitSecurityKeyAssertationWithPinCode(_ pinCode: String?, sessionData: AppleSessionData, authOptions: AuthOptionsResponse) {
self.presentedSheet = .securityKeyTouchToConfirm
guard let fsaChallenge = authOptions.fsaChallenge else {
// This shouldn't happen
// we shouldn't have called this method without setting the fsaChallenge
// so this is an assertionFailure
assertionFailure()
self.authError = "Something went wrong. Please file a bug report"
return
}
// The challenge is encoded in Base64URL encoding
let challengeUrl = fsaChallenge.challenge
let challenge = FIDO2.base64urlToBase64(base64url: challengeUrl)
let origin = "https://idmsa.apple.com"
let rpId = "apple.com"
// Allowed creds is sent as a comma separated string
let validCreds = fsaChallenge.allowedCredentials.split(separator: ",").map(String.init)
Task {
do {
let response = try fido2.respondToChallenge(args: ChallengeArgs(rpId: rpId, validCredentials: validCreds, devPin: pinCode, challenge: challenge, origin: origin))
Task { @MainActor in
self.isProcessingAuthRequest = true
}
let respData = try JSONEncoder().encode(response)
client.submitChallenge(response: respData, sessionData: AppleSessionData(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt))
.receive(on: DispatchQueue.main)
.handleEvents(
receiveOutput: { authenticationState in
self.authenticationState = authenticationState
},
receiveCompletion: { completion in
self.handleAuthenticationFlowCompletion(completion)
self.isProcessingAuthRequest = false
}
).sink(
receiveCompletion: { _ in },
receiveValue: { _ in }
).store(in: &cancellables)
} catch FIDO2Error.canceledByUser {
// User cancelled the auth flow
// we don't have to show an error
// because the sheet will already be dismissed
} catch {
Task { @MainActor in
authError = error
}
}
}
}
func fido2DeviceIsPresent() -> Bool {
fido2.hasDeviceAttached()
}
func fido2DeviceNeedsPin() -> Bool {
do {
return try fido2.deviceHasPin()
} catch {
Task { @MainActor in
authError = error
}
return true
}
}
func cancelSecurityKeyAssertationRequest() {
self.fido2.cancel()
}
private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion<Error>) {
switch completion {
case let .failure(error):
@ -408,7 +526,7 @@ class AppState: ObservableObject {
func checkMinVersionAndInstall(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
// Check to see if users MacOS is supported
// Check to see if users macOS is supported
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
// prompt
@ -436,6 +554,11 @@ class AppState: ObservableObject {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
installationPublishers[id] = signInIfNeeded()
.handleEvents(
receiveSubscription: { [unowned self] _ in
self.setInstallationStep(of: availableXcode.version, to: .authenticating)
}
)
.flatMap { [unowned self] in
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
// This publisher will wait for the @Published authentication state to change to authenticated or unauthenticated before finishing,
@ -479,7 +602,7 @@ class AppState: ObservableObject {
.mapError { $0 as Error }
}
.flatMap { [unowned self] in
self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
}
.receive(on: DispatchQueue.main)
.sink(
@ -505,7 +628,7 @@ class AppState: ObservableObject {
func installWithoutLogin(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [unowned self] completion in

View file

@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti
case .xcodeReleases: return "Xcode Releases"
}
}
var isManaged: Bool { PreferenceKey.dataSource.isManaged() }
}

View file

@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti
case .aria2: return "aria2"
}
}
var isManaged: Bool { PreferenceKey.downloader.isManaged() }
}

View file

@ -116,7 +116,8 @@ public struct Shell {
return AsyncThrowingStream<Progress, Error> { continuation in
Task {
var progress = Progress()
// Assume progress will not have data races, so we manually opt-out isolation checks.
nonisolated(unsafe) var progress = Progress()
progress.kind = .file
progress.fileOperationKind = .downloading
@ -195,6 +196,77 @@ public struct Shell {
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
}
public var downloadRuntime: (String, String) -> AsyncThrowingStream<Progress, Error> = { platform, version in
return AsyncThrowingStream<Progress, Error> { continuation in
Task {
// Assume progress will not have data races, so we manually opt-out isolation checks.
nonisolated(unsafe) var progress = Progress()
progress.kind = .file
progress.fileOperationKind = .downloading
let process = Process()
let xcodeBuildPath = Path.root.usr.bin.join("xcodebuild").url
process.executableURL = xcodeBuildPath
process.arguments = [
"-downloadPlatform",
"\(platform)",
"-buildVersion",
"\(version)"
]
let stdOutPipe = Pipe()
process.standardOutput = stdOutPipe
let stdErrPipe = Pipe()
process.standardError = stdErrPipe
let observer = NotificationCenter.default.addObserver(
forName: .NSFileHandleDataAvailable,
object: nil,
queue: OperationQueue.main
) { note in
guard
// This should always be the case for Notification.Name.NSFileHandleDataAvailable
let handle = note.object as? FileHandle,
handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading
else { return }
defer { handle.waitForDataInBackgroundAndNotify() }
let string = String(decoding: handle.availableData, as: UTF8.self)
// TODO: fix warning. ObservingProgressView is currently tied to an updating progress
progress.updateFromXcodebuild(text: string)
continuation.yield(progress)
}
stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
continuation.onTermination = { @Sendable _ in
process.terminate()
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
}
do {
try process.run()
} catch {
continuation.finish(throwing: error)
}
process.waitUntilExit()
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
continuation.finish(throwing: ProcessExecutionError(process: process, standardOutput: "", standardError: ""))
return
}
continuation.finish()
}
}
}
}
public struct Files {

View file

@ -70,5 +70,38 @@ extension Progress {
}
}
func updateFromXcodebuild(text: String) {
self.totalUnitCount = 100
self.completedUnitCount = 0
self.localizedAdditionalDescription = "" // to not show the addtional
do {
let downloadPattern = #"(\d+\.\d+)% \(([\d.]+ (?:MB|GB)) of ([\d.]+ GB)\)"#
let downloadRegex = try NSRegularExpression(pattern: downloadPattern)
// Search for matches in the text
if let match = downloadRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)) {
// Extract the percentage - simpler then trying to extract size MB/GB and convert to bytes.
if let percentRange = Range(match.range(at: 1), in: text), let percentDouble = Double(text[percentRange]) {
let percent = Int64(percentDouble.rounded())
self.completedUnitCount = percent
}
}
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing..." or
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing (registering download)..."
if text.range(of: "Installing") != nil {
// sets the progress to indeterminite to show animating progress
self.totalUnitCount = 0
self.completedUnitCount = 0
}
} catch {
Logger.appState.error("Invalid regular expression")
}
}
}

View file

@ -35,12 +35,11 @@ struct XcodeCommands: Commands {
struct InstallButton: View {
@EnvironmentObject var appState: AppState
@State private var isLoading = false
let xcode: Xcode?
var body: some View {
ProgressButton(isInProgress: isLoading) {
Button {
install()
} label: {
Text("Install")
@ -49,7 +48,6 @@ struct InstallButton: View {
}
private func install() {
isLoading = true
guard let xcode = xcode else { return }
appState.checkMinVersionAndInstall(id: xcode.id)
}
@ -61,7 +59,7 @@ struct CancelInstallButton: View {
var body: some View {
Button(action: cancelInstall) {
Image(systemName: "xmark.circle.fill")
Label("Cancel", systemImage: "xmark")
}
.help(localizeString("StopInstallation"))
.buttonStyle(.plain)

View file

@ -10,7 +10,7 @@ struct AcknowledgmentsView: View {
)!
.addingAttribute(.foregroundColor, value: NSColor.labelColor)
)
.frame(minWidth: 500, minHeight: 500)
.frame(minWidth: 600, minHeight: 500)
}
}

View file

@ -20,27 +20,16 @@ struct NavigationSplitViewWrapper<Sidebar, Detail>: View where Sidebar: View, De
}
var body: some View {
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, visionOS 1, *) {
// Use the latest API available
NavigationSplitView {
if #available(macOS 14, *) {
sidebar
.toolbar(removing: .sidebarToggle)
} else {
sidebar
}
} detail: {
detail
}
} else {
// Alternative code for earlier versions of OS.
NavigationView {
// The first column is the sidebar.
NavigationSplitView {
if #available(macOS 14, *) {
sidebar
.navigationSplitViewColumnWidth(min: 290, ideal: 290)
} else {
sidebar
detail
}
.navigationViewStyle(.columns)
} detail: {
detail
}
.navigationSplitViewStyle(.balanced)
}
}

View file

@ -31,6 +31,7 @@ public struct ObservingProgressIndicator: View {
self.progress = progress
cancellable = progress.publisher(for: \.fractionCompleted)
.combineLatest(progress.publisher(for: \.localizedAdditionalDescription))
.combineLatest(progress.publisher(for: \.isIndeterminate))
.throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in self?.objectWillChange.send() }
}
@ -82,6 +83,18 @@ struct ObservingProgressBar_Previews: PreviewProvider {
style: .bar,
showsAdditionalDescription: true
)
ObservingProgressIndicator(
configure(Progress()) {
$0.kind = .file
$0.fileOperationKind = .downloading
$0.totalUnitCount = 0
$0.completedUnitCount = 0
},
controlSize: .regular,
style: .bar,
showsAdditionalDescription: true
)
}
.previewLayout(.sizeThatFits)
}

View file

@ -22,7 +22,10 @@ struct ProgressIndicator: NSViewRepresentable {
nsView.doubleValue = doubleValue
nsView.controlSize = controlSize
nsView.isIndeterminate = isIndeterminate
nsView.usesThreadedAnimation = true
nsView.style = style
nsView.startAnimation(nil)
}
}

View file

@ -0,0 +1,24 @@
//
// TagView.swift
// Xcodes
//
// Created by Matt Kiazyk on 2025-06-25.//
import SwiftUI
struct TagView: View {
let text: String
var body: some View {
Text(text)
.font(.system(size: 10))
.foregroundColor(.primary)
.padding(.horizontal, 5)
.padding(.vertical, 2)
.background(
Capsule()
.fill(.quaternary)
)
}
}

View file

@ -0,0 +1,22 @@
//
// TrailingIconLabelStyle.swift
// Xcodes
//
// Created by Daniel Chick on 3/11/24.
// Copyright © 2024 Robots and Pencils. All rights reserved.
//
import SwiftUI
struct TrailingIconLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.title
configuration.icon
}
}
}
extension LabelStyle where Self == TrailingIconLabelStyle {
static var trailingIcon: Self { Self() }
}

View file

@ -4,6 +4,7 @@ import AppleAPI
enum XcodesSheet: Identifiable {
case signIn
case twoFactor(SecondFactorData)
case securityKeyTouchToConfirm
var id: Int { Kind(self).hashValue }
@ -16,12 +17,13 @@ enum XcodesSheet: Identifiable {
extension XcodesSheet {
private enum Kind: Hashable {
case signIn, twoFactor(TwoFactorOption)
case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm
enum TwoFactorOption {
case smsSent
case codeSent
case smsPendingChoice
case securityKeyPin
}
init(_ sheet: XcodesSheet) {
@ -32,7 +34,9 @@ extension XcodesSheet {
case .smsSent: self = .twoFactor(.smsSent)
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
case .codeSent: self = .twoFactor(.codeSent)
case .securityKey: self = .twoFactor(.securityKeyPin)
}
case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm
}
}
}

View file

@ -16,7 +16,9 @@ struct CompilersView: View {
if let compilers = compilers {
VStack(alignment: .leading) {
Text("Compilers").font(.headline)
Text(Self.content(from: compilers)).font(.subheadline)
Text(Self.content(from: compilers))
.font(.subheadline)
.textSelection(.enabled)
}
} else {
EmptyView()

View file

@ -29,7 +29,7 @@ extension View {
struct Previews_CornerRadius_Previews: PreviewProvider {
static var previews: some View {
HStack {
Text("XCODES RULES!")
Text(verbatim: "XCODES RULES!")
}.xcodesBackground()
}
}

View file

@ -6,17 +6,18 @@
// Copyright © 2023 Robots and Pencils. All rights reserved.
//
import SwiftUI
import Path
import SwiftUI
import Version
struct IconView: View {
let installState: XcodeInstallState
let xcode: Xcode
var body: some View {
if case let .installed(path) = installState {
if case let .installed(path) = xcode.installState {
Image(nsImage: NSWorkspace.shared.icon(forFile: path.string))
} else {
Image(systemName: "app.fill")
Image(xcode.version.isPrerelease ? "xcode-beta" : "xcode")
.resizable()
.frame(width: 32, height: 32)
.foregroundColor(.secondary)
@ -25,13 +26,19 @@ struct IconView: View {
}
#Preview("Installed") {
IconView(installState: XcodeInstallState.installed(Path("/Applications/Xcode.app")!))
.frame(width: 300, height: 100)
.padding()
IconView(xcode: Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil))
.frame(width: 300, height: 100)
.padding()
}
#Preview("Installed") {
IconView(xcode: Xcode(version: Version("12.3.0")!, installState: .notInstalled, selected: true, icon: nil))
.frame(width: 300, height: 100)
.padding()
}
#Preview("Not Installed") {
IconView(installState: XcodeInstallState.notInstalled)
.frame(width: 300, height: 100)
.padding()
IconView(xcode: Xcode(version: Version("12.0.0-1234A")!, installState: .notInstalled, selected: false, icon: nil))
.frame(width: 300, height: 100)
.padding()
}

View file

@ -29,7 +29,7 @@ struct IdenticalBuildsView: View {
.font(.headline)
ForEach(builds, id: \.description) { version in
Text("\(version.appleDescription)")
Text(verbatim: "\(version.appleDescription)")
.font(.subheadline)
}
}

View file

@ -9,30 +9,35 @@ import struct XCModel.SDKs
struct InfoPane: View {
let xcode: Xcode
var body: some View {
if #available(macOS 14.0, *) {
mainContent
.contentMargins(10, for: .scrollContent)
} else {
mainContent
.padding()
}
}
private var mainContent: some View {
ScrollView(.vertical) {
HStack(alignment: .top) {
VStack {
VStack(spacing: 5) {
HStack {
IconView(installState: xcode.installState)
IconView(xcode: xcode)
Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
InfoPaneControls(xcode: xcode)
}
.xcodesBackground()
VStack {
Text("Platforms")
.font(.title3)
.frame(maxWidth: .infinity, alignment: .leading)
PlatformsView(xcode: xcode)
}
.xcodesBackground()
PlatformsView(xcode: xcode)
}
.frame(minWidth: 380)
VStack(alignment: .leading) {
ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL)
@ -64,15 +69,16 @@ struct InfoPane: View {
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
private func makePreviewContent(for index: Int) -> some View {
let name = XcodePreviewName.allCases[index]
let name = XcodePreviewName.allCases[index]
return InfoPane(xcode: xcodeDict[name]!)
.environmentObject(configure(AppState()) {
$0.allXcodes = [xcodeDict[name]!]
$0.allXcodes = [xcodeDict[name]!]
})
.frame(width: 300, height: 400)
.padding()
.frame(width: 600, height: 400)
.padding()
}
enum XcodePreviewName: String, CaseIterable, Identifiable {
@ -81,7 +87,8 @@ enum XcodePreviewName: String, CaseIterable, Identifiable {
case Populated_Uninstalled
case Basic_Installed
case Basic_Installing
case Basic_Unarchiving
var id: XcodePreviewName { self }
}
@ -141,17 +148,25 @@ var xcodeDict: [XcodePreviewName: Xcode] = [
sdks: nil,
compilers: nil
),
.Basic_Unarchiving: .init(
version: _versionWithMeta,
installState: .installing(.unarchiving),
selected: false,
icon: nil,
sdks: nil,
compilers: nil
),
]
var downloadableRuntimes: [DownloadableRuntime] = {
var runtimes = try! JSONDecoder().decode([DownloadableRuntime].self, from: Current.files.contents(atPath: Path.runtimeCacheFile.string)!)
// set iOS to installed
let iOSIndex = runtimes.firstIndex { $0.sdkBuildUpdate == "19E239" }!
let iOSIndex = 0//runtimes.firstIndex { $0.sdkBuildUpdate.contains == "19E239" }!
var iOSRuntime = runtimes[iOSIndex]
iOSRuntime.installState = .installed
runtimes[iOSIndex] = iOSRuntime
let watchOSIndex = runtimes.firstIndex { $0.sdkBuildUpdate == "20R362" }!
let watchOSIndex = 0//runtimes.firstIndex { $0.sdkBuildUpdate.first == "20R362" }!
var runtime = runtimes[watchOSIndex]
runtime.installState = .installing(
RuntimeInstallationStep.downloading(
@ -163,7 +178,7 @@ var downloadableRuntimes: [DownloadableRuntime] = {
$0.completedUnitCount = 848_444_920
$0.throughput = 9_211_681
}
)
)
)
runtimes[watchOSIndex] = runtime

View file

@ -25,6 +25,7 @@ struct InfoPaneControls: View {
case .installing(let installationStep):
HStack(alignment: .top) {
InstallationStepDetailView(installationStep: installationStep)
.frame(maxWidth: .infinity, alignment: .leading)
CancelInstallButton(xcode: xcode)
}
case .installed(_):
@ -39,6 +40,7 @@ struct InfoPaneControls: View {
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
private func makePreviewContent(for index: Int) -> some View {
let name = XcodePreviewName.allCases[index]
@ -47,6 +49,6 @@ private func makePreviewContent(for index: Int) -> some View {
.environmentObject(configure(AppState()) {
$0.allXcodes = [xcodeDict[name]!]
})
.frame(width: 300)
.frame(width: 500)
.padding()
}

View file

@ -17,7 +17,7 @@ struct InstallationStepDetailView: View {
showsAdditionalDescription: true
)
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
ProgressView()
.scaleEffect(0.5)
}

View file

@ -11,7 +11,8 @@ import XcodesKit
struct PlatformsView: View {
@EnvironmentObject var appState: AppState
@AppStorage("selectedRuntimeArchitecture") private var selectedRuntimeArchitecture: RuntimeArchitecture = .arm64
let xcode: Xcode
var body: some View {
@ -19,17 +20,50 @@ struct PlatformsView: View {
let builds = xcode.sdks?.allBuilds()
let runtimes = builds?.flatMap { sdkBuild in
appState.downloadableRuntimes.filter {
$0.sdkBuildUpdate == sdkBuild
$0.sdkBuildUpdate?.contains(sdkBuild) ?? false &&
($0.architectures?.isEmpty ?? true ||
$0.architectures?.contains(selectedRuntimeArchitecture.rawValue) ?? false)
}
}
ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in
runtimeView(runtime: runtime)
.frame(minWidth: 200)
.padding()
.background(.quinary)
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
let architectures = Set((runtimes ?? []).flatMap { $0.architectures ?? [] })
VStack {
HStack {
Text("Platforms")
.font(.title3)
.frame(maxWidth: .infinity, alignment: .leading)
if !architectures.isEmpty {
Spacer()
Button {
switch selectedRuntimeArchitecture {
case .arm64: selectedRuntimeArchitecture = .x86_64
case .x86_64: selectedRuntimeArchitecture = .arm64
}
} label: {
switch selectedRuntimeArchitecture {
case .arm64:
Label(selectedRuntimeArchitecture.displayValue, systemImage: "m4.button.horizontal")
.labelStyle(.trailingIcon)
case .x86_64:
Label(selectedRuntimeArchitecture.displayValue, systemImage: "cpu.fill")
.labelStyle(.trailingIcon)
}
}
}
}
ForEach(runtimes ?? [], id: \.identifier) { runtime in
runtimeView(runtime: runtime)
.frame(minWidth: 200)
.padding()
.background(.quinary)
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
}
}
.xcodesBackground()
}
@ViewBuilder
@ -39,34 +73,37 @@ struct PlatformsView: View {
runtime.icon()
Text("\(runtime.visibleIdentifier)")
.font(.headline)
ForEach(runtime.architectures ?? [], id: \.self) { architecture in
TagView(text: architecture)
}
pathIfAvailable(xcode: xcode, runtime: runtime)
if runtime.installState == .notInstalled {
// TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly
if appState.runtimeInstallPath(xcode: xcode, runtime: runtime) != nil {
EmptyView()
} else {
HStack {
Spacer()
DownloadRuntimeButton(runtime: runtime)
}
}
}
Spacer()
Text(runtime.downloadFileSizeString)
.font(.subheadline)
.frame(width: 70, alignment: .trailing)
}
switch runtime.installState {
case .installed:
EmptyView()
case .notInstalled:
// TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly
if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) {
EmptyView()
} else {
HStack {
Spacer()
DownloadRuntimeButton(runtime: runtime)
}
}
case .installing(let installationStep):
HStack(alignment: .top, spacing: 5){
RuntimeInstallationStepDetailView(installationStep: installationStep)
.fixedSize(horizontal: false, vertical: true)
Spacer()
CancelRuntimeInstallButton(runtime: runtime)
}
}
if case let .installing(installationStep) = runtime.installState {
HStack(alignment: .top, spacing: 5){
RuntimeInstallationStepDetailView(installationStep: installationStep)
.fixedSize(horizontal: false, vertical: true)
Spacer()
CancelRuntimeInstallButton(runtime: runtime)
}
}
}
}

View file

@ -0,0 +1,17 @@
//
// RuntimeArchitecture.swift
// Xcodes
//
// Created by Matt Kiazyk on 2025-07-07.
//
enum RuntimeArchitecture: String, CaseIterable, Identifiable {
case arm64
case x86_64
var id: Self { self }
var displayValue: String {
return rawValue
}
}

View file

@ -26,8 +26,12 @@ struct RuntimeInstallationStepDetailView: View {
)
case .installing, .trashingArchive:
ProgressView()
.scaleEffect(0.5)
ObservingProgressIndicator(
Progress(),
controlSize: .regular,
style: .bar,
showsAdditionalDescription: false
)
}
}
}

View file

@ -18,7 +18,9 @@ struct SDKsView: View {
} else {
VStack(alignment: .leading) {
Text("SDKs").font(.headline)
Text(content).font(.subheadline)
Text(content)
.font(.subheadline)
.textSelection(.enabled)
}
}
}

View file

@ -16,11 +16,10 @@ struct MainWindow: View {
@AppStorage("isShowingInfoPane") private var isShowingInfoPane = false
@AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all
@AppStorage("isInstalledOnly") private var isInstalledOnly = false
var body: some View {
NavigationSplitViewWrapper {
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly)
.frame(minWidth: 250)
.layoutPriority(1)
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
@ -42,7 +41,6 @@ struct MainWindow: View {
UnselectedView()
}
}
.padding()
.toolbar {
ToolbarItemGroup {
Button(action: { appState.presentedSheet = .signIn }, label: {
@ -56,11 +54,7 @@ struct MainWindow: View {
.help("PreferencesDescription")
} else {
Button(action: {
if #available(macOS 13, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}, label: {
Label("Preferences", systemImage: "gearshape")
})
@ -82,6 +76,9 @@ struct MainWindow: View {
case .twoFactor(let secondFactorData):
secondFactorView(secondFactorData)
.environmentObject(appState)
case .securityKeyTouchToConfirm:
SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil)
.environmentObject(appState)
}
}
.alert(item: $appState.presentedAlert, content: { presentedAlert in
@ -113,6 +110,8 @@ struct MainWindow: View {
SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsPendingChoice:
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .securityKey:
SignInSecurityKeyPinView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
}
}

View file

@ -36,8 +36,10 @@ struct AdvancedPreferencePane: View {
self.appState.installPath = path.string
}
}
.disabled(appState.disableInstallPathChange)
Text("InstallPathDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
@ -71,8 +73,10 @@ struct AdvancedPreferencePane: View {
self.appState.localPath = path.string
}
}
.disabled(appState.disableLocalPathChange)
Text("LocalCachePathDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
@ -80,18 +84,22 @@ struct AdvancedPreferencePane: View {
GroupBox(label: Text("Active/Select")) {
VStack(alignment: .leading) {
Picker("OnSelect", selection: $appState.onSelectActionType) {
Picker(selection: $appState.onSelectActionType) {
Text(SelectedActionType.none.description)
.tag(SelectedActionType.none)
Text(SelectedActionType.rename.description)
.tag(SelectedActionType.rename)
} label: {
Text(verbatim: "OnSelect")
}
.labelsHidden()
.pickerStyle(.inline)
.disabled(appState.onSelectActionTypeDisabled)
Text(appState.onSelectActionType.detailedDescription)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Spacer()
.frame(height: 20)
@ -100,6 +108,7 @@ struct AdvancedPreferencePane: View {
.disabled(appState.createSymLinkOnSelectDisabled)
Text("AutomaticallyCreateSymbolicLinkDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.fixedSize(horizontal: false, vertical: true)
@ -112,6 +121,7 @@ struct AdvancedPreferencePane: View {
.disabled(appState.createSymLinkOnSelectDisabled)
Text("ShowOpenInRosettaDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
@ -126,16 +136,18 @@ struct AdvancedPreferencePane: View {
case .installed:
Text("HelperInstalled")
case .notInstalled:
HStack {
Text("HelperNotInstalled")
VStack(alignment: .leading) {
Button("InstallHelper") {
appState.installHelperIfNecessary()
}
Text("HelperNotInstalled")
.font(.footnote)
}
}
Text("PrivilegedHelperDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Spacer()
@ -151,9 +163,9 @@ struct AdvancedPreferencePane_Previews: PreviewProvider {
Group {
AdvancedPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 500)
.frame(maxWidth: 600)
}
.frame(width: 500, height: 700, alignment: .center)
.frame(width: 600, height: 700, alignment: .center)
}
}
@ -161,11 +173,8 @@ struct AdvancedPreferencePane_Previews: PreviewProvider {
struct PreferencesGroupBoxStyle: GroupBoxStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .top, spacing: 20) {
HStack {
Spacer()
configuration.label
}
.frame(width: 120)
configuration.label
.frame(width: 180, alignment: .trailing)
VStack(alignment: .leading) {
configuration.content

View file

@ -18,13 +18,17 @@ struct DownloadPreferencePane: View {
}
}
.labelsHidden()
AttributedText(dataSourceFootnote)
.fixedSize()
Text("DataSourceDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
.disabled(dataSource.isManaged)
GroupBox(label: Text("Downloader")) {
VStack(alignment: .leading) {
Picker("Downloader", selection: $downloader) {
@ -34,49 +38,27 @@ struct DownloadPreferencePane: View {
}
}
.labelsHidden()
AttributedText(downloaderFootnote)
.fixedSize()
Text("DownloaderDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
.disabled(downloader.isManaged)
}
}
private var dataSourceFootnote: NSAttributedString {
let string = localizeString("DataSourceDescription")
let attributedString = NSMutableAttributedString(
string: string,
attributes: [
.font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
.foregroundColor: NSColor.labelColor
]
)
attributedString.addAttribute(.link, value: URL(string: "https://xcodereleases.com")!, range: NSRange(string.range(of: "Xcode Releases")!, in: string))
return attributedString
}
private var downloaderFootnote: NSAttributedString {
let string = localizeString("DownloaderDescription")
let attributedString = NSMutableAttributedString(
string: string,
attributes: [
.font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
.foregroundColor: NSColor.labelColor
]
)
attributedString.addAttribute(.link, value: URL(string: "https://github.com/aria2/aria2")!, range: NSRange(string.range(of: "aria2")!, in: string))
return attributedString
}
}
struct DownloadPreferencePane_Previews: PreviewProvider {
static var previews: some View {
Group {
GeneralPreferencePane()
DownloadPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 500)
.frame(maxWidth: 600)
.frame(minHeight: 300)
}
}
}

View file

@ -1,6 +1,6 @@
import AppleAPI
import SwiftUI
import Path
import SwiftUI
struct ExperimentsPreferencePane: View {
@EnvironmentObject var appState: AppState
@ -13,29 +13,17 @@ struct ExperimentsPreferencePane: View {
"UseUnxipExperiment",
isOn: $appState.unxipExperiment
)
AttributedText(unxipFootnote)
.disabled(appState.disableUnxipExperiment)
Text("FasterUnxipDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.fixedSize(horizontal: false, vertical: true)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
Divider()
}
}
private var unxipFootnote: NSAttributedString {
let string = localizeString("FasterUnxipDescription")
let attributedString = NSMutableAttributedString(
string: string,
attributes: [
.font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
.foregroundColor: NSColor.labelColor
]
)
attributedString.addAttribute(.link, value: URL(string: "https://twitter.com/_saagarjha")!, range: NSRange(string.range(of: "@_saagarjha")!, in: string))
attributedString.addAttribute(.link, value: URL(string: "https://github.com/saagarjha/unxip")!, range: NSRange(string.range(of: "https://github.com/saagarjha/unxip")!, in: string))
return attributedString
}
}
struct ExperimentsPreferencePane_Previews: PreviewProvider {
@ -43,7 +31,7 @@ struct ExperimentsPreferencePane_Previews: PreviewProvider {
Group {
ExperimentsPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 500)
.frame(maxWidth: 600)
}
}
}

View file

@ -20,6 +20,12 @@ struct GeneralPreferencePane: View {
NotificationsView().environmentObject(appState)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
Divider()
GroupBox(label: Text("Misc")) {
Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
}
}
@ -29,7 +35,7 @@ struct GeneralPreferencePane_Previews: PreviewProvider {
Group {
GeneralPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 500)
.frame(maxWidth: 600)
}
}
}

View file

@ -39,6 +39,6 @@ struct PreferencesView: View {
.tag(Tabs.experiment)
}
.padding(20)
.frame(width: 500)
.frame(width: 600)
}
}

View file

@ -15,11 +15,13 @@ struct UpdatesPreferencePane: View {
"AutomaticInstallNewVersion",
isOn: $autoInstallationType.isAutoInstalling
)
.disabled(updater.disableAutoInstallNewVersions)
Toggle(
"IncludePreRelease",
isOn: $autoInstallationType.isAutoInstallingBeta
)
.disabled(updater.disableIncludePrereleaseVersions)
}
.fixedSize(horizontal: false, vertical: true)
}
@ -34,18 +36,23 @@ struct UpdatesPreferencePane: View {
isOn: $updater.automaticallyChecksForUpdates
)
.fixedSize(horizontal: true, vertical: false)
.disabled(updater.disableAutoUpdateXcodesApp)
Toggle(
"IncludePreRelease",
isOn: $updater.includePrereleaseVersions
)
.disabled(updater.disableAutoUpdateXcodesAppPrereleaseVersions)
Button("CheckNow") {
updater.checkForUpdates()
}
.padding(.top)
.disabled(updater.disableAutoUpdateXcodesApp)
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
.font(.footnote)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
@ -81,12 +88,18 @@ class ObservableUpdater: ObservableObject {
private var lastUpdateCheckDateObservation: NSKeyValueObservation?
@Published var includePrereleaseVersions = false {
didSet {
UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions")
Current.defaults.set(includePrereleaseVersions, forKey: "includePrereleaseVersions")
updaterDelegate.includePrereleaseVersions = includePrereleaseVersions
}
}
var disableAutoInstallNewVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
var disableIncludePrereleaseVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
var disableAutoUpdateXcodesApp: Bool { PreferenceKey.SUEnableAutomaticChecks.isManaged() }
var disableAutoUpdateXcodesAppPrereleaseVersions: Bool { PreferenceKey.includePrereleaseVersions.isManaged() }
init() {
updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater
@ -109,7 +122,7 @@ class ObservableUpdater: ObservableObject {
self.lastUpdateCheckDate = updater.lastUpdateCheckDate
}
)
includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions")
includePrereleaseVersions = Current.defaults.bool(forKey: "includePrereleaseVersions") ?? false
}
func checkForUpdates() {
@ -140,7 +153,9 @@ struct UpdatesPreferencePane_Previews: PreviewProvider {
Group {
UpdatesPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 500)
.environmentObject(ObservableUpdater())
.frame(maxWidth: 600)
.frame(minHeight: 300)
}
}
}

View file

@ -10,12 +10,12 @@ struct SignIn2FAView: View {
var body: some View {
VStack(alignment: .leading) {
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length))
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode!.length))
.fixedSize(horizontal: true, vertical: false)
HStack {
Spacer()
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
appState.submitSecurityCode(.device(code: $0), sessionData: sessionData)
}
Spacer()
@ -32,7 +32,7 @@ struct SignIn2FAView: View {
Text("Continue")
}
.keyboardShortcut(.defaultAction)
.disabled(code.count != authOptions.securityCode.length)
.disabled(code.count != authOptions.securityCode!.length)
}
.frame(height: 25)
}

View file

@ -1,9 +1,14 @@
import SwiftUI
struct SignInCredentialsView: View {
private enum FocusedField {
case username, password
}
@EnvironmentObject var appState: AppState
@State private var username: String = ""
@State private var password: String = ""
@FocusState private var focusedField: FocusedField?
var body: some View {
VStack(alignment: .leading) {
@ -13,12 +18,16 @@ struct SignInCredentialsView: View {
HStack {
Text("AppleID")
.frame(minWidth: 100, alignment: .trailing)
TextField("example@icloud.com", text: $username)
TextField(text: $username) {
Text(verbatim: "example@icloud.com")
}
.focused($focusedField, equals: .username)
}
HStack {
Text("Password")
.frame(minWidth: 100, alignment: .trailing)
SecureField("Required", text: $password)
.focused($focusedField, equals: .password)
}
if appState.authError != nil {
HStack {

View file

@ -1,5 +1,5 @@
import SwiftUI
import AppleAPI
import SwiftUI
struct SignInPhoneListView: View {
@EnvironmentObject var appState: AppState
@ -7,12 +7,12 @@ struct SignInPhoneListView: View {
@State private var selectedPhoneNumberID: AuthOptionsResponse.TrustedPhoneNumber.ID?
let authOptions: AuthOptionsResponse
let sessionData: AppleSessionData
var body: some View {
VStack(alignment: .leading) {
if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty {
Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode.length))
Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode!.length))
List(phoneNumbers, selection: $selectedPhoneNumberID) {
Text($0.numberWithDialCode)
}
@ -22,19 +22,18 @@ struct SignInPhoneListView: View {
}
}
} else {
AttributedText(
NSAttributedString(string: localizeString("NoTrustedPhones"))
.convertingURLsToLinkAttributes()
)
Text("NoTrustedPhones")
.font(.callout)
Spacer()
}
HStack {
Button("Cancel", action: { isPresented = false })
.keyboardShortcut(.cancelAction)
Spacer()
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
action: { appState.requestSMS(to: authOptions.trustedPhoneNumbers!.first { $0.id == selectedPhoneNumberID }!, authOptions: authOptions, sessionData: sessionData) }) {
action: { appState.requestSMS(to: authOptions.trustedPhoneNumbers!.first { $0.id == selectedPhoneNumberID }!, authOptions: authOptions, sessionData: sessionData) })
{
Text("Continue")
}
.keyboardShortcut(.defaultAction)
@ -54,9 +53,10 @@ struct SignInPhoneListView_Previews: PreviewProvider {
SignInPhoneListView(
isPresented: .constant(true),
authOptions: AuthOptionsResponse(
trustedPhoneNumbers: [.init(id: 0, numberWithDialCode: "(•••) •••-••90")],
trustedPhoneNumbers: [.init(id: 0, numberWithDialCode: "(•••) •••-••90")],
trustedDevices: nil,
securityCode: .init(length: 6)),
securityCode: .init(length: 6)
),
sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "")
)
.environmentObject(AppState())
@ -64,9 +64,10 @@ struct SignInPhoneListView_Previews: PreviewProvider {
SignInPhoneListView(
isPresented: .constant(true),
authOptions: AuthOptionsResponse(
trustedPhoneNumbers: [],
trustedPhoneNumbers: [],
trustedDevices: nil,
securityCode: .init(length: 6)),
securityCode: .init(length: 6)
),
sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "")
)
.environmentObject(AppState())

View file

@ -11,11 +11,11 @@ struct SignInSMSView: View {
var body: some View {
VStack(alignment: .leading) {
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode))
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode!.length, trustedPhoneNumber.numberWithDialCode))
HStack {
Spacer()
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
appState.submitSecurityCode(.sms(code: $0, phoneNumberId: trustedPhoneNumber.id), sessionData: sessionData)
}
Spacer()
@ -31,7 +31,7 @@ struct SignInSMSView: View {
Text("Continue")
}
.keyboardShortcut(.defaultAction)
.disabled(code.count != authOptions.securityCode.length)
.disabled(code.count != authOptions.securityCode!.length)
}
.frame(height: 25)
}

View file

@ -0,0 +1,70 @@
//
// SignInSecurityKeyPin.swift
// Xcodes
//
// Created by Kino on 2024-09-26.
// Copyright © 2024 Robots and Pencils. All rights reserved.
//
import SwiftUI
import AppleAPI
struct SignInSecurityKeyPinView: View {
@EnvironmentObject var appState: AppState
@Binding var isPresented: Bool
@State private var pin: String = ""
let authOptions: AuthOptionsResponse
let sessionData: AppleSessionData
var body: some View {
VStack(alignment: .leading) {
Text(localizeString("SecurityKeyPinDescription"))
.fixedSize(horizontal: true, vertical: false)
HStack {
Spacer()
SecureField("PIN", text: $pin)
Spacer()
}
.padding()
HStack {
Button("Cancel", action: { isPresented = false })
.keyboardShortcut(.cancelAction)
Spacer()
Button("PIN not set", action: submitWithoutPinCode)
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
action: submitPinCode) {
Text("Continue")
}
.keyboardShortcut(.defaultAction)
// FIDO2 device pin codes must be at least 4 code points
// https://docs.yubico.com/yesdk/users-manual/application-fido2/fido2-pin.html
.disabled(pin.count < 4)
}
.frame(height: 25)
}
.padding()
.emittingError($appState.authError, recoveryHandler: { _ in })
}
func submitPinCode() {
appState.createAndSubmitSecurityKeyAssertationWithPinCode(pin, sessionData: sessionData, authOptions: authOptions)
}
func submitWithoutPinCode() {
appState.createAndSubmitSecurityKeyAssertationWithPinCode(nil, sessionData: sessionData, authOptions: authOptions)
}
}
#Preview {
SignInSecurityKeyPinView(isPresented: .constant(true),
authOptions: AuthOptionsResponse(
trustedPhoneNumbers: nil,
trustedDevices: nil,
securityCode: .init(length: 6)
), sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: ""))
.environmentObject(AppState())
}

View file

@ -0,0 +1,54 @@
//
// SignInSecurityKeyPin.swift
// Xcodes
//
// Created by Kino on 2024-09-26.
// Copyright © 2024 Robots and Pencils. All rights reserved.
//
import SwiftUI
import AppleAPI
struct SignInSecurityKeyTouchView: View {
@EnvironmentObject var appState: AppState
@Binding var isPresented: Bool
var body: some View {
VStack(alignment: .center) {
Image(systemName: "key.radiowaves.forward")
.font(.system(size: 32)).bold()
.padding(.bottom)
HStack {
Spacer()
Text(localizeString("SecurityKeyTouchDescription"))
.fixedSize(horizontal: true, vertical: false)
Spacer()
}
HStack {
Button("Cancel", action: self.cancel)
.keyboardShortcut(.cancelAction)
Spacer()
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.scaleEffect(x: 0.5, y: 0.5, anchor: .center)
.isHidden(!appState.isProcessingAuthRequest)
.keyboardShortcut(.defaultAction)
}
.frame(height: 25)
}
.padding()
.emittingError($appState.authError, recoveryHandler: { _ in })
}
func cancel() {
appState.cancelSecurityKeyAssertationRequest()
isPresented = false
}
}
#Preview {
SignInSecurityKeyTouchView(isPresented: .constant(true))
.environmentObject(AppState())
}

View file

@ -14,10 +14,10 @@ extension View {
struct View_IsHidden_Previews: PreviewProvider {
static var previews: some View {
Group {
Text("Not Hidden")
Text(verbatim: "Not Hidden")
.isHidden(false)
Text("Hidden")
Text(verbatim: "Hidden")
.isHidden(true)
}
}

View file

@ -97,33 +97,45 @@ struct AppStoreButtonStyle_Previews: PreviewProvider {
Group {
ForEach([ColorScheme.light, .dark], id: \.self) { colorScheme in
Group {
Button("OPEN", action: {})
Button{ } label: {
Text(verbatim: "OPEN")
}
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false))
.padding()
.background(Color(.textBackgroundColor))
.previewDisplayName("Primary")
Button("OPEN", action: {})
Button{ } label: {
Text(verbatim: "OPEN")
}
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: true))
.padding()
.background(Color(.controlAccentColor))
.previewDisplayName("Primary, Highlighted")
Button("OPEN", action: {})
Button{ } label: {
Text(verbatim: "OPEN")
}
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false))
.padding()
.disabled(true)
.background(Color(.textBackgroundColor))
.previewDisplayName("Primary, Disabled")
Button("INSTALL", action: {})
Button{ } label: {
Text(verbatim: "INSTALL")
}
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false))
.padding()
.background(Color(.textBackgroundColor))
.previewDisplayName("Secondary")
Button("INSTALL", action: {})
Button{ } label: {
Text(verbatim: "INSTALL")
}
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: true))
.padding()
.background(Color(.controlAccentColor))
.previewDisplayName("Secondary, Highlighted")
Button("INSTALL", action: {})
Button{ } label: {
Text(verbatim: "INSTALL")
}
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false))
.padding()
.disabled(true)

View file

@ -11,6 +11,8 @@ import SwiftUI
struct BottomStatusModifier: ViewModifier {
@EnvironmentObject var appState: AppState
@AppStorage(PreferenceKey.hideSupportXcodes.rawValue) var hideSupportXcodes = false
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
func body(content: Content) -> some View {
@ -20,17 +22,19 @@ struct BottomStatusModifier: ViewModifier {
Divider()
HStack {
Text(appState.bottomStatusBarMessage)
.font(.subheadline)
.font(.subheadline)
Spacer()
Button(action: {
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
}) {
HStack {
Image(systemName: "heart.circle")
Text("Support.Xcodes")
if !hideSupportXcodes {
Button(action: {
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
}) {
HStack {
Image(systemName: "heart.circle")
Text("Support.Xcodes")
}
}
}
Text(Bundle.main.shortVersion!)
Text(verbatim: "\(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
.font(.subheadline)
}
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
@ -51,8 +55,34 @@ extension View {
struct Previews_BottomStatusBar_Previews: PreviewProvider {
static var previews: some View {
HStack {
}.bottomStatusBar()
Group {
HStack {
}
.bottomStatusBar()
.environmentObject({ () -> AppState in
let a = AppState()
return a }()
)
.defaultAppStorage({ () -> UserDefaults in
let d = UserDefaults(suiteName: "hide_support")!
d.set(true, forKey: PreferenceKey.hideSupportXcodes.rawValue)
return d
}())
HStack {
}
.bottomStatusBar()
.environmentObject({ () -> AppState in
let a = AppState()
return a }()
)
.defaultAppStorage({ () -> UserDefaults in
let d = UserDefaults(suiteName: "show_support")!
d.set(false, forKey: PreferenceKey.hideSupportXcodes.rawValue)
return d
}())
}
}
}

View file

@ -18,7 +18,7 @@ struct InstallationStepRowView: View {
controlSize: .small,
style: .spinning
)
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
ProgressView()
.scaleEffect(0.5)
}

View file

@ -5,7 +5,7 @@ struct MainToolbarModifier: ViewModifier {
@Binding var category: XcodeListCategory
@Binding var isInstalledOnly: Bool
@Binding var isShowingInfoPane: Bool
func body(content: Content) -> some View {
content
.toolbar { toolbar }
@ -13,9 +13,8 @@ struct MainToolbarModifier: ViewModifier {
private var toolbar: some ToolbarContent {
ToolbarItemGroup {
ProgressButton(
isInProgress: appState.isUpdating,
isInProgress: appState.isUpdating,
action: appState.update
) {
Label("Refresh", systemImage: "arrow.clockwise")
@ -23,6 +22,7 @@ struct MainToolbarModifier: ViewModifier {
.keyboardShortcut(KeyEquivalent("r"))
.help("RefreshDescription")
Spacer()
Button(action: {
switch category {
case .all: category = .release
@ -35,39 +35,22 @@ struct MainToolbarModifier: ViewModifier {
case .all:
Label("All", systemImage: "line.horizontal.3.decrease.circle")
case .release:
if #available(macOS 11.3, *) {
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleAndIconLabelStyle())
.labelStyle(.trailingIcon)
.foregroundColor(.accentColor)
} else {
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleOnlyLabelStyle())
.foregroundColor(.accentColor)
}
case .beta:
if #available(macOS 11.3, *) {
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleAndIconLabelStyle())
.foregroundColor(.accentColor)
} else {
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleOnlyLabelStyle())
.foregroundColor(.accentColor)
}
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(.trailingIcon)
.foregroundColor(.accentColor)
case .releasePlusNewBetas:
if #available(macOS 11.3, *) {
Label("ReleasePlusNewBetas", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleAndIconLabelStyle())
.foregroundColor(.accentColor)
} else {
Label("ReleasePlusNewBetas", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleOnlyLabelStyle())
.foregroundColor(.accentColor)
}
Label("ReleasePlusNewBetas", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(.trailingIcon)
.foregroundColor(.accentColor)
}
}
.help("FilterAvailableDescription")
.disabled(category.isManaged)
Button(action: {
isInstalledOnly.toggle()
}) {
@ -76,11 +59,9 @@ struct MainToolbarModifier: ViewModifier {
.foregroundColor(.accentColor)
} else {
Label("Filter", systemImage: "arrow.down.app")
}
}
.help("FilterInstalledDescription")
}
}
}
@ -91,7 +72,7 @@ extension View {
isInstalledOnly: Binding<Bool>,
isShowingInfoPane: Binding<Bool>
) -> some View {
self.modifier(
modifier(
MainToolbarModifier(
category: category,
isInstalledOnly: isInstalledOnly,

View file

@ -16,4 +16,6 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti
case .releasePlusNewBetas: return localizeString("ReleasePlusNewBetas")
}
}
var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() }
}

View file

@ -8,7 +8,8 @@ struct XcodeListView: View {
private let searchText: String
private let category: XcodeListCategory
private let isInstalledOnly: Bool
@AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) {
self._selectedXcodeID = selectedXcodeID
self.searchText = searchText
@ -36,6 +37,22 @@ struct XcodeListView: View {
}
}
let latestMajor = xcodes.sorted(\.version)
.filter { $0.version.isNotPrerelease }
.last?
.version
.major
xcodes = xcodes.filter {
if $0.installState.notInstalled,
let latestMajor = latestMajor,
$0.version.major < (latestMajor - min(latestMajor,allowedMajorVersions)) {
return false
}
return true
}
if !searchText.isEmpty {
xcodes = xcodes.filter { $0.description.contains(searchText) }
}
@ -54,7 +71,8 @@ struct XcodeListView: View {
.listStyle(.sidebar)
.safeAreaInset(edge: .bottom, spacing: 0) {
PlatformsPocket()
.padding()
.padding(.horizontal)
.padding(.vertical, 8)
}
}
}
@ -63,19 +81,21 @@ struct PlatformsPocket: View {
@SwiftUI.Environment(\.openWindow) private var openWindow
var body: some View {
Button(action: {
openWindow(id: "platforms") }
Button(action: {
openWindow(id: "platforms")
}
) {
VStack(spacing: 5) {
HStack(spacing: 5) {
Image(systemName: "square.3.layers.3d")
.font(.title)
Text("Platforms")
.font(.callout)
.font(.title3.weight(.medium))
Text("PlatformsDescription")
Spacer()
}
.frame(width: 70, height: 70)
.background(.quaternary)
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
.font(.body.weight(.medium))
.padding(.horizontal)
.padding(.vertical, 12)
.background(.quaternary.opacity(0.75))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.buttonStyle(.plain)
}
@ -93,6 +113,9 @@ struct XcodeListView_Previews: PreviewProvider {
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil),
Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil),
Xcode(version: Version("12.0.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
Xcode(version: Version("10.1.0")!, installState: .notInstalled, selected: false, icon: nil),
Xcode(version: Version("10.0.0")!, installState: .installed(Path("/Applications/Xcode-10.0.0.app")!), selected: false, icon: nil),
Xcode(version: Version("9.0.0")!, installState: .notInstalled, selected: false, icon: nil),
]
return a
}())

View file

@ -30,9 +30,6 @@ struct XcodeListViewRow: View {
Text(verbatim: path.string)
.font(.caption)
.foregroundColor(.secondary)
} else {
Text(verbatim: "")
.font(.caption)
}
}
@ -42,6 +39,7 @@ struct XcodeListViewRow: View {
.padding(.trailing, 16)
installControl(for: xcode)
}
.padding(.vertical, 4)
.contextMenu {
switch xcode.installState {
case .notInstalled:
@ -75,9 +73,10 @@ struct XcodeListViewRow: View {
if let icon = xcode.icon {
Image(nsImage: icon)
} else {
Color.clear
Image(xcode.version.isPrerelease ? "xcode-beta" : "xcode")
.resizable()
.frame(width: 32, height: 32)
.foregroundColor(.secondary)
.opacity(0.2)
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -40,6 +40,6 @@
<key>SUFeedURL</key>
<string>https://www.xcodes.app/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>GrqOrFQHxfqoLFCAGc9luvsAWQifHtG9gQ3NVJ583tE=</string>
<string>SEcz0vgUSeBTOoAXYe+64zea95G6lIf5NgzFs3InYJQ=</string>
</dict>
</plist>

View file

@ -1,4 +1,4 @@
{\rtf1\ansi\ansicpg1252\cocoartf2759
{\rtf1\ansi\ansicpg1252\cocoartf2822
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
@ -58,6 +58,33 @@ SOFTWARE.\
\
\
\fs34 LibFido2Swift\
\
\fs26 MIT License\
\
Copyright (c) 2024 Kino Roy\
\
Permission is hereby granted, free of charge, to any person obtaining a copy\
of this software and associated documentation files (the "Software"), to deal\
in the Software without restriction, including without limitation the rights\
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
copies of the Software, and to permit persons to whom the Software is\
furnished to do so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included in all\
copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
SOFTWARE.\
\
\
\fs34 ErrorHandling\
\
@ -86,6 +113,241 @@ SOFTWARE.\
\
\
\fs34 big-num\
\
\fs26 MIT License\
\
Copyright (c) 2019 Adam Fowler\
\
Permission is hereby granted, free of charge, to any person obtaining a copy\
of this software and associated documentation files (the "Software"), to deal\
in the Software without restriction, including without limitation the rights\
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
copies of the Software, and to permit persons to whom the Software is\
furnished to do so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included in all\
copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
SOFTWARE.\
\
\
\fs34 swift-crypto\
\
\fs26 \
Apache License\
Version 2.0, January 2004\
http://www.apache.org/licenses/\
\
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\
\
1. Definitions.\
\
"License" shall mean the terms and conditions for use, reproduction,\
and distribution as defined by Sections 1 through 9 of this document.\
\
"Licensor" shall mean the copyright owner or entity authorized by\
the copyright owner that is granting the License.\
\
"Legal Entity" shall mean the union of the acting entity and all\
other entities that control, are controlled by, or are under common\
control with that entity. For the purposes of this definition,\
"control" means (i) the power, direct or indirect, to cause the\
direction or management of such entity, whether by contract or\
otherwise, or (ii) ownership of fifty percent (50%) or more of the\
outstanding shares, or (iii) beneficial ownership of such entity.\
\
"You" (or "Your") shall mean an individual or Legal Entity\
exercising permissions granted by this License.\
\
"Source" form shall mean the preferred form for making modifications,\
including but not limited to software source code, documentation\
source, and configuration files.\
\
"Object" form shall mean any form resulting from mechanical\
transformation or translation of a Source form, including but\
not limited to compiled object code, generated documentation,\
and conversions to other media types.\
\
"Work" shall mean the work of authorship, whether in Source or\
Object form, made available under the License, as indicated by a\
copyright notice that is included in or attached to the work\
(an example is provided in the Appendix below).\
\
"Derivative Works" shall mean any work, whether in Source or Object\
form, that is based on (or derived from) the Work and for which the\
editorial revisions, annotations, elaborations, or other modifications\
represent, as a whole, an original work of authorship. For the purposes\
of this License, Derivative Works shall not include works that remain\
separable from, or merely link (or bind by name) to the interfaces of,\
the Work and Derivative Works thereof.\
\
"Contribution" shall mean any work of authorship, including\
the original version of the Work and any modifications or additions\
to that Work or Derivative Works thereof, that is intentionally\
submitted to Licensor for inclusion in the Work by the copyright owner\
or by an individual or Legal Entity authorized to submit on behalf of\
the copyright owner. For the purposes of this definition, "submitted"\
means any form of electronic, verbal, or written communication sent\
to the Licensor or its representatives, including but not limited to\
communication on electronic mailing lists, source code control systems,\
and issue tracking systems that are managed by, or on behalf of, the\
Licensor for the purpose of discussing and improving the Work, but\
excluding communication that is conspicuously marked or otherwise\
designated in writing by the copyright owner as "Not a Contribution."\
\
"Contributor" shall mean Licensor and any individual or Legal Entity\
on behalf of whom a Contribution has been received by Licensor and\
subsequently incorporated within the Work.\
\
2. Grant of Copyright License. Subject to the terms and conditions of\
this License, each Contributor hereby grants to You a perpetual,\
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
copyright license to reproduce, prepare Derivative Works of,\
publicly display, publicly perform, sublicense, and distribute the\
Work and such Derivative Works in Source or Object form.\
\
3. Grant of Patent License. Subject to the terms and conditions of\
this License, each Contributor hereby grants to You a perpetual,\
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
(except as stated in this section) patent license to make, have made,\
use, offer to sell, sell, import, and otherwise transfer the Work,\
where such license applies only to those patent claims licensable\
by such Contributor that are necessarily infringed by their\
Contribution(s) alone or by combination of their Contribution(s)\
with the Work to which such Contribution(s) was submitted. If You\
institute patent litigation against any entity (including a\
cross-claim or counterclaim in a lawsuit) alleging that the Work\
or a Contribution incorporated within the Work constitutes direct\
or contributory patent infringement, then any patent licenses\
granted to You under this License for that Work shall terminate\
as of the date such litigation is filed.\
\
4. Redistribution. You may reproduce and distribute copies of the\
Work or Derivative Works thereof in any medium, with or without\
modifications, and in Source or Object form, provided that You\
meet the following conditions:\
\
(a) You must give any other recipients of the Work or\
Derivative Works a copy of this License; and\
\
(b) You must cause any modified files to carry prominent notices\
stating that You changed the files; and\
\
(c) You must retain, in the Source form of any Derivative Works\
that You distribute, all copyright, patent, trademark, and\
attribution notices from the Source form of the Work,\
excluding those notices that do not pertain to any part of\
the Derivative Works; and\
\
(d) If the Work includes a "NOTICE" text file as part of its\
distribution, then any Derivative Works that You distribute must\
include a readable copy of the attribution notices contained\
within such NOTICE file, excluding those notices that do not\
pertain to any part of the Derivative Works, in at least one\
of the following places: within a NOTICE text file distributed\
as part of the Derivative Works; within the Source form or\
documentation, if provided along with the Derivative Works; or,\
within a display generated by the Derivative Works, if and\
wherever such third-party notices normally appear. The contents\
of the NOTICE file are for informational purposes only and\
do not modify the License. You may add Your own attribution\
notices within Derivative Works that You distribute, alongside\
or as an addendum to the NOTICE text from the Work, provided\
that such additional attribution notices cannot be construed\
as modifying the License.\
\
You may add Your own copyright statement to Your modifications and\
may provide additional or different license terms and conditions\
for use, reproduction, or distribution of Your modifications, or\
for any such Derivative Works as a whole, provided Your use,\
reproduction, and distribution of the Work otherwise complies with\
the conditions stated in this License.\
\
5. Submission of Contributions. Unless You explicitly state otherwise,\
any Contribution intentionally submitted for inclusion in the Work\
by You to the Licensor shall be under the terms and conditions of\
this License, without any additional terms or conditions.\
Notwithstanding the above, nothing herein shall supersede or modify\
the terms of any separate license agreement you may have executed\
with Licensor regarding such Contributions.\
\
6. Trademarks. This License does not grant permission to use the trade\
names, trademarks, service marks, or product names of the Licensor,\
except as required for reasonable and customary use in describing the\
origin of the Work and reproducing the content of the NOTICE file.\
\
7. Disclaimer of Warranty. Unless required by applicable law or\
agreed to in writing, Licensor provides the Work (and each\
Contributor provides its Contributions) on an "AS IS" BASIS,\
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\
implied, including, without limitation, any warranties or conditions\
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\
PARTICULAR PURPOSE. You are solely responsible for determining the\
appropriateness of using or redistributing the Work and assume any\
risks associated with Your exercise of permissions under this License.\
\
8. Limitation of Liability. In no event and under no legal theory,\
whether in tort (including negligence), contract, or otherwise,\
unless required by applicable law (such as deliberate and grossly\
negligent acts) or agreed to in writing, shall any Contributor be\
liable to You for damages, including any direct, indirect, special,\
incidental, or consequential damages of any character arising as a\
result of this License or out of the use or inability to use the\
Work (including but not limited to damages for loss of goodwill,\
work stoppage, computer failure or malfunction, or any and all\
other commercial damages or losses), even if such Contributor\
has been advised of the possibility of such damages.\
\
9. Accepting Warranty or Additional Liability. While redistributing\
the Work or Derivative Works thereof, You may choose to offer,\
and charge a fee for, acceptance of support, warranty, indemnity,\
or other liability obligations and/or rights consistent with this\
License. However, in accepting such obligations, You may act only\
on Your own behalf and on Your sole responsibility, not on behalf\
of any other Contributor, and only if You agree to indemnify,\
defend, and hold each Contributor harmless for any liability\
incurred by, or claims asserted against, such Contributor by reason\
of your accepting any such warranty or additional liability.\
\
END OF TERMS AND CONDITIONS\
\
APPENDIX: How to apply the Apache License to your work.\
\
To apply the Apache License to your work, attach the following\
boilerplate notice, with the fields enclosed by brackets "[]"\
replaced with your own identifying information. (Don't include\
the brackets!) The text should be enclosed in the appropriate\
comment syntax for the file format. We also recommend that a\
file or class name and description of purpose be included on the\
same "printed page" as the copyright notice for easier\
identification within third-party archives.\
\
Copyright [yyyy] [name of copyright owner]\
\
Licensed under the Apache License, Version 2.0 (the "License");\
you may not use this file except in compliance with the License.\
You may obtain a copy of the License at\
\
http://www.apache.org/licenses/LICENSE-2.0\
\
Unless required by applicable law or agreed to in writing, software\
distributed under the License is distributed on an "AS IS" BASIS,\
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\
See the License for the specific language governing permissions and\
limitations under the License.\
\
\
\fs34 Path.swift\
\
@ -552,12 +814,219 @@ For more information, please refer to &lt;<http://unlicense.org/>&gt;\
\
\
\fs34 swift-srp\
\
\fs26 Apache License\
Version 2.0, January 2004\
http://www.apache.org/licenses/\
\
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\
\
1. Definitions.\
\
"License" shall mean the terms and conditions for use, reproduction,\
and distribution as defined by Sections 1 through 9 of this document.\
\
"Licensor" shall mean the copyright owner or entity authorized by\
the copyright owner that is granting the License.\
\
"Legal Entity" shall mean the union of the acting entity and all\
other entities that control, are controlled by, or are under common\
control with that entity. For the purposes of this definition,\
"control" means (i) the power, direct or indirect, to cause the\
direction or management of such entity, whether by contract or\
otherwise, or (ii) ownership of fifty percent (50%) or more of the\
outstanding shares, or (iii) beneficial ownership of such entity.\
\
"You" (or "Your") shall mean an individual or Legal Entity\
exercising permissions granted by this License.\
\
"Source" form shall mean the preferred form for making modifications,\
including but not limited to software source code, documentation\
source, and configuration files.\
\
"Object" form shall mean any form resulting from mechanical\
transformation or translation of a Source form, including but\
not limited to compiled object code, generated documentation,\
and conversions to other media types.\
\
"Work" shall mean the work of authorship, whether in Source or\
Object form, made available under the License, as indicated by a\
copyright notice that is included in or attached to the work\
(an example is provided in the Appendix below).\
\
"Derivative Works" shall mean any work, whether in Source or Object\
form, that is based on (or derived from) the Work and for which the\
editorial revisions, annotations, elaborations, or other modifications\
represent, as a whole, an original work of authorship. For the purposes\
of this License, Derivative Works shall not include works that remain\
separable from, or merely link (or bind by name) to the interfaces of,\
the Work and Derivative Works thereof.\
\
"Contribution" shall mean any work of authorship, including\
the original version of the Work and any modifications or additions\
to that Work or Derivative Works thereof, that is intentionally\
submitted to Licensor for inclusion in the Work by the copyright owner\
or by an individual or Legal Entity authorized to submit on behalf of\
the copyright owner. For the purposes of this definition, "submitted"\
means any form of electronic, verbal, or written communication sent\
to the Licensor or its representatives, including but not limited to\
communication on electronic mailing lists, source code control systems,\
and issue tracking systems that are managed by, or on behalf of, the\
Licensor for the purpose of discussing and improving the Work, but\
excluding communication that is conspicuously marked or otherwise\
designated in writing by the copyright owner as "Not a Contribution."\
\
"Contributor" shall mean Licensor and any individual or Legal Entity\
on behalf of whom a Contribution has been received by Licensor and\
subsequently incorporated within the Work.\
\
2. Grant of Copyright License. Subject to the terms and conditions of\
this License, each Contributor hereby grants to You a perpetual,\
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
copyright license to reproduce, prepare Derivative Works of,\
publicly display, publicly perform, sublicense, and distribute the\
Work and such Derivative Works in Source or Object form.\
\
3. Grant of Patent License. Subject to the terms and conditions of\
this License, each Contributor hereby grants to You a perpetual,\
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
(except as stated in this section) patent license to make, have made,\
use, offer to sell, sell, import, and otherwise transfer the Work,\
where such license applies only to those patent claims licensable\
by such Contributor that are necessarily infringed by their\
Contribution(s) alone or by combination of their Contribution(s)\
with the Work to which such Contribution(s) was submitted. If You\
institute patent litigation against any entity (including a\
cross-claim or counterclaim in a lawsuit) alleging that the Work\
or a Contribution incorporated within the Work constitutes direct\
or contributory patent infringement, then any patent licenses\
granted to You under this License for that Work shall terminate\
as of the date such litigation is filed.\
\
4. Redistribution. You may reproduce and distribute copies of the\
Work or Derivative Works thereof in any medium, with or without\
modifications, and in Source or Object form, provided that You\
meet the following conditions:\
\
(a) You must give any other recipients of the Work or\
Derivative Works a copy of this License; and\
\
(b) You must cause any modified files to carry prominent notices\
stating that You changed the files; and\
\
(c) You must retain, in the Source form of any Derivative Works\
that You distribute, all copyright, patent, trademark, and\
attribution notices from the Source form of the Work,\
excluding those notices that do not pertain to any part of\
the Derivative Works; and\
\
(d) If the Work includes a "NOTICE" text file as part of its\
distribution, then any Derivative Works that You distribute must\
include a readable copy of the attribution notices contained\
within such NOTICE file, excluding those notices that do not\
pertain to any part of the Derivative Works, in at least one\
of the following places: within a NOTICE text file distributed\
as part of the Derivative Works; within the Source form or\
documentation, if provided along with the Derivative Works; or,\
within a display generated by the Derivative Works, if and\
wherever such third-party notices normally appear. The contents\
of the NOTICE file are for informational purposes only and\
do not modify the License. You may add Your own attribution\
notices within Derivative Works that You distribute, alongside\
or as an addendum to the NOTICE text from the Work, provided\
that such additional attribution notices cannot be construed\
as modifying the License.\
\
You may add Your own copyright statement to Your modifications and\
may provide additional or different license terms and conditions\
for use, reproduction, or distribution of Your modifications, or\
for any such Derivative Works as a whole, provided Your use,\
reproduction, and distribution of the Work otherwise complies with\
the conditions stated in this License.\
\
5. Submission of Contributions. Unless You explicitly state otherwise,\
any Contribution intentionally submitted for inclusion in the Work\
by You to the Licensor shall be under the terms and conditions of\
this License, without any additional terms or conditions.\
Notwithstanding the above, nothing herein shall supersede or modify\
the terms of any separate license agreement you may have executed\
with Licensor regarding such Contributions.\
\
6. Trademarks. This License does not grant permission to use the trade\
names, trademarks, service marks, or product names of the Licensor,\
except as required for reasonable and customary use in describing the\
origin of the Work and reproducing the content of the NOTICE file.\
\
7. Disclaimer of Warranty. Unless required by applicable law or\
agreed to in writing, Licensor provides the Work (and each\
Contributor provides its Contributions) on an "AS IS" BASIS,\
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\
implied, including, without limitation, any warranties or conditions\
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\
PARTICULAR PURPOSE. You are solely responsible for determining the\
appropriateness of using or redistributing the Work and assume any\
risks associated with Your exercise of permissions under this License.\
\
8. Limitation of Liability. In no event and under no legal theory,\
whether in tort (including negligence), contract, or otherwise,\
unless required by applicable law (such as deliberate and grossly\
negligent acts) or agreed to in writing, shall any Contributor be\
liable to You for damages, including any direct, indirect, special,\
incidental, or consequential damages of any character arising as a\
result of this License or out of the use or inability to use the\
Work (including but not limited to damages for loss of goodwill,\
work stoppage, computer failure or malfunction, or any and all\
other commercial damages or losses), even if such Contributor\
has been advised of the possibility of such damages.\
\
9. Accepting Warranty or Additional Liability. While redistributing\
the Work or Derivative Works thereof, You may choose to offer,\
and charge a fee for, acceptance of support, warranty, indemnity,\
or other liability obligations and/or rights consistent with this\
License. However, in accepting such obligations, You may act only\
on Your own behalf and on Your sole responsibility, not on behalf\
of any other Contributor, and only if You agree to indemnify,\
defend, and hold each Contributor harmless for any liability\
incurred by, or claims asserted against, such Contributor by reason\
of your accepting any such warranty or additional liability.\
\
END OF TERMS AND CONDITIONS\
\
APPENDIX: How to apply the Apache License to your work.\
\
To apply the Apache License to your work, attach the following\
boilerplate notice, with the fields enclosed by brackets "[]"\
replaced with your own identifying information. (Don't include\
the brackets!) The text should be enclosed in the appropriate\
comment syntax for the file format. We also recommend that a\
file or class name and description of purpose be included on the\
same "printed page" as the copyright notice for easier\
identification within third-party archives.\
\
Copyright [yyyy] [name of copyright owner]\
\
Licensed under the Apache License, Version 2.0 (the "License");\
you may not use this file except in compliance with the License.\
You may obtain a copy of the License at\
\
http://www.apache.org/licenses/LICENSE-2.0\
\
Unless required by applicable law or agreed to in writing, software\
distributed under the License is distributed on an "AS IS" BASIS,\
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\
See the License for the specific language governing permissions and\
limitations under the License.\
\
\
\fs34 DockProgress\
\
\fs26 MIT License\
\
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\
\
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
\

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ struct XcodesApp: App {
@StateObject private var updater = ObservableUpdater()
var body: some Scene {
WindowGroup("Xcodes") {
Window("Xcodes", id: "main") {
MainWindow()
.environmentObject(appState)
.environmentObject(updater)
@ -51,19 +51,19 @@ struct XcodesApp: App {
CommandGroup(replacing: CommandGroupPlacement.help) {
Button("Menu.GitHubRepo") {
let xcodesRepoURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!
let xcodesRepoURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/")!
openURL(xcodesRepoURL)
}
Divider()
Button("Menu.ReportABug") {
let bugReportURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
let bugReportURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
openURL(bugReportURL)
}
Button("Menu.RequestNewFeature") {
let featureRequestURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
let featureRequestURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
openURL(featureRequestURL)
}
}
@ -166,12 +166,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
func applicationDidFinishLaunching(_: Notification) {}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
}
}
func localizeString(_ key: String, comment: String = "") -> String {
if #available(macOS 12, *) {
return String(localized: String.LocalizationValue(key))
} else {
return NSLocalizedString(key, comment: comment)
}
return String(localized: String.LocalizationValue(key))
}

View file

@ -11,7 +11,8 @@ public struct DownloadableRuntimesResponse: Codable {
public struct DownloadableRuntime: Codable, Identifiable, Hashable {
public let category: Category
public let simulatorVersion: SimulatorVersion
public let source: String
public let source: String?
public let architectures: [String]?
public let dictionaryVersion: Int
public let contentType: ContentType
public let platform: Platform
@ -21,16 +22,19 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
public let hostRequirements: HostRequirements?
public let name: String
public let authentication: Authentication?
public var url: URL {
return URL(string: source)!
public var url: URL? {
if let source {
return URL(string: source)!
}
return nil
}
public var downloadPath: String {
url.path
public var downloadPath: String? {
url?.path
}
// dynamically updated - not decoded
public var installState: RuntimeInstallState = .notInstalled
public var sdkBuildUpdate: String?
public var sdkBuildUpdate: [String]?
enum CodingKeys: CodingKey {
case category
@ -46,10 +50,11 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
case name
case authentication
case sdkBuildUpdate
case architectures
}
var betaNumber: Int? {
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+$") }
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+") }
guard var foundString = Regex.shared.firstString(in: identifier) else { return nil }
foundString.removeFirst()
return Int(foundString)!
@ -91,6 +96,7 @@ public struct SDKToSimulatorMapping: Codable {
public let sdkBuildUpdate: String
public let simulatorBuildUpdate: String
public let sdkIdentifier: String
public let downloadableIdentifiers: [String]?
}
extension DownloadableRuntime {
@ -117,6 +123,7 @@ extension DownloadableRuntime {
public enum ContentType: String, Codable {
case diskImage = "diskImage"
case package = "package"
case cryptexDiskImage = "cryptexDiskImage"
}
public enum Platform: String, Codable {

View file

@ -9,6 +9,7 @@ import Foundation
// A numbered step
public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
case authenticating
case downloading(progress: Progress)
case unarchiving
case moving(destination: String)
@ -22,6 +23,8 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
public var message: String {
switch self {
case .authenticating:
return localizeString("Authenticating")
case .downloading:
return localizeString("Downloading")
case .unarchiving:
@ -39,16 +42,17 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
public var stepNumber: Int {
switch self {
case .downloading: return 1
case .unarchiving: return 2
case .moving: return 3
case .trashingArchive: return 4
case .checkingSecurity: return 5
case .finishing: return 6
case .authenticating: return 1
case .downloading: return 2
case .unarchiving: return 3
case .moving: return 4
case .trashingArchive: return 5
case .checkingSecurity: return 6
case .finishing: return 7
}
}
public var stepCount: Int { 6 }
public var stepCount: Int { 7 }
}
func localizeString(_ key: String, comment: String = "") -> String {

View file

@ -22,9 +22,14 @@ public struct RuntimeService {
// Apple gives a plist for download
let (data, _) = try await networkService.requestData(urlRequest, validators: [])
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
return decodedResponse
do {
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
return decodedResponse
} catch {
print("error: \(error)")
throw error
}
}
public func installedRuntimes() async throws -> [InstalledRuntime] {

View file

@ -1,7 +1,11 @@
import Path
import CryptoKit
import Version
@testable import Xcodes
import XCTest
import CommonCrypto
import BigNum
import SRP
class AppStateUpdateTests: XCTestCase {
var subject: AppState!
@ -258,4 +262,81 @@ class AppStateUpdateTests: XCTestCase {
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0")!, Version("12.3.0-RC")!])
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[], []])
}
func sha256(data : Data) -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
return Data(hash)
}
func testSIRP() throws {
/*
Obtaind by running fastlane spaceauth --verbose -u anand.appleseed@example.com with some custom logging
Starting SIRP Apple ID login
a: {"a":"VLEKLa+n2cyeYNWbECm45CuS4kCdCxodlTDGlW1FKaUyOrv/RbtN2sM0pVE12zI7k3VkocPC3rN5DZBIkahR6I8JHj/J97dtTvzsR+aNRWTYCT2HGP1PBI0QArp3eitAbFqTWI4+Zw+oOnV8+AYdH/wjbq7gOK4C4dvIHE+FzRwIlmguPb5qu2r47R9W3y1msVdoUGlFBOMOMb7Gyq7F0MaEIFH63lNzGomwq74mfss/cFqurd6fxU+Y7tdVTPZw1GWyBEPiXWpk8sNm2zE+S6zWo5tOsICprU75IC9galh1igfzN7VNe0SUFLNFTbFK+Bb1SFAOrAbBZOmyOG5uSQ==","accountName":"anand.appleseed@example.com","protocols":["s2k","s2k_fo"]}
Received SIRP signin init response: {"iteration"=>20309, "salt"=>"fIjNflgqSJXACWwwvhDU+w==", "protocol"=>"s2k", "b"=>"PMbU75wwG6rDTySXn2ASWyfQuPoW5ham15SzIscpInwOPE2uk7sePsW4ra0dHcLDUMFQn/LgBggIKOo7YZ9hf1VReiAzXwSKSHdJHjHUURTC2eNpANGUPO1qzuXYgc/MP3MR+GipKHsz+KTLT+8wLjNaiCIHsL/7evJBMw9QqiwhyXlAIm5mGZfhdTVbGpLz2/QzrFmI6pUTrHpio6m1Q74DH3FBxxIeiIcuEdGdeVt9iUweowBRyf2woasTvSV1fbMQbl+lsWPwzt/a73+J30eOGFdSubqSVYh2pV0OLqRz7zPzJars12teCWUV+0WUIaxb14Mp7tlmqcTPuqZe9w==", "c"=>"d-533-eccbc4e9-9564-11ef-84a6-018111c8cc60:PRN"}
encrypted_password: 40532b4de9353fc537dc62ee84eacebd7ecb5ec26efca98bd01b0380e302100f 32
bb: 7672345903537871991962715758896796468138571329014139041563495295907370682045347022183702983061785424983278857706335295545994877883818377653653442007828499221881058994644619578239367613808278802931379172730746665773282250642455690715139985911303055104847971308813151718669484181874342088801251592138154023949370621963319928723678385968989085032385411532317797659749008135679504901238396934480214258070495365760319076978872181485178648397361564241555189629889320567561713407566532187413091018319494367244540399242523126294027225387432028960726767445027313453858210115810946641002311734776929442587065438110307439763191
x: 726436461883978586175291668001486484510457416477927591386889224605776454162
u: 49415306980415573732801389514223606278850554555635359953422678270536095422201
private 23161374166158551996079451276150657702422963034121842124445818241826569345033578345120284496449280736328513302994568402583647660370960353252836732307301957364261384324957527103960720408713825427474127658415917826326829664923997096839970397226662116904369925262192683131695683487505523842260218490007066160096482662715246662018133837725691586205535995663334471723776536640973591229093933458552240634178864509015968350855952558520147559154646484379002445961375384929682566525908284011230815908584648931495968206840416022306138033496705677078512266958633477047047323620540878121579549170392075029336954975132431417099801
S: 4f75b6ea99c2d7d121357cce80c75c8e1bf74a65e8f66f75f8f66a481301afb8bebf0e54a3fac4f8bfdd60c77d6e670c87968b341f62175e25eb1d4f496e4e6596e1a387f2840688a35002419b70115b7902a46544cc7b31eb4c909c0acaeb752835d1562a687c431421510ebc7535c007a2bd12a4f7696c8c96a75a491b1eb9189ade2bef23dd5b0bb962b4f03e7fba7f6ba6fe67ba34cc18647daf3e474876f85dac5a15eb51c99d1ed78783579ffd6c8b6911f72564d87dc8f76519c8fc1535b83743ed5f7d6b8461d7154ce2a874cbb45bf63018352b9b997fbafbd6b15eac2a544a801c0152470796f3b69a84a4a653e5186b30efeeb148ff3c32ab8e08
K: c5207f707186a52f1adee41bf0a7bc41e51e6dffc25cdaeca8acb7de2710b20a
hN: 65908899099613711898698321155848703477601840791750658211391687862255842366922
hG: 23094592799618609623465742609366149076596436609130823198107630312273622653270
hxor 73599884097654065452785151481733181870375477364472235597514429707329935690908
response: {"accountName":"anand.appleseed@example.com","c":"d-533-eccbc4e9-9564-11ef-84a6-018111c8cc60:PRN","m1":"f/Bkq8gBTYxl7SyiRd4SXTyE/jM/g6E0mVyZIQDIsPg=","m2":"R2rgqC9cMAtWiXUImOrvs4oF+ccibf8KaFsZQ22WokM=","rememberMe":false}
*/
let publicKey = Data(base64Encoded: "VLEKLa+n2cyeYNWbECm45CuS4kCdCxodlTDGlW1FKaUyOrv/RbtN2sM0pVE12zI7k3VkocPC3rN5DZBIkahR6I8JHj/J97dtTvzsR+aNRWTYCT2HGP1PBI0QArp3eitAbFqTWI4+Zw+oOnV8+AYdH/wjbq7gOK4C4dvIHE+FzRwIlmguPb5qu2r47R9W3y1msVdoUGlFBOMOMb7Gyq7F0MaEIFH63lNzGomwq74mfss/cFqurd6fxU+Y7tdVTPZw1GWyBEPiXWpk8sNm2zE+S6zWo5tOsICprU75IC9galh1igfzN7VNe0SUFLNFTbFK+Bb1SFAOrAbBZOmyOG5uSQ==".data(using: .utf8)!)
let clientKeys = SRPKeyPair(public: .init([UInt8](publicKey!)),
private: .init(BigNum("23161374166158551996079451276150657702422963034121842124445818241826569345033578345120284496449280736328513302994568402583647660370960353252836732307301957364261384324957527103960720408713825427474127658415917826326829664923997096839970397226662116904369925262192683131695683487505523842260218490007066160096482662715246662018133837725691586205535995663334471723776536640973591229093933458552240634178864509015968350855952558520147559154646484379002445961375384929682566525908284011230815908584648931495968206840416022306138033496705677078512266958633477047047323620540878121579549170392075029336954975132431417099801")!))
let password = sha256(data: "example".data(using: .utf8)!)
let salt = Data(base64Encoded: "fIjNflgqSJXACWwwvhDU+w==".data(using: .utf8)!)!
let iterations: Int = 20309
let derivedKeyLength: Int = 32
var keyArray = Array<UInt8>(repeating: 0, count: derivedKeyLength)
let result:Int32 = keyArray.withUnsafeMutableBytes { keyBytes -> Int32 in
let keyBuffer = UnsafeMutablePointer<UInt8>(keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
return password.withUnsafeBytes { passwordDigestBytes -> Int32 in
let passwordBuffer = UnsafePointer<UInt8>(passwordDigestBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
return salt.withUnsafeBytes { saltBytes -> Int32 in
let saltBuffer = UnsafePointer<UInt8>(saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
return CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
passwordBuffer,
password.count,
saltBuffer,
salt.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
UInt32(iterations),
keyBuffer,
derivedKeyLength)
}
}
}
let expectedKey: [UInt8] = [0x40, 0x53, 0x2b, 0x4d, 0xe9, 0x35, 0x3f, 0xc5, 0x37, 0xdc, 0x62, 0xee, 0x84, 0xea, 0xce, 0xbd, 0x7e, 0xcb, 0x5e, 0xc2, 0x6e, 0xfc, 0xa9, 0x8b, 0xd0, 0x1b, 0x03, 0x80, 0xe3, 0x02, 0x10, 0x0f]
XCTAssertEqual(expectedKey, keyArray)
let decodedB = Data(base64Encoded: "PMbU75wwG6rDTySXn2ASWyfQuPoW5ham15SzIscpInwOPE2uk7sePsW4ra0dHcLDUMFQn/LgBggIKOo7YZ9hf1VReiAzXwSKSHdJHjHUURTC2eNpANGUPO1qzuXYgc/MP3MR+GipKHsz+KTLT+8wLjNaiCIHsL/7evJBMw9QqiwhyXlAIm5mGZfhdTVbGpLz2/QzrFmI6pUTrHpio6m1Q74DH3FBxxIeiIcuEdGdeVt9iUweowBRyf2woasTvSV1fbMQbl+lsWPwzt/a73+J30eOGFdSubqSVYh2pV0OLqRz7zPzJars12teCWUV+0WUIaxb14Mp7tlmqcTPuqZe9w==".data(using: .utf8)!)!
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
let sharedSecret = try client.calculateSharedSecret(password: Data(keyArray), salt: [UInt8](salt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
let accountName = "anand.appleseed@example.com"
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](salt), clientPublicKey: clientKeys.public, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
let m2 = client.calculateServerProof(clientPublicKey: clientKeys.public, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
XCTAssertEqual(Data(m1).base64EncodedString(), "f/Bkq8gBTYxl7SyiRd4SXTyE/jM/g6E0mVyZIQDIsPg=")
XCTAssertEqual(Data(m2).base64EncodedString(), "R2rgqC9cMAtWiXUImOrvs4oF+ccibf8KaFsZQ22WokM=")
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 193 KiB