Prepare App Store 1.0 build 4

This commit is contained in:
2026-08-17 16:50:30 +02:00
parent f894d43eb6
commit 0b7c938b23
19 changed files with 191 additions and 62 deletions

View File

@@ -7,8 +7,9 @@ private diagnostics in the repository or release logs.
## Release identity
- iPhone application: `de.rfc1437.ironstorage`
- AutoFill extension: `de.rfc1437.ironstorage.autofill`
- 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`
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:
```sh
rustup toolchain install nightly --component rust-src
cargo metadata --format-version 1 --filter-platform aarch64-apple-ios \
> /private/tmp/ironstorage-ios-metadata.json
cargo run -p ironstorage-apple-release -- licenses \
@@ -38,10 +40,11 @@ cargo run -p ironstorage-apple-release -- verify .
xcodegen generate
xcodebuild -project IronStorage.xcodeproj -scheme IronStorage \
-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
embedded; privacy manifests and license resources must be present; the
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 \
-archivePath /private/tmp/IronStorage.xcarchive \
-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
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
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

View File

@@ -11,48 +11,51 @@ privacy_url = "https://rfc1437.de/apps/ironstorage/privacy/"
support_url = "https://git.rfc1437.de/hugo/IronStorage/issues"
marketing_url = "https://rfc1437.de/apps/ironstorage/"
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]
version = "0.1.0"
build = "1"
version = "1.0"
build = "4"
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."
[[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"
width = 1206
height = 2622
width = 1284
height = 2778
[[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"
width = 1206
height = 2622
width = 1284
height = 2778
[[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"
width = 1206
height = 2622
width = 1284
height = 2778
[[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"
width = 1206
height = 2622
width = 1284
height = 2778
[[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"
width = 1206
height = 2622
width = 1284
height = 2778
[[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"
width = 1206
height = 2622
width = 1284
height = 2778
[[watch_screenshots]]
file = "apple/AppStore/screenshots/watch-list.png"

View File

@@ -8,11 +8,11 @@
<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>
<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>
<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>
<p>Remove configured credentials and key material in IronStorage before uninstalling when you want them deleted immediately. Keychain items otherwise follow Apples 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 Apples secure-storage lifecycle and may survive an app reinstall.</p>
<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>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -99,11 +99,19 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
snapshot.resetBytes(in: 0..<snapshot.count)
transfer.snapshot.removeAll(keepingCapacity: false)
}
let payload: [String: Any] = [
Self.snapshotKey: snapshot,
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
]
do {
try session.updateApplicationContext([
Self.snapshotKey: snapshot,
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
])
try session.updateApplicationContext(payload)
if session.isReachable {
session.sendMessage(payload, replyHandler: nil) { _ in
session.transferUserInfo(payload)
}
} else {
session.transferUserInfo(payload)
}
} catch {
_ = try? authentication.failWatchSnapshot(
revision: transfer.revision,
@@ -5366,6 +5374,7 @@ private final class MobileEntryEditorViewController: UITableViewController,
title = page.creating ? "New \(page.title)" : "Edit \(page.title)"
navigationItem.largeTitleDisplayMode = .never
tableView.register(MobileEntryEditorCell.self, forCellReuseIdentifier: "EditorField")
tableView.keyboardDismissMode = .interactive
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 120
tableView.setEditing(true, animated: false)
@@ -5373,7 +5382,7 @@ private final class MobileEntryEditorViewController: UITableViewController,
systemItem: .cancel,
primaryAction: UIAction { [weak self] _ in self?.cancelRequested() }
)
navigationItem.rightBarButtonItem = UIBarButtonItem(
let save = UIBarButtonItem(
systemItem: .save,
primaryAction: UIAction { [weak self] _ in self?.saveRequested() }
)
@@ -5382,15 +5391,16 @@ private final class MobileEntryEditorViewController: UITableViewController,
primaryAction: UIAction { [weak self] _ in self?.addRequested() }
)
add.accessibilityLabel = "Add entry field"
navigationItem.rightBarButtonItems = [save, add]
let generate = UIBarButtonItem(
title: "Generate Password",
image: UIImage(systemName: "wand.and.stars"),
primaryAction: UIAction { [weak self] _ in self?.generateRequested() }
)
toolbarItems = [
add,
UIBarButtonItem(systemItem: .flexibleSpace),
generate,
UIBarButtonItem(systemItem: .flexibleSpace),
]
NotificationCenter.default.addObserver(
self,
@@ -5783,7 +5793,7 @@ private final class MobileEntryEditorViewController: UITableViewController,
}
@MainActor
private final class MobileEntryEditorCell: UITableViewCell, UITextViewDelegate {
private final class MobileEntryEditorCell: UITableViewCell, UITextFieldDelegate, UITextViewDelegate {
private let iconView = UIImageView()
private let labelView = UILabel()
private let nameField = UITextField()
@@ -5808,6 +5818,8 @@ private final class MobileEntryEditorCell: UITableViewCell, UITextViewDelegate {
field.font = .preferredFont(forTextStyle: .body)
field.adjustsFontForContentSizeCategory = true
field.clearButtonMode = .whileEditing
field.delegate = self
field.returnKeyType = .done
field.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
}
nameField.placeholder = "Field Name"
@@ -5926,6 +5938,11 @@ private final class MobileEntryEditorCell: UITableViewCell, UITextViewDelegate {
changed?(currentName, currentValue)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textViewDidChange(_ textView: UITextView) {
currentValue = textView.text
changed?(currentName, currentValue)

View File

@@ -172,6 +172,18 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
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 {

View File

@@ -2,9 +2,6 @@
set -euo pipefail
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
profile=debug
else
@@ -21,10 +18,16 @@ fi
libraries=()
for arch in $ARCHS; do
toolchain=stable
cargo_args=(--locked)
if [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
target=aarch64-apple-watchos-sim
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" ]]; then
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
target=x86_64-apple-watchos-sim
elif [[ "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
@@ -38,11 +41,13 @@ for arch in $ARCHS; do
exit 1
fi
export RUSTC="$(rustup which --toolchain "$toolchain" rustc)"
export RUSTDOC="$(rustup which --toolchain "$toolchain" rustdoc)"
if [[ "$profile" == "debug" ]]; then
rustup run stable cargo build --locked \
rustup run "$toolchain" cargo build "${cargo_args[@]}" \
--target "$target" --package "$package" --lib
else
rustup run stable cargo build --locked --release \
rustup run "$toolchain" cargo build "${cargo_args[@]}" --release \
--target "$target" --package "$package" --lib
fi
libraries+=("$CARGO_TARGET_DIR/$target/$profile/lib$library.a")

View File

@@ -6,8 +6,8 @@ settings:
SWIFT_VERSION: "5.0"
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: MU22FMRGK8
MARKETING_VERSION: "0.1.0"
CURRENT_PROJECT_VERSION: "1"
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "4"
targets:
IronStorage:
type: application
@@ -39,8 +39,6 @@ targets:
- Sources/App
- Generated/ironstorage_apple.swift
dependencies:
- target: IronStorageAutoFill
embed: true
- target: IronStorageWatch
embed: true
preBuildScripts:
@@ -85,11 +83,12 @@ targets:
IronStorageWatch:
type: application
platform: watchOS
deploymentTarget: "10.0"
deploymentTarget: "26.0"
settings:
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch
PRODUCT_NAME: IronStorage Watch
SKIP_INSTALL: YES
"ARCHS[sdk=watchos*]": "arm64 arm64_32"
"EXCLUDED_ARCHS[sdk=watchsimulator*]": x86_64
SWIFT_OBJC_BRIDGING_HEADER: IronStorage-Watch-Bridging-Header.h
LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust"