Prepare Apple App Store distribution (#59)
69
apple/AppStore/DISTRIBUTION.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Apple App Store distribution
|
||||
|
||||
Use `distribution/` only as ignored local staging. Never store Apple credentials,
|
||||
signing material, review credentials, GPG material, application tokens, or
|
||||
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 Developer team: `MU22FMRGK8`
|
||||
|
||||
The submitted archive must embed the Watch application beneath the iPhone
|
||||
application. An iPhone-only archive is not a releasable IronStorage build.
|
||||
|
||||
## Prepare and submit
|
||||
|
||||
1. Generate the project and release attribution, then run the release verifier:
|
||||
|
||||
```sh
|
||||
cargo metadata --format-version 1 --filter-platform aarch64-apple-ios \
|
||||
> /private/tmp/ironstorage-ios-metadata.json
|
||||
cargo run -p ironstorage-apple-release -- licenses \
|
||||
/private/tmp/ironstorage-ios-metadata.json \
|
||||
apple/Resources/App/ThirdPartyLicenses.txt
|
||||
cargo run -p ironstorage-apple-release -- verify .
|
||||
```
|
||||
|
||||
2. Run the repository gates and build both simulator applications. Validate the
|
||||
local-first flow with synthetic data: generate or import a GPG key, create
|
||||
folders and encrypted entries, read TOTP codes, select Watch entries, and
|
||||
verify the Watch list and selected-code progress display.
|
||||
3. Create the production archive with normal App Store distribution signing:
|
||||
|
||||
```sh
|
||||
cd apple
|
||||
xcodegen generate
|
||||
xcodebuild -project IronStorage.xcodeproj -scheme IronStorage \
|
||||
-configuration Release -destination 'generic/platform=iOS' \
|
||||
-archivePath /private/tmp/IronStorage.xcarchive archive
|
||||
```
|
||||
|
||||
4. Inspect the archive before upload. The iPhone, AutoFill, 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`,
|
||||
simulator artifacts, synthetic demo data, private endpoints, or secrets.
|
||||
5. Export and upload with the checked-in App Store Connect export options:
|
||||
|
||||
```sh
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath /private/tmp/IronStorage.xcarchive \
|
||||
-exportPath /private/tmp/IronStorage-AppStore \
|
||||
-exportOptionsPlist AppStore/ExportOptions.plist
|
||||
```
|
||||
|
||||
6. In App Store Connect, complete the listing from `metadata.toml`, upload the
|
||||
reviewed 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
|
||||
iPhone and Apple Watch. Verify local onboarding, key overwrite confirmation,
|
||||
local Git history, folders, encrypted entries, Face ID, TOTP, optional HTTPS
|
||||
pull and push, Watch snapshot delivery, the Watch entry-only list, and the
|
||||
larger selected code with its validity progress indicator.
|
||||
8. Verify an App Store update in place. Verify the documented uninstall and
|
||||
reinstall Keychain lifecycle only with disposable test data; never erase the
|
||||
paired physical device or its unrelated IronStorage data.
|
||||
20
apple/AppStore/ExportOptions.plist
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>MU22FMRGK8</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
52
apple/AppStore/apps-index.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>rfc1437 Apps</title>
|
||||
<link rel="stylesheet" href="/assets/pico.pumpkin.min.css">
|
||||
<link rel="stylesheet" href="/assets/bds.css">
|
||||
<style>.app-header{display:flex;align-items:center;gap:1rem}.app-header img{width:5rem;height:5rem;border-radius:1.1rem}.gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem}figure{margin:0}figure img{display:block;width:100%;height:auto;border-radius:1.5rem}figcaption{margin-top:.5rem;color:var(--pico-muted-color)}</style>
|
||||
</head>
|
||||
<body><main>
|
||||
<header><h1 class="archive-heading">rfc1437 Apps</h1><p>Small, focused apps published by <a href="https://rfc1437.de/">rfc1437</a>.</p></header>
|
||||
<nav class="blog-menu" aria-label="Site navigation"><ul class="blog-menu-list"><li class="blog-menu-item"><a class="blog-menu-link" href="https://rfc1437.de/">Home</a></li><li class="blog-menu-item"><span class="blog-menu-link" aria-current="page">Apps</span></li></ul></nav>
|
||||
<article class="post">
|
||||
<h2>Install</h2>
|
||||
<p>Gotcha is available through the rfc1437 AltStore PAL source at <code>https://rfc1437.de/apps/source.json</code>.</p>
|
||||
<p>IronStorage is being prepared for the Apple App Store so its iPhone app and Apple Watch companion install together through the supported App Store path.</p>
|
||||
</article>
|
||||
<section><h2>Apps</h2>
|
||||
<article class="post">
|
||||
<header class="app-header"><img src="gotcha/icon.png" alt="Gotcha app icon"><div><h3>Gotcha</h3><p>Native Gitea and Forgejo on iPhone.</p></div></header>
|
||||
<p>Follow repositories, issues, pull requests, commits, diffs, and milestones on servers you control.</p>
|
||||
<nav><a href="gotcha/privacy/">Privacy</a> · <a href="https://git.rfc1437.de/hugo/Gotcha">Source</a> · <a href="https://git.rfc1437.de/hugo/Gotcha/issues">Support</a></nav>
|
||||
<div class="gallery">
|
||||
<figure><img src="gotcha/screenshots/01-home-activity.png" alt="Gotcha activity"><figcaption>Activity</figcaption></figure>
|
||||
<figure><img src="gotcha/screenshots/02-issues.png" alt="Gotcha issues"><figcaption>Issues</figcaption></figure>
|
||||
<figure><img src="gotcha/screenshots/03-commit-history.png" alt="Gotcha commits"><figcaption>Commits</figcaption></figure>
|
||||
<figure><img src="gotcha/screenshots/04-diff.png" alt="Gotcha diff"><figcaption>Diffs</figcaption></figure>
|
||||
<figure><img src="gotcha/screenshots/05-pull-request.png" alt="Gotcha pull request"><figcaption>Pull requests</figcaption></figure>
|
||||
<figure><img src="gotcha/screenshots/06-milestones.png" alt="Gotcha milestones"><figcaption>Milestones</figcaption></figure>
|
||||
</div>
|
||||
</article>
|
||||
<article class="post">
|
||||
<header class="app-header"><img src="ironstorage/icon.png" alt="IronStorage app icon"><div><h3>IronStorage</h3><p>Native pass and pass-otp on iPhone and Apple Watch.</p></div></header>
|
||||
<p>Generate or import a protected GPG key, keep local Git history, create encrypted entries and TOTP codes, and add optional HTTPS synchronization later.</p>
|
||||
<p><strong>Apple App Store release in preparation.</strong></p>
|
||||
<nav><a href="ironstorage/privacy/">Privacy</a> · <a href="https://git.rfc1437.de/hugo/IronStorage">Source</a> · <a href="https://git.rfc1437.de/hugo/IronStorage/issues">Support</a></nav>
|
||||
<div class="gallery">
|
||||
<figure><img src="ironstorage/screenshots/01-home.png" alt="IronStorage local Git home"><figcaption>Local Git status</figcaption></figure>
|
||||
<figure><img src="ironstorage/screenshots/02-passwords.png" alt="IronStorage password folders"><figcaption>Passwords and folders</figcaption></figure>
|
||||
<figure><img src="ironstorage/screenshots/03-totp.png" alt="IronStorage TOTP list"><figcaption>TOTP and Watch selection</figcaption></figure>
|
||||
<figure><img src="ironstorage/screenshots/04-totp-detail.png" alt="IronStorage TOTP detail"><figcaption>Live TOTP detail</figcaption></figure>
|
||||
<figure><img src="ironstorage/screenshots/05-preferences.png" alt="IronStorage preferences"><figcaption>Local-first preferences</figcaption></figure>
|
||||
<figure><img src="ironstorage/screenshots/06-search.png" alt="IronStorage search"><figcaption>Password search</figcaption></figure>
|
||||
<figure><img src="ironstorage/screenshots/watch-01-list.png" alt="IronStorage Watch entry list"><figcaption>Watch entry list</figcaption></figure>
|
||||
<figure><img src="ironstorage/screenshots/watch-02-detail.png" alt="IronStorage Watch code detail"><figcaption>Watch code and progress</figcaption></figure>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<footer><small>Both apps are free and open-source software. Each listing identifies its supported distribution channel.</small></footer>
|
||||
</main></body></html>
|
||||
26
apple/AppStore/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>IronStorage</title>
|
||||
<style>body{font:17px system-ui;line-height:1.55;max-width:64rem;margin:3rem auto;padding:0 1rem;color:#1d1d1f}header{display:flex;align-items:center;gap:1rem}header img{width:6rem;border-radius:1.3rem}.gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem}.gallery img{width:100%;border-radius:1.5rem}nav{margin:1.5rem 0}</style>
|
||||
<header><img src="icon.png" alt="IronStorage app icon"><div><h1>IronStorage</h1><p>Native pass and pass-otp for iPhone and Apple Watch.</p></div></header>
|
||||
<p>Start locally with a protected GPG key and versioned password store. Create folders and encrypted entries on your iPhone, generate TOTP codes, and optionally connect an HTTPS Git remote later.</p>
|
||||
<p>The Apple Watch companion receives only the TOTP entries you explicitly share.</p>
|
||||
<p><strong>Apple App Store release in preparation.</strong></p>
|
||||
<nav><a href="privacy/">Privacy</a> · <a href="https://git.rfc1437.de/hugo/IronStorage">Source code</a> · <a href="https://git.rfc1437.de/hugo/IronStorage/issues">Support</a></nav>
|
||||
<div class="gallery">
|
||||
<img src="screenshots/01-home.png" alt="Local Git status">
|
||||
<img src="screenshots/02-passwords.png" alt="Password entries and folders">
|
||||
<img src="screenshots/03-totp.png" alt="TOTP entries selected for Apple Watch">
|
||||
<img src="screenshots/04-totp-detail.png" alt="Live TOTP detail">
|
||||
<img src="screenshots/05-preferences.png" alt="Local-first preferences">
|
||||
<img src="screenshots/06-search.png" alt="Password search">
|
||||
</div>
|
||||
<h2>Apple Watch</h2>
|
||||
<div class="gallery">
|
||||
<img src="screenshots/watch-01-list.png" alt="Apple Watch TOTP entry list">
|
||||
<img src="screenshots/watch-02-detail.png" alt="Apple Watch selected code and progress bar">
|
||||
</div>
|
||||
</html>
|
||||
74
apple/AppStore/metadata.toml
Normal file
@@ -0,0 +1,74 @@
|
||||
[app]
|
||||
name = "IronStorage"
|
||||
bundle_identifier = "de.rfc1437.ironstorage"
|
||||
developer_name = "rfc1437"
|
||||
subtitle = "Native pass on iPhone"
|
||||
description = "IronStorage is a native iPhone and Apple Watch client for pass and pass-otp password stores. Start entirely on-device with a protected GPG key and local Git history, then add optional HTTPS Git synchronization whenever you want it."
|
||||
keywords = "password,gpg,pass,totp,git,security,watch"
|
||||
icon_file = "apple/Assets.xcassets/AppIcon.appiconset/AppIcon.png"
|
||||
category = "utilities"
|
||||
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+"
|
||||
|
||||
[release]
|
||||
version = "0.1.0"
|
||||
build = "1"
|
||||
minimum_ios = "17.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"
|
||||
caption = "Local Git history works before a sync remote is configured"
|
||||
width = 1206
|
||||
height = 2622
|
||||
|
||||
[[screenshots]]
|
||||
file = "apple/AppStore/screenshots/passwords.png"
|
||||
caption = "Password entries and folders stay organized on device"
|
||||
width = 1206
|
||||
height = 2622
|
||||
|
||||
[[screenshots]]
|
||||
file = "apple/AppStore/screenshots/totp.png"
|
||||
caption = "TOTP entries are shared with Apple Watch only when selected"
|
||||
width = 1206
|
||||
height = 2622
|
||||
|
||||
[[screenshots]]
|
||||
file = "apple/AppStore/screenshots/totp-detail.png"
|
||||
caption = "Live TOTP codes include an exact validity indicator"
|
||||
width = 1206
|
||||
height = 2622
|
||||
|
||||
[[screenshots]]
|
||||
file = "apple/AppStore/screenshots/preferences.png"
|
||||
caption = "Generate or import a GPG key and add optional HTTPS sync later"
|
||||
width = 1206
|
||||
height = 2622
|
||||
|
||||
[[screenshots]]
|
||||
file = "apple/AppStore/screenshots/search.png"
|
||||
caption = "Search encrypted password entries by name or folder"
|
||||
width = 1206
|
||||
height = 2622
|
||||
|
||||
[[watch_screenshots]]
|
||||
file = "apple/AppStore/screenshots/watch-list.png"
|
||||
caption = "The Watch list shows only explicitly shared TOTP entries"
|
||||
width = 416
|
||||
height = 496
|
||||
|
||||
[[watch_screenshots]]
|
||||
file = "apple/AppStore/screenshots/watch-detail.png"
|
||||
caption = "The selected Watch code is large with a validity progress bar"
|
||||
width = 416
|
||||
height = 496
|
||||
|
||||
[content]
|
||||
advertising = false
|
||||
account_creation = false
|
||||
digital_purchases = false
|
||||
tracking = false
|
||||
user_generated_content = false
|
||||
18
apple/AppStore/privacy/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>IronStorage Privacy</title>
|
||||
<style>body{font:17px system-ui;line-height:1.55;max-width:44rem;margin:3rem auto;padding:0 1rem;color:#1d1d1f}h1,h2{line-height:1.2}</style>
|
||||
<h1>IronStorage Privacy</h1>
|
||||
<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>
|
||||
<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 Apple’s 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>
|
||||
BIN
apple/AppStore/screenshots/home.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
apple/AppStore/screenshots/passwords.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
apple/AppStore/screenshots/preferences.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
apple/AppStore/screenshots/search.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
apple/AppStore/screenshots/totp-detail.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
apple/AppStore/screenshots/totp.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
apple/AppStore/screenshots/watch-detail.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
apple/AppStore/screenshots/watch-list.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
@@ -17,9 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@@ -619,6 +619,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
||||
|
||||
func copyTotpCode(path: String, unixSeconds: UInt64) throws -> MobileEntryCopy
|
||||
|
||||
func createDirectory(parent: String, name: String) throws
|
||||
|
||||
func discardEntryEditor(editor: UInt64) throws
|
||||
|
||||
func entryEditor(editor: UInt64) throws -> MobileEntryEditorPage
|
||||
@@ -823,6 +825,16 @@ open func copyTotpCode(path: String, unixSeconds: UInt64)throws -> MobileEntryC
|
||||
})
|
||||
}
|
||||
|
||||
open func createDirectory(parent: String, name: String)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_create_directory(
|
||||
self.uniffiCloneHandle(),
|
||||
FfiConverterString.lower(parent),
|
||||
FfiConverterString.lower(name),uniffiCallStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open func discardEntryEditor(editor: UInt64)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_discard_entry_editor(
|
||||
@@ -1551,7 +1563,7 @@ public protocol MobileKeyTransferImportProtocol: AnyObject, Sendable {
|
||||
|
||||
func addFrame(payload: String) throws -> MobileKeyTransferProgress
|
||||
|
||||
func `import`(passphrase: String?, makeDefault: Bool) throws -> MobileKeyTransferOutcome
|
||||
func `import`(passphrase: String?, makeDefault: Bool, overwrite: Bool) throws -> MobileKeyTransferOutcome
|
||||
|
||||
}
|
||||
open class MobileKeyTransferImport: MobileKeyTransferImportProtocol, @unchecked Sendable {
|
||||
@@ -1617,13 +1629,14 @@ open func addFrame(payload: String)throws -> MobileKeyTransferProgress {
|
||||
})
|
||||
}
|
||||
|
||||
open func `import`(passphrase: String?, makeDefault: Bool)throws -> MobileKeyTransferOutcome {
|
||||
open func `import`(passphrase: String?, makeDefault: Bool, overwrite: Bool)throws -> MobileKeyTransferOutcome {
|
||||
return try FfiConverterTypeMobileKeyTransferOutcome_lift(try rustCallWithError(FfiConverterTypeMobileKeyTransferFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(
|
||||
self.uniffiCloneHandle(),
|
||||
FfiConverterOptionString.lower(passphrase),
|
||||
FfiConverterBool.lower(makeDefault),uniffiCallStatus
|
||||
FfiConverterBool.lower(makeDefault),
|
||||
FfiConverterBool.lower(overwrite),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1678,10 +1691,142 @@ public func FfiConverterTypeMobileKeyTransferImport_lower(_ value: MobileKeyTran
|
||||
|
||||
|
||||
|
||||
public protocol MobileLocalOnboardingOperationProtocol: AnyObject, Sendable {
|
||||
|
||||
func cancel()
|
||||
|
||||
func generate(userId: String, passphrase: String, replaceExistingKey: Bool) throws -> MobileOnboardingOutcome
|
||||
|
||||
}
|
||||
open class MobileLocalOnboardingOperation: MobileLocalOnboardingOperationProtocol, @unchecked Sendable {
|
||||
fileprivate let handle: UInt64
|
||||
|
||||
/// Used to instantiate a [FFIObject] without an actual handle, for fakes in tests, mostly.
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct NoHandle {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// TODO: We'd like this to be `private` but for Swifty reasons,
|
||||
// we can't implement `FfiConverter` without making this `required` and we can't
|
||||
// make it `required` without making it `public`.
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
required public init(unsafeFromHandle handle: UInt64) {
|
||||
self.handle = handle
|
||||
}
|
||||
|
||||
// This constructor can be used to instantiate a fake object.
|
||||
// - Parameter noHandle: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject].
|
||||
//
|
||||
// - Warning:
|
||||
// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing handle the FFI lower functions will crash.
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public init(noHandle: NoHandle) {
|
||||
self.handle = 0
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func uniffiCloneHandle() -> UInt64 {
|
||||
return try! rustCall { uniffi_ironstorage_apple_fn_clone_mobilelocalonboardingoperation(self.handle, $0) }
|
||||
}
|
||||
// No primary constructor declared for this class.
|
||||
|
||||
deinit {
|
||||
if handle == 0 {
|
||||
// Mock objects have handle=0 don't try to free them
|
||||
return
|
||||
}
|
||||
|
||||
try! rustCall { uniffi_ironstorage_apple_fn_free_mobilelocalonboardingoperation(handle, $0) }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
open func cancel() {try! rustCall() {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_cancel(
|
||||
self.uniffiCloneHandle(),uniffiCallStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open func generate(userId: String, passphrase: String, replaceExistingKey: Bool)throws -> MobileOnboardingOutcome {
|
||||
return try FfiConverterTypeMobileOnboardingOutcome_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_generate(
|
||||
self.uniffiCloneHandle(),
|
||||
FfiConverterString.lower(userId),
|
||||
FfiConverterString.lower(passphrase),
|
||||
FfiConverterBool.lower(replaceExistingKey),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileLocalOnboardingOperation: FfiConverter {
|
||||
typealias FfiType = UInt64
|
||||
typealias SwiftType = MobileLocalOnboardingOperation
|
||||
|
||||
public static func lift(_ handle: UInt64) throws -> MobileLocalOnboardingOperation {
|
||||
return MobileLocalOnboardingOperation(unsafeFromHandle: handle)
|
||||
}
|
||||
|
||||
public static func lower(_ value: MobileLocalOnboardingOperation) -> UInt64 {
|
||||
return value.uniffiCloneHandle()
|
||||
}
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileLocalOnboardingOperation {
|
||||
let handle: UInt64 = try readInt(&buf)
|
||||
return try lift(handle)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileLocalOnboardingOperation, into buf: inout [UInt8]) {
|
||||
writeInt(&buf, lower(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileLocalOnboardingOperation_lift(_ handle: UInt64) throws -> MobileLocalOnboardingOperation {
|
||||
return try FfiConverterTypeMobileLocalOnboardingOperation.lift(handle)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileLocalOnboardingOperation_lower(_ value: MobileLocalOnboardingOperation) -> UInt64 {
|
||||
return FfiConverterTypeMobileLocalOnboardingOperation.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public protocol MobileOnboardingOperationProtocol: AnyObject, Sendable {
|
||||
|
||||
func cancel()
|
||||
|
||||
func connectLocalStore() throws -> MobileOnboardingOutcome
|
||||
|
||||
func discover() throws -> MobileOnboardingDiscovery
|
||||
|
||||
func progress() -> MobileOnboardingProgress
|
||||
@@ -1750,6 +1895,15 @@ open func cancel() {try! rustCall() {
|
||||
}
|
||||
}
|
||||
|
||||
open func connectLocalStore()throws -> MobileOnboardingOutcome {
|
||||
return try FfiConverterTypeMobileOnboardingOutcome_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_connect_local_store(
|
||||
self.uniffiCloneHandle(),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
open func discover()throws -> MobileOnboardingDiscovery {
|
||||
return try FfiConverterTypeMobileOnboardingDiscovery_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
@@ -2929,6 +3083,7 @@ public func FfiConverterTypeMobileHomeNotice_lower(_ value: MobileHomeNotice) ->
|
||||
|
||||
|
||||
public struct MobileHomePage: Equatable, Hashable {
|
||||
public var remoteConfigured: Bool
|
||||
public var freshness: MobileHomeFreshness
|
||||
public var refreshedAt: Int64?
|
||||
public var summaries: [MobileHomeSummaryRow]
|
||||
@@ -2940,7 +3095,8 @@ public struct MobileHomePage: Equatable, Hashable {
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(freshness: MobileHomeFreshness, refreshedAt: Int64?, summaries: [MobileHomeSummaryRow], incoming: [MobileHomeCommit], outgoing: [MobileHomeCommit], incomingTotal: UInt32, outgoingTotal: UInt32, notice: MobileHomeNotice?) {
|
||||
public init(remoteConfigured: Bool, freshness: MobileHomeFreshness, refreshedAt: Int64?, summaries: [MobileHomeSummaryRow], incoming: [MobileHomeCommit], outgoing: [MobileHomeCommit], incomingTotal: UInt32, outgoingTotal: UInt32, notice: MobileHomeNotice?) {
|
||||
self.remoteConfigured = remoteConfigured
|
||||
self.freshness = freshness
|
||||
self.refreshedAt = refreshedAt
|
||||
self.summaries = summaries
|
||||
@@ -2967,6 +3123,7 @@ public struct FfiConverterTypeMobileHomePage: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileHomePage {
|
||||
return
|
||||
try MobileHomePage(
|
||||
remoteConfigured: FfiConverterBool.read(from: &buf),
|
||||
freshness: FfiConverterTypeMobileHomeFreshness.read(from: &buf),
|
||||
refreshedAt: FfiConverterOptionInt64.read(from: &buf),
|
||||
summaries: FfiConverterSequenceTypeMobileHomeSummaryRow.read(from: &buf),
|
||||
@@ -2979,6 +3136,7 @@ public struct FfiConverterTypeMobileHomePage: FfiConverterRustBuffer {
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileHomePage, into buf: inout [UInt8]) {
|
||||
FfiConverterBool.write(value.remoteConfigured, into: &buf)
|
||||
FfiConverterTypeMobileHomeFreshness.write(value.freshness, into: &buf)
|
||||
FfiConverterOptionInt64.write(value.refreshedAt, into: &buf)
|
||||
FfiConverterSequenceTypeMobileHomeSummaryRow.write(value.summaries, into: &buf)
|
||||
@@ -4081,6 +4239,7 @@ public func FfiConverterTypeMobilePasswordRow_lower(_ value: MobilePasswordRow)
|
||||
|
||||
|
||||
public struct MobilePreferences: Equatable, Hashable {
|
||||
public var syncConfigured: Bool
|
||||
public var repositoryTitle: String
|
||||
public var repositoryUrl: String
|
||||
public var serverTitle: String
|
||||
@@ -4097,7 +4256,8 @@ public struct MobilePreferences: Equatable, Hashable {
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(repositoryTitle: String, repositoryUrl: String, serverTitle: String, serverIdentity: String, applicationAccount: String?, defaultKeyTitle: String, defaultKeyFingerprint: String, authenticationTimeoutSeconds: UInt64, biometricUnlockEnabled: Bool, appearance: MobileAppearance, watchState: MobileWatchPreferenceState, watchTitle: String, watchDetail: String) {
|
||||
public init(syncConfigured: Bool, repositoryTitle: String, repositoryUrl: String, serverTitle: String, serverIdentity: String, applicationAccount: String?, defaultKeyTitle: String, defaultKeyFingerprint: String, authenticationTimeoutSeconds: UInt64, biometricUnlockEnabled: Bool, appearance: MobileAppearance, watchState: MobileWatchPreferenceState, watchTitle: String, watchDetail: String) {
|
||||
self.syncConfigured = syncConfigured
|
||||
self.repositoryTitle = repositoryTitle
|
||||
self.repositoryUrl = repositoryUrl
|
||||
self.serverTitle = serverTitle
|
||||
@@ -4129,6 +4289,7 @@ public struct FfiConverterTypeMobilePreferences: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePreferences {
|
||||
return
|
||||
try MobilePreferences(
|
||||
syncConfigured: FfiConverterBool.read(from: &buf),
|
||||
repositoryTitle: FfiConverterString.read(from: &buf),
|
||||
repositoryUrl: FfiConverterString.read(from: &buf),
|
||||
serverTitle: FfiConverterString.read(from: &buf),
|
||||
@@ -4146,6 +4307,7 @@ public struct FfiConverterTypeMobilePreferences: FfiConverterRustBuffer {
|
||||
}
|
||||
|
||||
public static func write(_ value: MobilePreferences, into buf: inout [UInt8]) {
|
||||
FfiConverterBool.write(value.syncConfigured, into: &buf)
|
||||
FfiConverterString.write(value.repositoryTitle, into: &buf)
|
||||
FfiConverterString.write(value.repositoryUrl, into: &buf)
|
||||
FfiConverterString.write(value.serverTitle, into: &buf)
|
||||
@@ -5674,12 +5836,78 @@ public func FfiConverterTypeMobileHomePhase_lower(_ value: MobileHomePhase) -> R
|
||||
|
||||
|
||||
|
||||
|
||||
public enum MobileKeyTransferErrorKind: Equatable, Hashable {
|
||||
|
||||
case existingKey
|
||||
case failed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileKeyTransferErrorKind: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileKeyTransferErrorKind: FfiConverterRustBuffer {
|
||||
typealias SwiftType = MobileKeyTransferErrorKind
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferErrorKind {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
switch variant {
|
||||
|
||||
case 1: return .existingKey
|
||||
|
||||
case 2: return .failed
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileKeyTransferErrorKind, into buf: inout [UInt8]) {
|
||||
switch value {
|
||||
|
||||
|
||||
case .existingKey:
|
||||
writeInt(&buf, Int32(1))
|
||||
|
||||
|
||||
case .failed:
|
||||
writeInt(&buf, Int32(2))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileKeyTransferErrorKind_lift(_ buf: RustBuffer) throws -> MobileKeyTransferErrorKind {
|
||||
return try FfiConverterTypeMobileKeyTransferErrorKind.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileKeyTransferErrorKind_lower(_ value: MobileKeyTransferErrorKind) -> RustBuffer {
|
||||
return FfiConverterTypeMobileKeyTransferErrorKind.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public
|
||||
enum MobileKeyTransferFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||
|
||||
|
||||
|
||||
case Failed(message: String
|
||||
case Failed(kind: MobileKeyTransferErrorKind, message: String
|
||||
)
|
||||
|
||||
|
||||
@@ -5711,6 +5939,7 @@ public struct FfiConverterTypeMobileKeyTransferFfiError: FfiConverterRustBuffer
|
||||
|
||||
|
||||
case 1: return .Failed(
|
||||
kind: try FfiConverterTypeMobileKeyTransferErrorKind.read(from: &buf),
|
||||
message: try FfiConverterString.read(from: &buf)
|
||||
)
|
||||
|
||||
@@ -5725,8 +5954,9 @@ public struct FfiConverterTypeMobileKeyTransferFfiError: FfiConverterRustBuffer
|
||||
|
||||
|
||||
|
||||
case let .Failed(message):
|
||||
case let .Failed(kind,message):
|
||||
writeInt(&buf, Int32(1))
|
||||
FfiConverterTypeMobileKeyTransferErrorKind.write(kind, into: &buf)
|
||||
FfiConverterString.write(message, into: &buf)
|
||||
|
||||
}
|
||||
@@ -5896,6 +6126,7 @@ public enum MobileOnboardingErrorKind: Equatable, Hashable {
|
||||
case authentication
|
||||
case repository
|
||||
case existingClone
|
||||
case existingKey
|
||||
case interrupted
|
||||
case secureStorage
|
||||
case configuration
|
||||
@@ -5931,13 +6162,15 @@ public struct FfiConverterTypeMobileOnboardingErrorKind: FfiConverterRustBuffer
|
||||
|
||||
case 5: return .existingClone
|
||||
|
||||
case 6: return .interrupted
|
||||
case 6: return .existingKey
|
||||
|
||||
case 7: return .secureStorage
|
||||
case 7: return .interrupted
|
||||
|
||||
case 8: return .configuration
|
||||
case 8: return .secureStorage
|
||||
|
||||
case 9: return .alreadyConfigured
|
||||
case 9: return .configuration
|
||||
|
||||
case 10: return .alreadyConfigured
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
@@ -5967,21 +6200,25 @@ public struct FfiConverterTypeMobileOnboardingErrorKind: FfiConverterRustBuffer
|
||||
writeInt(&buf, Int32(5))
|
||||
|
||||
|
||||
case .interrupted:
|
||||
case .existingKey:
|
||||
writeInt(&buf, Int32(6))
|
||||
|
||||
|
||||
case .secureStorage:
|
||||
case .interrupted:
|
||||
writeInt(&buf, Int32(7))
|
||||
|
||||
|
||||
case .configuration:
|
||||
case .secureStorage:
|
||||
writeInt(&buf, Int32(8))
|
||||
|
||||
|
||||
case .alreadyConfigured:
|
||||
case .configuration:
|
||||
writeInt(&buf, Int32(9))
|
||||
|
||||
|
||||
case .alreadyConfigured:
|
||||
writeInt(&buf, Int32(10))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7574,6 +7811,20 @@ public func mobileKeyTransfer()throws -> MobileKeyTransfer {
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobileLocalKeyTransferImporter() -> MobileKeyTransferImport {
|
||||
return try! FfiConverterTypeMobileKeyTransferImport_lift(try! rustCall() {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_func_mobile_local_key_transfer_importer(uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobileLocalOnboardingOperation() -> MobileLocalOnboardingOperation {
|
||||
return try! FfiConverterTypeMobileLocalOnboardingOperation_lift(try! rustCall() {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_func_mobile_local_onboarding_operation(uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobileOnboardingOperation(serverUrl: String, account: String, repositoryPath: String, applicationToken: String)throws -> MobileOnboardingOperation {
|
||||
return try FfiConverterTypeMobileOnboardingOperation_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
@@ -7670,6 +7921,12 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_key_transfer() != 57389) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_local_key_transfer_importer() != 50238) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_local_onboarding_operation() != 18004) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
@@ -7721,6 +7978,9 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_totp_code() != 8344) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_create_directory() != 25476) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_discard_entry_editor() != 28195) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
@@ -7856,12 +8116,21 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_add_frame() != 27319) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import() != 37403) {
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import() != 44529) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_cancel() != 26455) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_generate() != 58931) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_cancel() != 49755) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_connect_local_store() != 57886) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_discover() != 2309) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
|
||||
@@ -293,6 +293,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_copy_entry_fi
|
||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_copy_totp_code(uint64_t ptr, RustBuffer path, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
|
||||
void uniffi_ironstorage_apple_fn_method_mobileauthentication_create_directory(uint64_t ptr, RustBuffer parent, RustBuffer name, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
|
||||
void uniffi_ironstorage_apple_fn_method_mobileauthentication_discard_entry_editor(uint64_t ptr, uint64_t editor, RustCallStatus *_Nonnull out_status
|
||||
@@ -550,7 +555,27 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_add_frame(
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
|
||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(uint64_t ptr, RustBuffer passphrase, int8_t make_default, RustCallStatus *_Nonnull out_status
|
||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(uint64_t ptr, RustBuffer passphrase, int8_t make_default, int8_t overwrite, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILELOCALONBOARDINGOPERATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILELOCALONBOARDINGOPERATION
|
||||
uint64_t uniffi_ironstorage_apple_fn_clone_mobilelocalonboardingoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILELOCALONBOARDINGOPERATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILELOCALONBOARDINGOPERATION
|
||||
void uniffi_ironstorage_apple_fn_free_mobilelocalonboardingoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
|
||||
void uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_cancel(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
|
||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_generate(uint64_t ptr, RustBuffer user_id, RustBuffer passphrase, int8_t replace_existing_key, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEONBOARDINGOPERATION
|
||||
@@ -568,6 +593,11 @@ void uniffi_ironstorage_apple_fn_free_mobileonboardingoperation(uint64_t handle,
|
||||
void uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_cancel(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
|
||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_connect_local_store(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_DISCOVER
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_DISCOVER
|
||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_discover(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||
@@ -618,6 +648,18 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustBuffer authe
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_KEY_TRANSFER
|
||||
uint64_t uniffi_ironstorage_apple_fn_func_mobile_key_transfer(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
|
||||
uint64_t uniffi_ironstorage_apple_fn_func_mobile_local_key_transfer_importer(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
|
||||
uint64_t uniffi_ironstorage_apple_fn_func_mobile_local_onboarding_operation(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_ONBOARDING_OPERATION
|
||||
@@ -944,6 +986,18 @@ uint16_t uniffi_ironstorage_apple_checksum_func_mobile_home_operation(void
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_KEY_TRANSFER
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_key_transfer(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_local_key_transfer_importer(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_local_onboarding_operation(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
|
||||
@@ -1046,6 +1100,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_entr
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_COPY_TOTP_CODE
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_totp_code(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_create_directory(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
|
||||
@@ -1322,12 +1382,30 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_add_fr
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_cancel(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_generate(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CANCEL
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CANCEL
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_cancel(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_connect_local_store(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_DISCOVER
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Unlock your GPG key for protected password operations.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Scan GPG key transfer QR codes that you choose to import.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Unlock your GPG key for protected password operations.</string>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
21
apple/Resources/App/License.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 rfc1437
|
||||
|
||||
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.
|
||||
17970
apple/Resources/App/ThirdPartyLicenses.txt
Normal file
@@ -20,6 +20,9 @@ extension Notification.Name {
|
||||
static let ironStorageKeyMaterialDidChange = Notification.Name(
|
||||
"de.rfc1437.ironstorage.key-material-did-change"
|
||||
)
|
||||
static let ironStorageConfigurationDidChange = Notification.Name(
|
||||
"de.rfc1437.ironstorage.configuration-did-change"
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -182,6 +185,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
name: .ironStorageKeyMaterialDidChange,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(keyMaterialDidChange),
|
||||
name: .ironStorageConfigurationDidChange,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -636,7 +645,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
|
||||
}
|
||||
|
||||
@objc private func setupRequested() {
|
||||
navigationController?.pushViewController(OnboardingViewController(), animated: true)
|
||||
navigationController?.pushViewController(SetupChoiceViewController(), animated: true)
|
||||
}
|
||||
|
||||
@objc private func tokenUpdateRequested() {
|
||||
@@ -703,8 +712,10 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
|
||||
)
|
||||
case .empty:
|
||||
content.image = UIImage(systemName: "checkmark.circle")
|
||||
content.text = "No Remote Activity"
|
||||
content.secondaryText = "There are no commits to pull or push."
|
||||
content.text = homePage.remoteConfigured ? "No Remote Activity" : "Local Store Ready"
|
||||
content.secondaryText = homePage.remoteConfigured
|
||||
? "There are no commits to pull or push."
|
||||
: "Changes are committed to the local Git repository."
|
||||
cell.selectionStyle = .none
|
||||
case .notice:
|
||||
if let notice = homePage.notice {
|
||||
@@ -734,6 +745,11 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
|
||||
}
|
||||
|
||||
private func freshnessDescription(_ page: MobileHomePage) -> String {
|
||||
if !page.remoteConfigured {
|
||||
return page.freshness == .current
|
||||
? "Current local Git status."
|
||||
: "Local Git status from this device."
|
||||
}
|
||||
let refreshed = page.refreshedAt.map(formattedDate)
|
||||
switch page.freshness {
|
||||
case .neverRefreshed:
|
||||
@@ -867,7 +883,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
|
||||
private func showHomeLoading() {
|
||||
var configuration = UIContentUnavailableConfiguration.loading()
|
||||
configuration.text = "Loading Activity"
|
||||
configuration.secondaryText = "Reading cached remote-branch state from storage."
|
||||
configuration.secondaryText = "Reading Git history and password-store status."
|
||||
contentUnavailableConfiguration = configuration
|
||||
}
|
||||
|
||||
@@ -1078,7 +1094,8 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
) -> Int {
|
||||
switch section {
|
||||
case 0: 2
|
||||
case 1, 2, 3: 3
|
||||
case 1: preferences?.syncConfigured == false ? 1 : 3
|
||||
case 2, 3: 3
|
||||
default: 1
|
||||
}
|
||||
}
|
||||
@@ -1089,7 +1106,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
) -> String? {
|
||||
switch section {
|
||||
case 0: "Password Store"
|
||||
case 1: "Application Token"
|
||||
case 1: preferences?.syncConfigured == false ? "Git Sync" : "Application Token"
|
||||
case 2: "GPG Key & Recovery"
|
||||
case 3: "Authentication"
|
||||
case 4: "Appearance"
|
||||
@@ -1106,7 +1123,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
case 0:
|
||||
"Non-secret repository and server identities from the shared configuration."
|
||||
case 1:
|
||||
"The application token is stored only in protected system storage and is never displayed."
|
||||
preferences?.syncConfigured == false
|
||||
? "The store has local Git history. An HTTPS remote can be configured later for synchronization."
|
||||
: "The application token is stored only in protected system storage and is never displayed."
|
||||
case 2:
|
||||
"Import provides initial or recovery key setup. Private-key transfers require explicit confirmation and passphrase validation."
|
||||
case 3:
|
||||
@@ -1138,11 +1157,18 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
content.secondaryText = preferences?.serverIdentity ?? "Server identity unavailable"
|
||||
cell.selectionStyle = .none
|
||||
case (1, 0):
|
||||
content.image = UIImage(systemName: "key.fill")
|
||||
content.text = preferences?.applicationAccount ?? "No Application Token"
|
||||
content.secondaryText = preferences?.applicationAccount == nil
|
||||
? "Git network operations require a replacement token"
|
||||
: "Token stored in protected system storage"
|
||||
if preferences?.syncConfigured == false {
|
||||
content.image = UIImage(systemName: "network.badge.shield.half.filled")
|
||||
content.text = "Add HTTPS Remote"
|
||||
content.secondaryText = "Keep local history and enable optional synchronization"
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
} else {
|
||||
content.image = UIImage(systemName: "key.fill")
|
||||
content.text = preferences?.applicationAccount ?? "No Application Token"
|
||||
content.secondaryText = preferences?.applicationAccount == nil
|
||||
? "Git network operations require a replacement token"
|
||||
: "Token stored in protected system storage"
|
||||
}
|
||||
cell.selectionStyle = .none
|
||||
case (1, 1):
|
||||
content.image = UIImage(systemName: "arrow.triangle.2.circlepath")
|
||||
@@ -1228,6 +1254,11 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch (indexPath.section, indexPath.row) {
|
||||
case (1, 0) where preferences?.syncConfigured == false:
|
||||
navigationController?.pushViewController(
|
||||
OnboardingViewController(attachLocalStore: true),
|
||||
animated: true
|
||||
)
|
||||
case (1, 1):
|
||||
tokenUpdateRequested()
|
||||
case (1, 2):
|
||||
@@ -1461,8 +1492,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "Remove Token", style: .destructive) {
|
||||
[weak self] _ in
|
||||
guard let authentication = self?.authentication else { return }
|
||||
self?.runPreferenceUpdate(announcement: "Application token removed") {
|
||||
try self?.authentication?.removeApplicationToken()
|
||||
try authentication.removeApplicationToken()
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
@@ -1483,8 +1515,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
alert.addAction(UIAlertAction(title: "Save", style: .default) {
|
||||
[weak self, weak alert] _ in
|
||||
let seconds = UInt64(alert?.textFields?.first?.text ?? "") ?? 0
|
||||
guard let authentication = self?.authentication else { return }
|
||||
self?.runPreferenceUpdate(announcement: "Inactivity timeout updated") {
|
||||
try self?.authentication?.setAuthenticationTimeout(seconds: seconds)
|
||||
try authentication.setAuthenticationTimeout(seconds: seconds)
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
@@ -1498,8 +1531,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
title: "\(selected ? "✓ " : "")\(appearanceTitle(appearance))",
|
||||
style: .default
|
||||
) { [weak self] _ in
|
||||
guard let authentication = self?.authentication else { return }
|
||||
self?.runPreferenceUpdate(announcement: "Appearance updated") {
|
||||
try self?.authentication?.setMobileAppearance(appearance: appearance)
|
||||
try authentication.setMobileAppearance(appearance: appearance)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1656,6 +1690,14 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
||||
watchPaired: watchSnapshot.isPaired,
|
||||
watchAppInstalled: watchSnapshot.isWatchAppInstalled
|
||||
)
|
||||
navigationItem.rightBarButtonItem = preferences?.syncConfigured == true
|
||||
? UIBarButtonItem(
|
||||
title: "Update Token",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(tokenUpdateRequested)
|
||||
)
|
||||
: nil
|
||||
if let appearance = preferences?.appearance {
|
||||
tabBarController?.overrideUserInterfaceStyle = switch appearance {
|
||||
case .system: .unspecified
|
||||
@@ -1673,6 +1715,7 @@ private final class KeyImportScannerViewController: UIViewController,
|
||||
DataScannerViewControllerDelegate
|
||||
{
|
||||
private let importer: MobileKeyTransferImport
|
||||
private let localSetup: Bool
|
||||
private let scanner = DataScannerViewController(
|
||||
recognizedDataTypes: [.barcode(symbologies: [.qr])],
|
||||
qualityLevel: .balanced,
|
||||
@@ -1688,11 +1731,20 @@ private final class KeyImportScannerViewController: UIViewController,
|
||||
|
||||
init(transfer: MobileKeyTransfer) {
|
||||
importer = transfer.importer()
|
||||
localSetup = false
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
title = "Import GPG Key"
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
}
|
||||
|
||||
init(localImporter: MobileKeyTransferImport) {
|
||||
importer = localImporter
|
||||
localSetup = true
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
title = "Import Local GPG Key"
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
@@ -1855,11 +1907,20 @@ private final class KeyImportScannerViewController: UIViewController,
|
||||
|
||||
private func confirmImport(_ key: MobileKeyTransferKey) {
|
||||
let privateKey = key.kind == .private
|
||||
if localSetup, !privateKey {
|
||||
completing = false
|
||||
presentFailure("A local store requires a private GPG key with an encryption subkey.") {
|
||||
[weak self] in try? self?.scanner.startScanning()
|
||||
}
|
||||
return
|
||||
}
|
||||
let warning = privateKey
|
||||
? "Import this private key only if you trust the device that displayed it."
|
||||
: "Import this public key?"
|
||||
let alert = UIAlertController(
|
||||
title: privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?",
|
||||
title: localSetup
|
||||
? "Create Local Store with This Key?"
|
||||
: (privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?"),
|
||||
message: "\(warning)\n\n\(key.title)\n\(key.detail)",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
@@ -1874,21 +1935,37 @@ private final class KeyImportScannerViewController: UIViewController,
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Import", style: .default) { [weak self, weak alert] _ in
|
||||
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: false)
|
||||
})
|
||||
if privateKey {
|
||||
alert.addAction(UIAlertAction(title: "Import as Default", style: .default) {
|
||||
if !localSetup {
|
||||
alert.addAction(UIAlertAction(title: "Import", style: .default) {
|
||||
[weak self, weak alert] _ in
|
||||
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: true)
|
||||
self?.finishImport(
|
||||
passphrase: alert?.textFields?.first?.text,
|
||||
makeDefault: false
|
||||
)
|
||||
})
|
||||
}
|
||||
if privateKey {
|
||||
alert.addAction(UIAlertAction(
|
||||
title: localSetup ? "Create Local Store" : "Import as Default",
|
||||
style: .default
|
||||
) {
|
||||
[weak self, weak alert] _ in
|
||||
self?.finishImport(
|
||||
passphrase: alert?.textFields?.first?.text,
|
||||
makeDefault: true
|
||||
)
|
||||
})
|
||||
}
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func finishImport(passphrase: String?, makeDefault: Bool) {
|
||||
private func finishImport(passphrase: String?, makeDefault: Bool, overwrite: Bool = false) {
|
||||
do {
|
||||
let outcome = try importer.import(passphrase: passphrase, makeDefault: makeDefault)
|
||||
let outcome = try importer.import(
|
||||
passphrase: passphrase,
|
||||
makeDefault: makeDefault,
|
||||
overwrite: overwrite
|
||||
)
|
||||
let alert = UIAlertController(
|
||||
title: outcome.title,
|
||||
message: outcome.detail,
|
||||
@@ -1899,6 +1976,16 @@ private final class KeyImportScannerViewController: UIViewController,
|
||||
NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
} catch let error as MobileKeyTransferFfiError {
|
||||
if case let .Failed(kind, _) = error, kind == .existingKey, !overwrite {
|
||||
confirmOverwrite(passphrase: passphrase, makeDefault: makeDefault)
|
||||
return
|
||||
}
|
||||
completing = false
|
||||
presentFailure(error.localizedDescription) { [weak self] in
|
||||
guard let self else { return }
|
||||
try? scanner.startScanning()
|
||||
}
|
||||
} catch {
|
||||
completing = false
|
||||
presentFailure(error.localizedDescription) { [weak self] in
|
||||
@@ -1908,6 +1995,27 @@ private final class KeyImportScannerViewController: UIViewController,
|
||||
}
|
||||
}
|
||||
|
||||
private func confirmOverwrite(passphrase: String?, makeDefault: Bool) {
|
||||
let alert = UIAlertController(
|
||||
title: "Replace Existing GPG Key?",
|
||||
message: "A key with this fingerprint already exists. Replacing key material can change which private key is used to unlock entries.",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
|
||||
self?.completing = false
|
||||
try? self?.scanner.startScanning()
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Replace Key", style: .destructive) {
|
||||
[weak self] _ in
|
||||
self?.finishImport(
|
||||
passphrase: passphrase,
|
||||
makeDefault: makeDefault,
|
||||
overwrite: true
|
||||
)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentFailure(_ detail: String, completion: (() -> Void)? = nil) {
|
||||
guard presentedViewController == nil else { return }
|
||||
let alert = UIAlertController(
|
||||
@@ -4202,6 +4310,55 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func createFolderRequested() {
|
||||
let alert = UIAlertController(
|
||||
title: "New Folder",
|
||||
message: "Enter a name for the password folder in this folder.",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addTextField { field in
|
||||
field.placeholder = "Folder Name"
|
||||
field.autocapitalizationType = .words
|
||||
field.clearButtonMode = .whileEditing
|
||||
field.returnKeyType = .go
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "Create", style: .default) { [weak self, weak alert] _ in
|
||||
guard let name = alert?.textFields?.first?.text, !name.isEmpty else { return }
|
||||
self?.beginCreateFolder(name: name)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func beginCreateFolder(name: String) {
|
||||
guard let authentication else {
|
||||
presentAuthenticationFailure(.unavailable)
|
||||
return
|
||||
}
|
||||
createTask?.cancel()
|
||||
let parent = path ?? ""
|
||||
createTask = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
try authentication.createDirectory(parent: parent, name: name)
|
||||
return Result<Void, AuthenticationFailure>.success(())
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
return .failure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
switch result {
|
||||
case .success:
|
||||
NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
|
||||
reload()
|
||||
case let .failure(failure):
|
||||
presentAuthenticationFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func beginCreate(name: String, passphrase: String?) {
|
||||
guard let authentication else {
|
||||
presentAuthenticationFailure(.unavailable)
|
||||
@@ -4345,12 +4502,21 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
|
||||
case let .success(page):
|
||||
directoryPage = page
|
||||
title = page.title
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
let add = UIBarButtonItem(
|
||||
barButtonSystemItem: .add,
|
||||
target: self,
|
||||
action: #selector(createRequested)
|
||||
target: nil,
|
||||
action: nil
|
||||
)
|
||||
navigationItem.rightBarButtonItem?.accessibilityLabel = "Create password entry"
|
||||
add.menu = UIMenu(children: [
|
||||
UIAction(title: "New Password", image: UIImage(systemName: "key")) {
|
||||
[weak self] _ in self?.createRequested()
|
||||
},
|
||||
UIAction(title: "New Folder", image: UIImage(systemName: "folder.badge.plus")) {
|
||||
[weak self] _ in self?.createFolderRequested()
|
||||
},
|
||||
])
|
||||
add.accessibilityLabel = "Create password or folder"
|
||||
navigationItem.rightBarButtonItem = add
|
||||
contentUnavailableConfiguration = nil
|
||||
tableView.reloadData()
|
||||
if page.rows.isEmpty {
|
||||
@@ -6058,8 +6224,315 @@ private final class TokenUpdateViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class SetupChoiceViewController: UITableViewController {
|
||||
init() {
|
||||
super.init(style: .insetGrouped)
|
||||
title = "Set Up IronStorage"
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { 2 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 2 : 1
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
titleForHeaderInSection section: Int
|
||||
) -> String? {
|
||||
section == 0 ? "Local Store" : "Existing Git Store"
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
titleForFooterInSection section: Int
|
||||
) -> String? {
|
||||
section == 0
|
||||
? "Creates a local Git repository. Add an HTTPS remote later when you want to sync."
|
||||
: "Clone an existing password-store repository over HTTPS."
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
var content = cell.defaultContentConfiguration()
|
||||
switch (indexPath.section, indexPath.row) {
|
||||
case (0, 0):
|
||||
content.image = UIImage(systemName: "key.badge.plus")
|
||||
content.text = "Generate Local GPG Key"
|
||||
content.secondaryText = "Create a protected key and local Git history"
|
||||
case (0, 1):
|
||||
content.image = UIImage(systemName: "qrcode.viewfinder")
|
||||
content.text = "Import Local GPG Key"
|
||||
content.secondaryText = "Scan a private IronStorage key transfer"
|
||||
default:
|
||||
content.image = UIImage(systemName: "network")
|
||||
content.text = "Connect Existing Store"
|
||||
content.secondaryText = "Clone an HTTPS Git repository"
|
||||
}
|
||||
content.secondaryTextProperties.numberOfLines = 0
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch (indexPath.section, indexPath.row) {
|
||||
case (0, 0):
|
||||
navigationController?.pushViewController(LocalSetupViewController(), animated: true)
|
||||
case (0, 1):
|
||||
requestLocalKeyScanner()
|
||||
default:
|
||||
navigationController?.pushViewController(OnboardingViewController(), animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func requestLocalKeyScanner() {
|
||||
guard DataScannerViewController.isSupported else {
|
||||
presentMessage("This device does not support live QR scanning.")
|
||||
return
|
||||
}
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
showLocalKeyScanner()
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
Task { @MainActor in
|
||||
if granted {
|
||||
self?.showLocalKeyScanner()
|
||||
} else {
|
||||
self?.presentMessage("Allow camera access in Settings to import a GPG key.")
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
presentMessage("Allow camera access in Settings to import a GPG key.")
|
||||
}
|
||||
}
|
||||
|
||||
private func showLocalKeyScanner() {
|
||||
guard DataScannerViewController.isAvailable else {
|
||||
presentMessage("Close other camera apps and try again.")
|
||||
return
|
||||
}
|
||||
navigationController?.pushViewController(
|
||||
KeyImportScannerViewController(localImporter: mobileLocalKeyTransferImporter()),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
private func presentMessage(_ detail: String) {
|
||||
let alert = UIAlertController(
|
||||
title: "GPG Key Import Is Unavailable",
|
||||
message: detail,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class LocalSetupViewController: UITableViewController {
|
||||
private let keyNameField = UITextField()
|
||||
private let passphraseField = UITextField()
|
||||
private let confirmationField = UITextField()
|
||||
private var task: Task<Void, Never>?
|
||||
private var operation: MobileLocalOnboardingOperation?
|
||||
private var isWorking = false
|
||||
|
||||
init() {
|
||||
super.init(style: .insetGrouped)
|
||||
title = "Create Local Store"
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
configureFields()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
deinit {
|
||||
operation?.cancel()
|
||||
task?.cancel()
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { 2 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 3 : 1
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
titleForHeaderInSection section: Int
|
||||
) -> String? {
|
||||
section == 0 ? "Protected GPG Key" : nil
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
titleForFooterInSection section: Int
|
||||
) -> String? {
|
||||
section == 0
|
||||
? "The passphrase protects the generated private key. Keep it somewhere safe; IronStorage cannot recover it."
|
||||
: "Creates a pass-compatible password store with local Git versioning and no network connection."
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
|
||||
if indexPath.section == 1 {
|
||||
cell.textLabel?.text = isWorking ? "Creating…" : "Create Local Store"
|
||||
cell.textLabel?.textAlignment = .center
|
||||
cell.textLabel?.textColor = isWorking ? .secondaryLabel : view.tintColor
|
||||
cell.isUserInteractionEnabled = !isWorking
|
||||
return cell
|
||||
}
|
||||
let field = [keyNameField, passphraseField, confirmationField][indexPath.row]
|
||||
field.translatesAutoresizingMaskIntoConstraints = false
|
||||
field.isEnabled = !isWorking
|
||||
cell.contentView.addSubview(field)
|
||||
NSLayoutConstraint.activate([
|
||||
field.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
|
||||
field.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor),
|
||||
field.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 10),
|
||||
field.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -10),
|
||||
])
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard indexPath.section == 1, !isWorking else { return }
|
||||
create(replaceExistingKey: false)
|
||||
}
|
||||
|
||||
private func configureFields() {
|
||||
for field in [keyNameField, passphraseField, confirmationField] {
|
||||
field.borderStyle = .none
|
||||
field.clearButtonMode = .whileEditing
|
||||
field.autocorrectionType = .no
|
||||
field.adjustsFontForContentSizeCategory = true
|
||||
field.font = .preferredFont(forTextStyle: .body)
|
||||
}
|
||||
keyNameField.placeholder = "GPG key name"
|
||||
keyNameField.text = "IronStorage Local Key"
|
||||
keyNameField.textContentType = .name
|
||||
keyNameField.autocapitalizationType = .words
|
||||
passphraseField.placeholder = "GPG key passphrase"
|
||||
confirmationField.placeholder = "Confirm passphrase"
|
||||
for field in [passphraseField, confirmationField] {
|
||||
field.isSecureTextEntry = true
|
||||
field.textContentType = .newPassword
|
||||
field.autocapitalizationType = .none
|
||||
}
|
||||
}
|
||||
|
||||
private func create(replaceExistingKey: Bool) {
|
||||
let keyName = keyNameField.text ?? ""
|
||||
let passphrase = passphraseField.text ?? ""
|
||||
guard !keyName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
!passphrase.isEmpty,
|
||||
passphrase == confirmationField.text
|
||||
else {
|
||||
presentFailure(
|
||||
title: "Check GPG Key Details",
|
||||
detail: "Enter a key name and matching non-empty passphrases."
|
||||
)
|
||||
return
|
||||
}
|
||||
let operation = mobileLocalOnboardingOperation()
|
||||
self.operation = operation
|
||||
isWorking = true
|
||||
navigationItem.prompt = "Generating a protected GPG key and local Git repository."
|
||||
tableView.reloadData()
|
||||
task = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobileOnboardingOutcome, OnboardingFailure>.success(
|
||||
try operation.generate(
|
||||
userId: keyName,
|
||||
passphrase: passphrase,
|
||||
replaceExistingKey: replaceExistingKey
|
||||
)
|
||||
)
|
||||
} catch let error as MobileOnboardingFfiError {
|
||||
return .failure(OnboardingFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
self?.finish(result, keyName: keyName, passphrase: passphrase)
|
||||
}
|
||||
}
|
||||
|
||||
private func finish(
|
||||
_ result: Result<MobileOnboardingOutcome, OnboardingFailure>,
|
||||
keyName: String,
|
||||
passphrase: String
|
||||
) {
|
||||
operation = nil
|
||||
task = nil
|
||||
isWorking = false
|
||||
navigationItem.prompt = nil
|
||||
tableView.reloadData()
|
||||
switch result {
|
||||
case let .success(outcome):
|
||||
passphraseField.text = nil
|
||||
confirmationField.text = nil
|
||||
let alert = UIAlertController(
|
||||
title: outcome.title,
|
||||
message: outcome.detail,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
|
||||
NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
case let .failure(failure) where failure.kind == .existingKey:
|
||||
let alert = UIAlertController(
|
||||
title: failure.title,
|
||||
message: failure.detail,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "Replace Key", style: .destructive) {
|
||||
[weak self] _ in
|
||||
self?.keyNameField.text = keyName
|
||||
self?.passphraseField.text = passphrase
|
||||
self?.confirmationField.text = passphrase
|
||||
self?.create(replaceExistingKey: true)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
case let .failure(failure):
|
||||
presentFailure(title: failure.title, detail: failure.detail)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentFailure(title: String, detail: String) {
|
||||
let alert = UIAlertController(title: title, message: detail, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class OnboardingViewController: UITableViewController {
|
||||
private let attachLocalStore: Bool
|
||||
private let serverField = UITextField()
|
||||
private let accountField = UITextField()
|
||||
private let repositoryField = UITextField()
|
||||
@@ -6072,9 +6545,10 @@ private final class OnboardingViewController: UITableViewController {
|
||||
private var generation = 0
|
||||
private var isWorking = false
|
||||
|
||||
init() {
|
||||
init(attachLocalStore: Bool = false) {
|
||||
self.attachLocalStore = attachLocalStore
|
||||
super.init(style: .insetGrouped)
|
||||
title = "Connect Store"
|
||||
title = attachLocalStore ? "Add Git Remote" : "Connect Store"
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
configureFields()
|
||||
}
|
||||
@@ -6096,7 +6570,7 @@ private final class OnboardingViewController: UITableViewController {
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
branches.isEmpty ? 2 : 3
|
||||
attachLocalStore ? 2 : (branches.isEmpty ? 2 : 3)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
@@ -6113,7 +6587,7 @@ private final class OnboardingViewController: UITableViewController {
|
||||
) -> String? {
|
||||
switch section {
|
||||
case 0: "HTTPS Repository"
|
||||
case 1: branches.isEmpty ? nil : "Remote"
|
||||
case 1: attachLocalStore || branches.isEmpty ? nil : "Remote"
|
||||
default: "Local Clone"
|
||||
}
|
||||
}
|
||||
@@ -6133,7 +6607,10 @@ private final class OnboardingViewController: UITableViewController {
|
||||
return fieldCell(indexPath.row)
|
||||
}
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
if branches.isEmpty {
|
||||
if attachLocalStore {
|
||||
cell.textLabel?.text = "Connect Local Store"
|
||||
cell.textLabel?.textColor = view.tintColor
|
||||
} else if branches.isEmpty {
|
||||
cell.textLabel?.text = "Discover Branches"
|
||||
cell.textLabel?.textColor = view.tintColor
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
@@ -6155,7 +6632,9 @@ private final class OnboardingViewController: UITableViewController {
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard !isWorking else { return }
|
||||
if branches.isEmpty, indexPath.section == 1 {
|
||||
if attachLocalStore, indexPath.section == 1 {
|
||||
connectLocalStore()
|
||||
} else if branches.isEmpty, indexPath.section == 1 {
|
||||
discoverBranches()
|
||||
} else if indexPath.section == 1 {
|
||||
chooseBranch(from: tableView.cellForRow(at: indexPath))
|
||||
@@ -6221,6 +6700,17 @@ private final class OnboardingViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func connectLocalStore() {
|
||||
do {
|
||||
let operation = try makeOperation()
|
||||
begin(operation, title: "Connecting Git Remote") { operation in
|
||||
.completed(try operation.connectLocalStore())
|
||||
}
|
||||
} catch {
|
||||
present(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func runSetup(useExisting: Bool) {
|
||||
guard branches.indices.contains(selectedBranch) else { return }
|
||||
do {
|
||||
@@ -6290,7 +6780,14 @@ private final class OnboardingViewController: UITableViewController {
|
||||
tokenField.text = nil
|
||||
let alert = UIAlertController(title: outcome.title, message: outcome.detail, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
if self?.attachLocalStore == true {
|
||||
NotificationCenter.default.post(
|
||||
name: .ironStorageConfigurationDidChange,
|
||||
object: nil
|
||||
)
|
||||
} else {
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
case let .failure(failure):
|
||||
|
||||
@@ -9,7 +9,7 @@ actually exercised.
|
||||
|
||||
| Area | Implemented boundary | Automated evidence | Manual release evidence |
|
||||
| --- | --- | --- | --- |
|
||||
| Home tab, remote activity, pull-to-refresh | UIKit renders Rust `MobileHomePage` and typed Commit/Fetch/Pull/Push actions | `mobile_home` unit tests, `git_embedded`, and `crates/apple` bridge tests | Exercise refresh and all four actions against a disposable HTTPS remote, including expired application-token recovery |
|
||||
| Home tab, local history, and optional remote activity | UIKit renders Rust `MobileHomePage` and typed Commit/Fetch/Pull/Push actions | `mobile_home` unit tests, `git_embedded`, and `crates/apple` bridge tests | Verify a clean local store first, then attach a disposable HTTPS remote and exercise refresh and all four actions, including expired application-token recovery |
|
||||
| Passwords tab, navigation, view, and edit | Rust supplies typed paths, fields, drafts, revisions, and mutations; UIKit presents them | `mobile_passwords`, `entry_documents`, `mobile_mutation`, and `mobile_authentication` tests | Open nested entries, edit/save/cancel, and confirm stale-edit recovery |
|
||||
| Swipe move/copy/delete | Rust plans destinations, collisions, dirty-editor handling, and commits; UIKit also exposes accessibility actions | `plans_destinations_collisions_hidden_paths_and_stale_revisions` and authentication serialization tests | Exercise swipe and VoiceOver alternatives; confirm destructive prompts |
|
||||
| TOTP tab and sharing | Rust discovers, caches, parses, selects, and generates OTP data | `mobile_totp`, `otp`, and `mobile_watch` integration tests | Discover entries, read a code, change Watch selection, and verify countdown |
|
||||
@@ -46,8 +46,7 @@ Git, non-TOTP Watch features, and production AutoFill behavior.
|
||||
- Face ID and camera access have purpose strings in `project.yml`. No custom
|
||||
entitlement is currently required: default signing supplies each target's
|
||||
application identifier and Keychain group, WatchConnectivity needs no added
|
||||
capability, and no App Group is used. Alternative-distribution entitlements
|
||||
belong to the final distribution issue.
|
||||
capability, and no App Group is used.
|
||||
|
||||
`cargo test -p ironstorage --test apple_mobile_audit` enforces the source and privacy-manifest
|
||||
parts of this review so those boundaries cannot silently regress.
|
||||
@@ -83,7 +82,8 @@ the tester is ready to enter the passphrase again.
|
||||
## Manual evidence still required
|
||||
|
||||
Record the device model/OS, build commit, and pass/fail result for each manual
|
||||
row above. Physical paired-Watch deployment and WatchConnectivity testing are
|
||||
currently skipped by project direction, so issues closed without that evidence
|
||||
must carry the `untested` label. Do not describe a simulator build or an opened
|
||||
Xcode project as device validation.
|
||||
row above. Simulator validation is required during implementation. The final
|
||||
distribution issue remains open until the approved App Store build is installed
|
||||
and verified on the paired physical iPhone and Apple Watch. Never overwrite that
|
||||
installation with a development-signed build, and do not describe a simulator
|
||||
build or an opened Xcode project as physical-device validation.
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>WKApplication</key>
|
||||
<true/>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
|
||||
@@ -4,6 +4,10 @@ options:
|
||||
settings:
|
||||
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
||||
SWIFT_VERSION: "5.0"
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
DEVELOPMENT_TEAM: MU22FMRGK8
|
||||
MARKETING_VERSION: "0.1.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
targets:
|
||||
IronStorage:
|
||||
type: application
|
||||
@@ -11,8 +15,6 @@ targets:
|
||||
deploymentTarget: "17.0"
|
||||
settings:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage
|
||||
MARKETING_VERSION: "0.1.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
TARGETED_DEVICE_FAMILY: "1"
|
||||
SWIFT_OBJC_BRIDGING_HEADER: IronStorage-Bridging-Header.h
|
||||
LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust"
|
||||
@@ -21,6 +23,8 @@ targets:
|
||||
path: Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: IronStorage
|
||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
NSFaceIDUsageDescription: Unlock your GPG key for protected password operations.
|
||||
NSCameraUsageDescription: Scan GPG key transfer QR codes that you choose to import.
|
||||
@@ -29,10 +33,14 @@ targets:
|
||||
- UIInterfaceOrientationPortrait
|
||||
sources:
|
||||
- Assets.xcassets
|
||||
- Resources/App/License.txt
|
||||
- Resources/App/PrivacyInfo.xcprivacy
|
||||
- Resources/App/ThirdPartyLicenses.txt
|
||||
- Sources/App
|
||||
- Generated/ironstorage_apple.swift
|
||||
dependencies:
|
||||
- target: IronStorageAutoFill
|
||||
embed: true
|
||||
- target: IronStorageWatch
|
||||
embed: true
|
||||
preBuildScripts:
|
||||
@@ -57,6 +65,9 @@ targets:
|
||||
path: AutoFill-Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: IronStorage AutoFill
|
||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
NSExtension:
|
||||
NSExtensionPointIdentifier: com.apple.authentication-services-credential-provider-ui
|
||||
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).CredentialProviderViewController
|
||||
@@ -87,6 +98,9 @@ targets:
|
||||
path: Watch-Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: IronStorage
|
||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
WKApplication: true
|
||||
WKCompanionAppBundleIdentifier: de.rfc1437.ironstorage
|
||||
sources:
|
||||
|
||||