Prepare App Store 1.0 build 4
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -5041,7 +5041,7 @@ checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage"
|
name = "ironstorage"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"apple-native-keyring-store",
|
"apple-native-keyring-store",
|
||||||
"arboard",
|
"arboard",
|
||||||
@@ -5083,7 +5083,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage-apple"
|
name = "ironstorage-apple"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironstorage",
|
"ironstorage",
|
||||||
"uniffi",
|
"uniffi",
|
||||||
@@ -5091,7 +5091,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage-apple-release"
|
name = "ironstorage-apple-release"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -5101,7 +5101,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage-cli"
|
name = "ironstorage-cli"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"ironstorage",
|
"ironstorage",
|
||||||
@@ -5111,7 +5111,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage-desktop"
|
name = "ironstorage-desktop"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"iced",
|
"iced",
|
||||||
@@ -5125,7 +5125,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage-macos-packager"
|
name = "ironstorage-macos-packager"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"apple-bundles",
|
"apple-bundles",
|
||||||
"apple-codesign",
|
"apple-codesign",
|
||||||
@@ -5136,7 +5136,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage-tui"
|
name = "ironstorage-tui"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"ironstorage",
|
"ironstorage",
|
||||||
@@ -5147,7 +5147,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironstorage-watch-apple"
|
name = "ironstorage-watch-apple"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironstorage",
|
"ironstorage",
|
||||||
"uniffi",
|
"uniffi",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ members = [
|
|||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.92"
|
rust-version = "1.92"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ private diagnostics in the repository or release logs.
|
|||||||
## Release identity
|
## Release identity
|
||||||
|
|
||||||
- iPhone application: `de.rfc1437.ironstorage`
|
- iPhone application: `de.rfc1437.ironstorage`
|
||||||
- AutoFill extension: `de.rfc1437.ironstorage.autofill`
|
|
||||||
- Apple Watch companion: `de.rfc1437.ironstorage.watch`
|
- Apple Watch companion: `de.rfc1437.ironstorage.watch`
|
||||||
|
- Apple Watch compatibility: watchOS 26; the archive carries Apple's required
|
||||||
|
arm64 and arm64_32 slices
|
||||||
- Apple Developer team: `MU22FMRGK8`
|
- Apple Developer team: `MU22FMRGK8`
|
||||||
|
|
||||||
The submitted archive must embed the Watch application beneath the iPhone
|
The submitted archive must embed the Watch application beneath the iPhone
|
||||||
@@ -19,6 +20,7 @@ application. An iPhone-only archive is not a releasable IronStorage build.
|
|||||||
1. Generate the project and release attribution, then run the release verifier:
|
1. Generate the project and release attribution, then run the release verifier:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
rustup toolchain install nightly --component rust-src
|
||||||
cargo metadata --format-version 1 --filter-platform aarch64-apple-ios \
|
cargo metadata --format-version 1 --filter-platform aarch64-apple-ios \
|
||||||
> /private/tmp/ironstorage-ios-metadata.json
|
> /private/tmp/ironstorage-ios-metadata.json
|
||||||
cargo run -p ironstorage-apple-release -- licenses \
|
cargo run -p ironstorage-apple-release -- licenses \
|
||||||
@@ -38,10 +40,11 @@ cargo run -p ironstorage-apple-release -- verify .
|
|||||||
xcodegen generate
|
xcodegen generate
|
||||||
xcodebuild -project IronStorage.xcodeproj -scheme IronStorage \
|
xcodebuild -project IronStorage.xcodeproj -scheme IronStorage \
|
||||||
-configuration Release -destination 'generic/platform=iOS' \
|
-configuration Release -destination 'generic/platform=iOS' \
|
||||||
-archivePath /private/tmp/IronStorage.xcarchive archive
|
-archivePath /private/tmp/IronStorage.xcarchive \
|
||||||
|
-allowProvisioningUpdates archive
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Inspect the archive before upload. The iPhone, AutoFill, and Watch bundle
|
4. Inspect the archive before upload. The iPhone and Watch bundle
|
||||||
identifiers and versions must match the metadata; the Watch app must be
|
identifiers and versions must match the metadata; the Watch app must be
|
||||||
embedded; privacy manifests and license resources must be present; the
|
embedded; privacy manifests and license resources must be present; the
|
||||||
archive must use distribution signing and must not contain `get-task-allow`,
|
archive must use distribution signing and must not contain `get-task-allow`,
|
||||||
@@ -52,11 +55,13 @@ cargo run -p ironstorage-apple-release -- verify .
|
|||||||
xcodebuild -exportArchive \
|
xcodebuild -exportArchive \
|
||||||
-archivePath /private/tmp/IronStorage.xcarchive \
|
-archivePath /private/tmp/IronStorage.xcarchive \
|
||||||
-exportPath /private/tmp/IronStorage-AppStore \
|
-exportPath /private/tmp/IronStorage-AppStore \
|
||||||
-exportOptionsPlist AppStore/ExportOptions.plist
|
-exportOptionsPlist AppStore/ExportOptions.plist \
|
||||||
|
-allowProvisioningUpdates
|
||||||
```
|
```
|
||||||
|
|
||||||
6. In App Store Connect, complete the listing from `metadata.toml`, upload the
|
6. In App Store Connect, complete the listing from `metadata.toml`, upload the
|
||||||
reviewed screenshots, answer privacy and encryption questions, provide
|
reviewed required-size iPhone screenshots and Watch screenshots, answer
|
||||||
|
privacy and encryption questions, provide
|
||||||
local-first review instructions, select the processed build, and submit it
|
local-first review instructions, select the processed build, and submit it
|
||||||
for review. Do not provide a production password store or real credentials.
|
for review. Do not provide a production password store or real credentials.
|
||||||
7. After approval, install the public release from the App Store on the paired
|
7. After approval, install the public release from the App Store on the paired
|
||||||
|
|||||||
@@ -11,48 +11,51 @@ privacy_url = "https://rfc1437.de/apps/ironstorage/privacy/"
|
|||||||
support_url = "https://git.rfc1437.de/hugo/IronStorage/issues"
|
support_url = "https://git.rfc1437.de/hugo/IronStorage/issues"
|
||||||
marketing_url = "https://rfc1437.de/apps/ironstorage/"
|
marketing_url = "https://rfc1437.de/apps/ironstorage/"
|
||||||
age_rating = "4+"
|
age_rating = "4+"
|
||||||
|
copyright = "2026 rfc1437"
|
||||||
|
review_notes = "IronStorage works entirely on-device without an account or remote service. On first launch, generate a local GPG key and choose a passphrase. You can then create folders without unlocking, and create encrypted password or TOTP entries after entering that passphrase. Git history is initialized locally; HTTPS synchronization is optional and is not needed for review. Entries appear on Apple Watch only after selecting them for Watch sharing on iPhone."
|
||||||
|
|
||||||
[release]
|
[release]
|
||||||
version = "0.1.0"
|
version = "1.0"
|
||||||
build = "1"
|
build = "4"
|
||||||
minimum_ios = "17.0"
|
minimum_ios = "17.0"
|
||||||
|
minimum_watchos = "26.0"
|
||||||
notes = "Initial iPhone and Apple Watch release with local GPG key generation, pass-compatible local Git history, optional HTTPS synchronization, password folders, and explicitly shared TOTP codes on Apple Watch."
|
notes = "Initial iPhone and Apple Watch release with local GPG key generation, pass-compatible local Git history, optional HTTPS synchronization, password folders, and explicitly shared TOTP codes on Apple Watch."
|
||||||
|
|
||||||
[[screenshots]]
|
[[screenshots]]
|
||||||
file = "apple/AppStore/screenshots/home.png"
|
file = "apple/AppStore/screenshots/iphone-6.5/home.png"
|
||||||
caption = "Local Git history works before a sync remote is configured"
|
caption = "Local Git history works before a sync remote is configured"
|
||||||
width = 1206
|
width = 1284
|
||||||
height = 2622
|
height = 2778
|
||||||
|
|
||||||
[[screenshots]]
|
[[screenshots]]
|
||||||
file = "apple/AppStore/screenshots/passwords.png"
|
file = "apple/AppStore/screenshots/iphone-6.5/passwords.png"
|
||||||
caption = "Password entries and folders stay organized on device"
|
caption = "Password entries and folders stay organized on device"
|
||||||
width = 1206
|
width = 1284
|
||||||
height = 2622
|
height = 2778
|
||||||
|
|
||||||
[[screenshots]]
|
[[screenshots]]
|
||||||
file = "apple/AppStore/screenshots/totp.png"
|
file = "apple/AppStore/screenshots/iphone-6.5/totp.png"
|
||||||
caption = "TOTP entries are shared with Apple Watch only when selected"
|
caption = "TOTP entries are shared with Apple Watch only when selected"
|
||||||
width = 1206
|
width = 1284
|
||||||
height = 2622
|
height = 2778
|
||||||
|
|
||||||
[[screenshots]]
|
[[screenshots]]
|
||||||
file = "apple/AppStore/screenshots/totp-detail.png"
|
file = "apple/AppStore/screenshots/iphone-6.5/totp-detail.png"
|
||||||
caption = "Live TOTP codes include an exact validity indicator"
|
caption = "Live TOTP codes include an exact validity indicator"
|
||||||
width = 1206
|
width = 1284
|
||||||
height = 2622
|
height = 2778
|
||||||
|
|
||||||
[[screenshots]]
|
[[screenshots]]
|
||||||
file = "apple/AppStore/screenshots/preferences.png"
|
file = "apple/AppStore/screenshots/iphone-6.5/preferences.png"
|
||||||
caption = "Generate or import a GPG key and add optional HTTPS sync later"
|
caption = "Generate or import a GPG key and add optional HTTPS sync later"
|
||||||
width = 1206
|
width = 1284
|
||||||
height = 2622
|
height = 2778
|
||||||
|
|
||||||
[[screenshots]]
|
[[screenshots]]
|
||||||
file = "apple/AppStore/screenshots/search.png"
|
file = "apple/AppStore/screenshots/iphone-6.5/search.png"
|
||||||
caption = "Search encrypted password entries by name or folder"
|
caption = "Search encrypted password entries by name or folder"
|
||||||
width = 1206
|
width = 1284
|
||||||
height = 2622
|
height = 2778
|
||||||
|
|
||||||
[[watch_screenshots]]
|
[[watch_screenshots]]
|
||||||
file = "apple/AppStore/screenshots/watch-list.png"
|
file = "apple/AppStore/screenshots/watch-list.png"
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
<p>Last updated: 16 August 2026</p>
|
<p>Last updated: 16 August 2026</p>
|
||||||
<p>IronStorage does not collect analytics, advertising identifiers, diagnostics, or personal data for the developer, and it does not track you.</p>
|
<p>IronStorage does not collect analytics, advertising identifiers, diagnostics, or personal data for the developer, and it does not track you.</p>
|
||||||
<h2>Data on your device</h2>
|
<h2>Data on your device</h2>
|
||||||
<p>Your local Git repository and encrypted password entries remain on your device. Application tokens, imported or generated private-key material, and protected unlock data use Apple Keychain storage. Camera frames used to scan a key-transfer QR code are processed on the device and are not retained by IronStorage.</p>
|
<p>Your local Git repository and encrypted password entries remain on your device. Imported or generated private-key material stays in the app container and is protected by its GPG passphrase. Application tokens and biometric unlock data use Apple Keychain storage. Camera frames used to scan a key-transfer QR code are processed on the device and are not retained by IronStorage.</p>
|
||||||
<h2>Network access</h2>
|
<h2>Network access</h2>
|
||||||
<p>IronStorage works without a network service. If you later configure an HTTPS Git server, it connects only to that server using the credentials you provide to pull and push your password store. The developer does not operate an IronStorage service and does not receive this traffic.</p>
|
<p>IronStorage works without a network service. If you later configure an HTTPS Git server, it connects only to that server using the credentials you provide to pull and push your password store. The developer does not operate an IronStorage service and does not receive this traffic.</p>
|
||||||
<h2>Deletion</h2>
|
<h2>Deletion</h2>
|
||||||
<p>Remove configured credentials and key material in IronStorage before uninstalling when you want them deleted immediately. Keychain items otherwise follow Apple’s secure-storage lifecycle and may survive an app reinstall.</p>
|
<p>Deleting IronStorage removes its local repository and key files from the app container. Remove a configured application token and disable biometric unlock in Preferences before uninstalling when you also want those Keychain records removed immediately. Keychain items otherwise follow Apple’s secure-storage lifecycle and may survive an app reinstall.</p>
|
||||||
<h2>Support</h2>
|
<h2>Support</h2>
|
||||||
<p>Questions and issues can be filed at <a href="https://git.rfc1437.de/hugo/IronStorage/issues">the IronStorage issue tracker</a>.</p>
|
<p>Questions and issues can be filed at <a href="https://git.rfc1437.de/hugo/IronStorage/issues">the IronStorage issue tracker</a>.</p>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
BIN
apple/AppStore/screenshots/iphone-6.5/home.png
Normal file
BIN
apple/AppStore/screenshots/iphone-6.5/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 257 KiB |
BIN
apple/AppStore/screenshots/iphone-6.5/passwords.png
Normal file
BIN
apple/AppStore/screenshots/iphone-6.5/passwords.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
BIN
apple/AppStore/screenshots/iphone-6.5/preferences.png
Normal file
BIN
apple/AppStore/screenshots/iphone-6.5/preferences.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
BIN
apple/AppStore/screenshots/iphone-6.5/search.png
Normal file
BIN
apple/AppStore/screenshots/iphone-6.5/search.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
BIN
apple/AppStore/screenshots/iphone-6.5/totp-detail.png
Normal file
BIN
apple/AppStore/screenshots/iphone-6.5/totp-detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
BIN
apple/AppStore/screenshots/iphone-6.5/totp.png
Normal file
BIN
apple/AppStore/screenshots/iphone-6.5/totp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -99,11 +99,19 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
|
|||||||
snapshot.resetBytes(in: 0..<snapshot.count)
|
snapshot.resetBytes(in: 0..<snapshot.count)
|
||||||
transfer.snapshot.removeAll(keepingCapacity: false)
|
transfer.snapshot.removeAll(keepingCapacity: false)
|
||||||
}
|
}
|
||||||
|
let payload: [String: Any] = [
|
||||||
|
Self.snapshotKey: snapshot,
|
||||||
|
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
|
||||||
|
]
|
||||||
do {
|
do {
|
||||||
try session.updateApplicationContext([
|
try session.updateApplicationContext(payload)
|
||||||
Self.snapshotKey: snapshot,
|
if session.isReachable {
|
||||||
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
|
session.sendMessage(payload, replyHandler: nil) { _ in
|
||||||
])
|
session.transferUserInfo(payload)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.transferUserInfo(payload)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
_ = try? authentication.failWatchSnapshot(
|
_ = try? authentication.failWatchSnapshot(
|
||||||
revision: transfer.revision,
|
revision: transfer.revision,
|
||||||
@@ -5366,6 +5374,7 @@ private final class MobileEntryEditorViewController: UITableViewController,
|
|||||||
title = page.creating ? "New \(page.title)" : "Edit \(page.title)"
|
title = page.creating ? "New \(page.title)" : "Edit \(page.title)"
|
||||||
navigationItem.largeTitleDisplayMode = .never
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
tableView.register(MobileEntryEditorCell.self, forCellReuseIdentifier: "EditorField")
|
tableView.register(MobileEntryEditorCell.self, forCellReuseIdentifier: "EditorField")
|
||||||
|
tableView.keyboardDismissMode = .interactive
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.estimatedRowHeight = 120
|
tableView.estimatedRowHeight = 120
|
||||||
tableView.setEditing(true, animated: false)
|
tableView.setEditing(true, animated: false)
|
||||||
@@ -5373,7 +5382,7 @@ private final class MobileEntryEditorViewController: UITableViewController,
|
|||||||
systemItem: .cancel,
|
systemItem: .cancel,
|
||||||
primaryAction: UIAction { [weak self] _ in self?.cancelRequested() }
|
primaryAction: UIAction { [weak self] _ in self?.cancelRequested() }
|
||||||
)
|
)
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
let save = UIBarButtonItem(
|
||||||
systemItem: .save,
|
systemItem: .save,
|
||||||
primaryAction: UIAction { [weak self] _ in self?.saveRequested() }
|
primaryAction: UIAction { [weak self] _ in self?.saveRequested() }
|
||||||
)
|
)
|
||||||
@@ -5382,15 +5391,16 @@ private final class MobileEntryEditorViewController: UITableViewController,
|
|||||||
primaryAction: UIAction { [weak self] _ in self?.addRequested() }
|
primaryAction: UIAction { [weak self] _ in self?.addRequested() }
|
||||||
)
|
)
|
||||||
add.accessibilityLabel = "Add entry field"
|
add.accessibilityLabel = "Add entry field"
|
||||||
|
navigationItem.rightBarButtonItems = [save, add]
|
||||||
let generate = UIBarButtonItem(
|
let generate = UIBarButtonItem(
|
||||||
title: "Generate Password",
|
title: "Generate Password",
|
||||||
image: UIImage(systemName: "wand.and.stars"),
|
image: UIImage(systemName: "wand.and.stars"),
|
||||||
primaryAction: UIAction { [weak self] _ in self?.generateRequested() }
|
primaryAction: UIAction { [weak self] _ in self?.generateRequested() }
|
||||||
)
|
)
|
||||||
toolbarItems = [
|
toolbarItems = [
|
||||||
add,
|
|
||||||
UIBarButtonItem(systemItem: .flexibleSpace),
|
UIBarButtonItem(systemItem: .flexibleSpace),
|
||||||
generate,
|
generate,
|
||||||
|
UIBarButtonItem(systemItem: .flexibleSpace),
|
||||||
]
|
]
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
@@ -5783,7 +5793,7 @@ private final class MobileEntryEditorViewController: UITableViewController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private final class MobileEntryEditorCell: UITableViewCell, UITextViewDelegate {
|
private final class MobileEntryEditorCell: UITableViewCell, UITextFieldDelegate, UITextViewDelegate {
|
||||||
private let iconView = UIImageView()
|
private let iconView = UIImageView()
|
||||||
private let labelView = UILabel()
|
private let labelView = UILabel()
|
||||||
private let nameField = UITextField()
|
private let nameField = UITextField()
|
||||||
@@ -5808,6 +5818,8 @@ private final class MobileEntryEditorCell: UITableViewCell, UITextViewDelegate {
|
|||||||
field.font = .preferredFont(forTextStyle: .body)
|
field.font = .preferredFont(forTextStyle: .body)
|
||||||
field.adjustsFontForContentSizeCategory = true
|
field.adjustsFontForContentSizeCategory = true
|
||||||
field.clearButtonMode = .whileEditing
|
field.clearButtonMode = .whileEditing
|
||||||
|
field.delegate = self
|
||||||
|
field.returnKeyType = .done
|
||||||
field.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
|
field.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
|
||||||
}
|
}
|
||||||
nameField.placeholder = "Field Name"
|
nameField.placeholder = "Field Name"
|
||||||
@@ -5926,6 +5938,11 @@ private final class MobileEntryEditorCell: UITableViewCell, UITextViewDelegate {
|
|||||||
changed?(currentName, currentValue)
|
changed?(currentName, currentValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
textField.resignFirstResponder()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
currentValue = textView.text
|
currentValue = textView.text
|
||||||
changed?(currentName, currentValue)
|
changed?(currentName, currentValue)
|
||||||
|
|||||||
@@ -172,6 +172,18 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
|
|||||||
self?.receive(applicationContext, session: session)
|
self?.receive(applicationContext, session: session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
self?.receive(message, session: session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
self?.receive(userInfo, session: session)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct WatchRootView: View {
|
private struct WatchRootView: View {
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
export CARGO_TARGET_DIR="$DERIVED_FILE_DIR/cargo"
|
export CARGO_TARGET_DIR="$DERIVED_FILE_DIR/cargo"
|
||||||
export RUSTC="$(rustup which --toolchain stable rustc)"
|
|
||||||
export RUSTDOC="$(rustup which --toolchain stable rustdoc)"
|
|
||||||
|
|
||||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||||
profile=debug
|
profile=debug
|
||||||
else
|
else
|
||||||
@@ -21,10 +18,16 @@ fi
|
|||||||
|
|
||||||
libraries=()
|
libraries=()
|
||||||
for arch in $ARCHS; do
|
for arch in $ARCHS; do
|
||||||
|
toolchain=stable
|
||||||
|
cargo_args=(--locked)
|
||||||
if [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
|
if [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
|
||||||
target=aarch64-apple-watchos-sim
|
target=aarch64-apple-watchos-sim
|
||||||
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" ]]; then
|
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" ]]; then
|
||||||
target=aarch64-apple-watchos
|
target=aarch64-apple-watchos
|
||||||
|
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64_32" ]]; then
|
||||||
|
target=arm64_32-apple-watchos
|
||||||
|
toolchain=nightly
|
||||||
|
cargo_args=(-Z build-std=std,panic_abort --locked)
|
||||||
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "x86_64" ]]; then
|
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "x86_64" ]]; then
|
||||||
target=x86_64-apple-watchos-sim
|
target=x86_64-apple-watchos-sim
|
||||||
elif [[ "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
|
elif [[ "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
|
||||||
@@ -38,11 +41,13 @@ for arch in $ARCHS; do
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export RUSTC="$(rustup which --toolchain "$toolchain" rustc)"
|
||||||
|
export RUSTDOC="$(rustup which --toolchain "$toolchain" rustdoc)"
|
||||||
if [[ "$profile" == "debug" ]]; then
|
if [[ "$profile" == "debug" ]]; then
|
||||||
rustup run stable cargo build --locked \
|
rustup run "$toolchain" cargo build "${cargo_args[@]}" \
|
||||||
--target "$target" --package "$package" --lib
|
--target "$target" --package "$package" --lib
|
||||||
else
|
else
|
||||||
rustup run stable cargo build --locked --release \
|
rustup run "$toolchain" cargo build "${cargo_args[@]}" --release \
|
||||||
--target "$target" --package "$package" --lib
|
--target "$target" --package "$package" --lib
|
||||||
fi
|
fi
|
||||||
libraries+=("$CARGO_TARGET_DIR/$target/$profile/lib$library.a")
|
libraries+=("$CARGO_TARGET_DIR/$target/$profile/lib$library.a")
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ settings:
|
|||||||
SWIFT_VERSION: "5.0"
|
SWIFT_VERSION: "5.0"
|
||||||
CODE_SIGN_STYLE: Automatic
|
CODE_SIGN_STYLE: Automatic
|
||||||
DEVELOPMENT_TEAM: MU22FMRGK8
|
DEVELOPMENT_TEAM: MU22FMRGK8
|
||||||
MARKETING_VERSION: "0.1.0"
|
MARKETING_VERSION: "1.0"
|
||||||
CURRENT_PROJECT_VERSION: "1"
|
CURRENT_PROJECT_VERSION: "4"
|
||||||
targets:
|
targets:
|
||||||
IronStorage:
|
IronStorage:
|
||||||
type: application
|
type: application
|
||||||
@@ -39,8 +39,6 @@ targets:
|
|||||||
- Sources/App
|
- Sources/App
|
||||||
- Generated/ironstorage_apple.swift
|
- Generated/ironstorage_apple.swift
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: IronStorageAutoFill
|
|
||||||
embed: true
|
|
||||||
- target: IronStorageWatch
|
- target: IronStorageWatch
|
||||||
embed: true
|
embed: true
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
@@ -85,11 +83,12 @@ targets:
|
|||||||
IronStorageWatch:
|
IronStorageWatch:
|
||||||
type: application
|
type: application
|
||||||
platform: watchOS
|
platform: watchOS
|
||||||
deploymentTarget: "10.0"
|
deploymentTarget: "26.0"
|
||||||
settings:
|
settings:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch
|
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch
|
||||||
PRODUCT_NAME: IronStorage Watch
|
PRODUCT_NAME: IronStorage Watch
|
||||||
SKIP_INSTALL: YES
|
SKIP_INSTALL: YES
|
||||||
|
"ARCHS[sdk=watchos*]": "arm64 arm64_32"
|
||||||
"EXCLUDED_ARCHS[sdk=watchsimulator*]": x86_64
|
"EXCLUDED_ARCHS[sdk=watchsimulator*]": x86_64
|
||||||
SWIFT_OBJC_BRIDGING_HEADER: IronStorage-Watch-Bridging-Header.h
|
SWIFT_OBJC_BRIDGING_HEADER: IronStorage-Watch-Bridging-Header.h
|
||||||
LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust"
|
LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust"
|
||||||
|
|||||||
@@ -229,7 +229,10 @@ impl WatchSnapshotReceiver {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut pairing_changed = false;
|
let mut pairing_changed = false;
|
||||||
if let Some(accepted) = self.accepted {
|
// A valid revocation can only remove secrets, so it must survive a sender reset.
|
||||||
|
if !snapshot.is_revocation()
|
||||||
|
&& let Some(accepted) = self.accepted
|
||||||
|
{
|
||||||
if snapshot.revision < accepted.revision {
|
if snapshot.revision < accepted.revision {
|
||||||
return Ok(WatchSnapshotApply::Stale);
|
return Ok(WatchSnapshotApply::Stale);
|
||||||
} else if snapshot.revision == accepted.revision {
|
} else if snapshot.revision == accepted.revision {
|
||||||
@@ -249,10 +252,10 @@ impl WatchSnapshotReceiver {
|
|||||||
revision: snapshot.revision,
|
revision: snapshot.revision,
|
||||||
digest: snapshot.digest,
|
digest: snapshot.digest,
|
||||||
};
|
};
|
||||||
let result = if pairing_changed {
|
let result = if snapshot.is_revocation() {
|
||||||
WatchSnapshotApply::PairingChanged
|
|
||||||
} else if snapshot.is_revocation() {
|
|
||||||
WatchSnapshotApply::Revoked
|
WatchSnapshotApply::Revoked
|
||||||
|
} else if pairing_changed {
|
||||||
|
WatchSnapshotApply::PairingChanged
|
||||||
} else {
|
} else {
|
||||||
WatchSnapshotApply::Replaced
|
WatchSnapshotApply::Replaced
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ fn apple_sources_preserve_the_mobile_security_boundary() {
|
|||||||
}
|
}
|
||||||
assert!(watch.contains("core.presentationAt"));
|
assert!(watch.contains("core.presentationAt"));
|
||||||
assert!(watch.contains("SecItemCopyMatching"));
|
assert!(watch.contains("SecItemCopyMatching"));
|
||||||
|
assert!(app.contains("session.sendMessage(payload"));
|
||||||
|
assert!(app.contains("session.transferUserInfo(payload)"));
|
||||||
|
assert!(watch.contains("didReceiveMessage message"));
|
||||||
|
assert!(watch.contains("didReceiveUserInfo userInfo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iphone_entry_editor_keeps_add_action_accessible_while_typing() {
|
||||||
|
let app = workspace_file("apple/Sources/App/IronStorageApp.swift");
|
||||||
|
assert!(app.contains("navigationItem.rightBarButtonItems = [save, add]"));
|
||||||
|
assert!(app.contains("tableView.keyboardDismissMode = .interactive"));
|
||||||
|
assert!(app.contains("field.returnKeyType = .done"));
|
||||||
|
assert!(app.contains("textField.resignFirstResponder()"));
|
||||||
|
assert!(!app.contains("inputAccessoryView = keyboardToolbar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -104,6 +104,32 @@ fn replacement_snapshots_reject_replays_conflicts_and_pairing_changes() -> TestR
|
|||||||
WatchSnapshotApply::Stale
|
WatchSnapshotApply::Stale
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let reset_journal = directory.path().join("reset-watch-snapshot.toml");
|
||||||
|
let mut reset_sender = WatchSnapshotSender::load(reset_journal);
|
||||||
|
let reset_revocation = reset_sender.prepare("paired-watch-a", Vec::new())?;
|
||||||
|
assert_eq!(reset_revocation.revision(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(
|
||||||
|
reset_revocation.snapshot().expose().to_vec()
|
||||||
|
))?,
|
||||||
|
WatchSnapshotApply::Revoked
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
receiver
|
||||||
|
.current()
|
||||||
|
.expect("reset revocation")
|
||||||
|
.is_revocation()
|
||||||
|
);
|
||||||
|
let after_reset = reset_sender.prepare(
|
||||||
|
"paired-watch-a",
|
||||||
|
vec![entry("otp/reset", "Acme", "reset", b"reset-secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(after_reset.revision(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(after_reset.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Replaced
|
||||||
|
);
|
||||||
|
|
||||||
let changed_watch = sender.prepare(
|
let changed_watch = sender.prepare(
|
||||||
"paired-watch-b",
|
"paired-watch-b",
|
||||||
vec![entry("otp/carol", "Acme", "carol", b"third-secret")],
|
vec![entry("otp/carol", "Acme", "carol", b"third-secret")],
|
||||||
@@ -138,6 +164,7 @@ fn replacement_snapshots_reject_replays_conflicts_and_pairing_changes() -> TestR
|
|||||||
"first-secret",
|
"first-secret",
|
||||||
"second-secret",
|
"second-secret",
|
||||||
"third-secret",
|
"third-secret",
|
||||||
|
"reset-secret",
|
||||||
"otpauth://",
|
"otpauth://",
|
||||||
"94287082",
|
"94287082",
|
||||||
] {
|
] {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ struct App {
|
|||||||
support_url: String,
|
support_url: String,
|
||||||
marketing_url: String,
|
marketing_url: String,
|
||||||
age_rating: String,
|
age_rating: String,
|
||||||
|
copyright: String,
|
||||||
|
review_notes: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -42,6 +44,7 @@ struct Release {
|
|||||||
version: String,
|
version: String,
|
||||||
build: String,
|
build: String,
|
||||||
minimum_ios: String,
|
minimum_ios: String,
|
||||||
|
minimum_watchos: String,
|
||||||
notes: String,
|
notes: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +116,12 @@ fn verify(root: &Path) -> Result<(), Box<dyn Error>> {
|
|||||||
if metadata.app.bundle_identifier != BUNDLE_ID {
|
if metadata.app.bundle_identifier != BUNDLE_ID {
|
||||||
return Err(format!("main bundle identifier must be {BUNDLE_ID}").into());
|
return Err(format!("main bundle identifier must be {BUNDLE_ID}").into());
|
||||||
}
|
}
|
||||||
if metadata.app.age_rating != "4+" || metadata.release.minimum_ios != "17.0" {
|
if metadata.app.age_rating != "4+"
|
||||||
|
|| metadata.release.minimum_ios != "17.0"
|
||||||
|
|| metadata.release.minimum_watchos != "26.0"
|
||||||
|
{
|
||||||
return Err(
|
return Err(
|
||||||
"release metadata must retain the reviewed 4+ rating and iOS 17.0 minimum".into(),
|
"release metadata must retain the reviewed 4+ rating, iOS 17.0 minimum, and watchOS 26.0 minimum".into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if metadata.release.build.parse::<u64>()? == 0 {
|
if metadata.release.build.parse::<u64>()? == 0 {
|
||||||
@@ -135,6 +141,8 @@ fn verify(root: &Path) -> Result<(), Box<dyn Error>> {
|
|||||||
("keywords", metadata.app.keywords.as_str()),
|
("keywords", metadata.app.keywords.as_str()),
|
||||||
("category", metadata.app.category.as_str()),
|
("category", metadata.app.category.as_str()),
|
||||||
("release notes", metadata.release.notes.as_str()),
|
("release notes", metadata.release.notes.as_str()),
|
||||||
|
("copyright", metadata.app.copyright.as_str()),
|
||||||
|
("review notes", metadata.app.review_notes.as_str()),
|
||||||
] {
|
] {
|
||||||
if value.trim().is_empty() {
|
if value.trim().is_empty() {
|
||||||
return Err(format!("{name} must not be empty").into());
|
return Err(format!("{name} must not be empty").into());
|
||||||
@@ -156,20 +164,20 @@ fn verify(root: &Path) -> Result<(), Box<dyn Error>> {
|
|||||||
let workspace_version = workspace["workspace"]["package"]["version"]
|
let workspace_version = workspace["workspace"]["package"]["version"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or("workspace version is missing")?;
|
.ok_or("workspace version is missing")?;
|
||||||
if metadata.release.version != workspace_version {
|
if normalized_version(&metadata.release.version)? != normalized_version(workspace_version)? {
|
||||||
return Err("App Store version must match the Rust workspace version".into());
|
return Err("App Store version must match the Rust workspace version".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let project = fs::read_to_string(root.join("apple/project.yml"))?;
|
let project = fs::read_to_string(root.join("apple/project.yml"))?;
|
||||||
for required in [
|
for required in [
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage\n",
|
"PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage\n",
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.autofill\n",
|
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch\n",
|
"PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch\n",
|
||||||
"DEVELOPMENT_TEAM: MU22FMRGK8\n",
|
"DEVELOPMENT_TEAM: MU22FMRGK8\n",
|
||||||
"ITSAppUsesNonExemptEncryption: false\n",
|
"ITSAppUsesNonExemptEncryption: false\n",
|
||||||
"NSFaceIDUsageDescription:",
|
"NSFaceIDUsageDescription:",
|
||||||
"NSCameraUsageDescription:",
|
"NSCameraUsageDescription:",
|
||||||
"- target: IronStorageAutoFill\n embed: true",
|
"deploymentTarget: \"26.0\"\n",
|
||||||
|
"\"ARCHS[sdk=watchos*]\": \"arm64 arm64_32\"\n",
|
||||||
"- target: IronStorageWatch\n embed: true",
|
"- target: IronStorageWatch\n embed: true",
|
||||||
] {
|
] {
|
||||||
if !project.contains(required) {
|
if !project.contains(required) {
|
||||||
@@ -203,6 +211,25 @@ fn verify(root: &Path) -> Result<(), Box<dyn Error>> {
|
|||||||
if metadata.screenshots.len() != 6 {
|
if metadata.screenshots.len() != 6 {
|
||||||
return Err("exactly six reviewed iPhone screenshots are required".into());
|
return Err("exactly six reviewed iPhone screenshots are required".into());
|
||||||
}
|
}
|
||||||
|
let iphone_size = (
|
||||||
|
metadata.screenshots[0].width,
|
||||||
|
metadata.screenshots[0].height,
|
||||||
|
);
|
||||||
|
if ![
|
||||||
|
(1242, 2688),
|
||||||
|
(1284, 2778),
|
||||||
|
(1260, 2736),
|
||||||
|
(1290, 2796),
|
||||||
|
(1320, 2868),
|
||||||
|
]
|
||||||
|
.contains(&iphone_size)
|
||||||
|
|| metadata
|
||||||
|
.screenshots
|
||||||
|
.iter()
|
||||||
|
.any(|screenshot| (screenshot.width, screenshot.height) != iphone_size)
|
||||||
|
{
|
||||||
|
return Err("iPhone screenshots must use one required 6.5-inch or 6.9-inch size".into());
|
||||||
|
}
|
||||||
if metadata.watch_screenshots.len() != 2 {
|
if metadata.watch_screenshots.len() != 2 {
|
||||||
return Err("exactly the reviewed Watch list and detail screenshots are required".into());
|
return Err("exactly the reviewed Watch list and detail screenshots are required".into());
|
||||||
}
|
}
|
||||||
@@ -357,6 +384,17 @@ fn require_https(name: &str, value: &str) -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalized_version(value: &str) -> Result<Vec<u64>, Box<dyn Error>> {
|
||||||
|
let mut parts = value
|
||||||
|
.split('.')
|
||||||
|
.map(str::parse)
|
||||||
|
.collect::<Result<Vec<u64>, _>>()?;
|
||||||
|
while parts.last() == Some(&0) {
|
||||||
|
parts.pop();
|
||||||
|
}
|
||||||
|
Ok(parts)
|
||||||
|
}
|
||||||
|
|
||||||
fn png_size(path: &Path) -> Result<(u32, u32), Box<dyn Error>> {
|
fn png_size(path: &Path) -> Result<(u32, u32), Box<dyn Error>> {
|
||||||
let bytes = fs::read(path)?;
|
let bytes = fs::read(path)?;
|
||||||
if bytes.len() < 24 || &bytes[..8] != b"\x89PNG\r\n\x1a\n" || &bytes[12..16] != b"IHDR" {
|
if bytes.len() < 24 || &bytes[..8] != b"\x89PNG\r\n\x1a\n" || &bytes[12..16] != b"IHDR" {
|
||||||
@@ -383,6 +421,12 @@ mod tests {
|
|||||||
assert!(metadata.release.notes.contains("Apple Watch"));
|
assert!(metadata.release.notes.contains("Apple Watch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_store_and_rust_versions_ignore_trailing_zeroes() {
|
||||||
|
assert_eq!(normalized_version("1.0").unwrap(), vec![1]);
|
||||||
|
assert_eq!(normalized_version("1.0.0").unwrap(), vec![1]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repository_release_metadata_is_consistent() {
|
fn repository_release_metadata_is_consistent() {
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
|||||||
Reference in New Issue
Block a user