Implement iPhone and Apple Watch preferences (#53)

This commit is contained in:
2026-08-15 22:37:31 +02:00
parent 81886756a2
commit affdb55519
15 changed files with 1409 additions and 60 deletions

View File

@@ -5,6 +5,12 @@
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "AppIcon.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {

View File

@@ -629,10 +629,16 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
func manualLock() throws
func mobileAppearance() throws -> MobileAppearance
func performEntryMutation(request: MobileMutationRequest) throws -> MobileMutationOutcome
func preferences(watchSupported: Bool, watchPaired: Bool, watchAppInstalled: Bool) throws -> MobilePreferences
func prepareEntryMutation(path: String, action: MobileMutationAction) throws -> MobileMutationPlan
func removeApplicationToken() throws
func removeEntryEditorField(editor: UInt64, field: UInt64) throws -> MobileEntryEditorPage
func reorderEntryEditorField(editor: UInt64, field: UInt64, index: UInt32) throws -> MobileEntryEditorPage
@@ -645,10 +651,14 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
func searchCachedTotpPage(query: String) throws -> MobileTotpPage?
func setAuthenticationTimeout(seconds: UInt64) throws
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
func setGitIdentity(name: String, email: String) throws -> MobileGitIdentity
func setMobileAppearance(appearance: MobileAppearance) throws
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
func state() throws -> MobileAuthenticationState
@@ -852,6 +862,15 @@ open func manualLock()throws {try rustCallWithError(FfiConverterTypeMobileAuth
}
}
open func mobileAppearance()throws -> MobileAppearance {
return try FfiConverterTypeMobileAppearance_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_mobile_appearance(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}
open func performEntryMutation(request: MobileMutationRequest)throws -> MobileMutationOutcome {
return try FfiConverterTypeMobileMutationOutcome_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -862,6 +881,18 @@ open func performEntryMutation(request: MobileMutationRequest)throws -> MobileM
})
}
open func preferences(watchSupported: Bool, watchPaired: Bool, watchAppInstalled: Bool)throws -> MobilePreferences {
return try FfiConverterTypeMobilePreferences_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_preferences(
self.uniffiCloneHandle(),
FfiConverterBool.lower(watchSupported),
FfiConverterBool.lower(watchPaired),
FfiConverterBool.lower(watchAppInstalled),uniffiCallStatus
)
})
}
open func prepareEntryMutation(path: String, action: MobileMutationAction)throws -> MobileMutationPlan {
return try FfiConverterTypeMobileMutationPlan_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -873,6 +904,14 @@ open func prepareEntryMutation(path: String, action: MobileMutationAction)throws
})
}
open func removeApplicationToken()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(
self.uniffiCloneHandle(),uniffiCallStatus
)
}
}
open func removeEntryEditorField(editor: UInt64, field: UInt64)throws -> MobileEntryEditorPage {
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -940,6 +979,15 @@ open func searchCachedTotpPage(query: String)throws -> MobileTotpPage? {
})
}
open func setAuthenticationTimeout(seconds: UInt64)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_authentication_timeout(
self.uniffiCloneHandle(),
FfiConverterUInt64.lower(seconds),uniffiCallStatus
)
}
}
open func setBiometricUnlock(enabled: Bool)throws -> MobileAuthenticationState {
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -961,6 +1009,15 @@ open func setGitIdentity(name: String, email: String)throws -> MobileGitIdentit
})
}
open func setMobileAppearance(appearance: MobileAppearance)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_mobile_appearance(
self.uniffiCloneHandle(),
FfiConverterTypeMobileAppearance_lower(appearance),uniffiCallStatus
)
}
}
open func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64)throws -> MobileTotpDetail {
return try FfiConverterTypeMobileTotpDetail_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -3962,6 +4019,104 @@ public func FfiConverterTypeMobilePasswordRow_lower(_ value: MobilePasswordRow)
}
public struct MobilePreferences: Equatable, Hashable {
public var repositoryTitle: String
public var repositoryUrl: String
public var serverTitle: String
public var serverIdentity: String
public var applicationAccount: String?
public var defaultKeyTitle: String
public var defaultKeyFingerprint: String
public var authenticationTimeoutSeconds: UInt64
public var biometricUnlockEnabled: Bool
public var appearance: MobileAppearance
public var watchState: MobileWatchPreferenceState
public var watchTitle: String
public var watchDetail: String
// 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) {
self.repositoryTitle = repositoryTitle
self.repositoryUrl = repositoryUrl
self.serverTitle = serverTitle
self.serverIdentity = serverIdentity
self.applicationAccount = applicationAccount
self.defaultKeyTitle = defaultKeyTitle
self.defaultKeyFingerprint = defaultKeyFingerprint
self.authenticationTimeoutSeconds = authenticationTimeoutSeconds
self.biometricUnlockEnabled = biometricUnlockEnabled
self.appearance = appearance
self.watchState = watchState
self.watchTitle = watchTitle
self.watchDetail = watchDetail
}
}
#if compiler(>=6)
extension MobilePreferences: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePreferences: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePreferences {
return
try MobilePreferences(
repositoryTitle: FfiConverterString.read(from: &buf),
repositoryUrl: FfiConverterString.read(from: &buf),
serverTitle: FfiConverterString.read(from: &buf),
serverIdentity: FfiConverterString.read(from: &buf),
applicationAccount: FfiConverterOptionString.read(from: &buf),
defaultKeyTitle: FfiConverterString.read(from: &buf),
defaultKeyFingerprint: FfiConverterString.read(from: &buf),
authenticationTimeoutSeconds: FfiConverterUInt64.read(from: &buf),
biometricUnlockEnabled: FfiConverterBool.read(from: &buf),
appearance: FfiConverterTypeMobileAppearance.read(from: &buf),
watchState: FfiConverterTypeMobileWatchPreferenceState.read(from: &buf),
watchTitle: FfiConverterString.read(from: &buf),
watchDetail: FfiConverterString.read(from: &buf)
)
}
public static func write(_ value: MobilePreferences, into buf: inout [UInt8]) {
FfiConverterString.write(value.repositoryTitle, into: &buf)
FfiConverterString.write(value.repositoryUrl, into: &buf)
FfiConverterString.write(value.serverTitle, into: &buf)
FfiConverterString.write(value.serverIdentity, into: &buf)
FfiConverterOptionString.write(value.applicationAccount, into: &buf)
FfiConverterString.write(value.defaultKeyTitle, into: &buf)
FfiConverterString.write(value.defaultKeyFingerprint, into: &buf)
FfiConverterUInt64.write(value.authenticationTimeoutSeconds, into: &buf)
FfiConverterBool.write(value.biometricUnlockEnabled, into: &buf)
FfiConverterTypeMobileAppearance.write(value.appearance, into: &buf)
FfiConverterTypeMobileWatchPreferenceState.write(value.watchState, into: &buf)
FfiConverterString.write(value.watchTitle, into: &buf)
FfiConverterString.write(value.watchDetail, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePreferences_lift(_ buf: RustBuffer) throws -> MobilePreferences {
return try FfiConverterTypeMobilePreferences.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePreferences_lower(_ value: MobilePreferences) -> RustBuffer {
return FfiConverterTypeMobilePreferences.lower(value)
}
public struct MobileShell: Equatable, Hashable {
public var selectedTab: MobileTab
public var pages: [MobilePage]
@@ -4351,6 +4506,79 @@ public func FfiConverterTypeMobileWatchSnapshotStatus_lower(_ value: MobileWatch
public enum MobileAppearance: Equatable, Hashable {
case system
case light
case dark
}
#if compiler(>=6)
extension MobileAppearance: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileAppearance: FfiConverterRustBuffer {
typealias SwiftType = MobileAppearance
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileAppearance {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .system
case 2: return .light
case 3: return .dark
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobileAppearance, into buf: inout [UInt8]) {
switch value {
case .system:
writeInt(&buf, Int32(1))
case .light:
writeInt(&buf, Int32(2))
case .dark:
writeInt(&buf, Int32(3))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileAppearance_lift(_ buf: RustBuffer) throws -> MobileAppearance {
return try FfiConverterTypeMobileAppearance.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileAppearance_lower(_ value: MobileAppearance) -> RustBuffer {
return FfiConverterTypeMobileAppearance.lower(value)
}
public enum MobileAuthenticationErrorKind: Equatable, Hashable {
case passphraseRequired
@@ -6383,6 +6611,93 @@ public func FfiConverterTypeMobileTotpDiscoveryPhase_lower(_ value: MobileTotpDi
public enum MobileWatchPreferenceState: Equatable, Hashable {
case unsupported
case notPaired
case appNotInstalled
case ready
case pending
}
#if compiler(>=6)
extension MobileWatchPreferenceState: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileWatchPreferenceState: FfiConverterRustBuffer {
typealias SwiftType = MobileWatchPreferenceState
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileWatchPreferenceState {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .unsupported
case 2: return .notPaired
case 3: return .appNotInstalled
case 4: return .ready
case 5: return .pending
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobileWatchPreferenceState, into buf: inout [UInt8]) {
switch value {
case .unsupported:
writeInt(&buf, Int32(1))
case .notPaired:
writeInt(&buf, Int32(2))
case .appNotInstalled:
writeInt(&buf, Int32(3))
case .ready:
writeInt(&buf, Int32(4))
case .pending:
writeInt(&buf, Int32(5))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileWatchPreferenceState_lift(_ buf: RustBuffer) throws -> MobileWatchPreferenceState {
return try FfiConverterTypeMobileWatchPreferenceState.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileWatchPreferenceState_lower(_ value: MobileWatchPreferenceState) -> RustBuffer {
return FfiConverterTypeMobileWatchPreferenceState.lower(value)
}
public enum MobileWatchSnapshotState: Equatable, Hashable {
case unavailable
@@ -7221,12 +7536,21 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock() != 57220) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_mobile_appearance() != 3564) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_perform_entry_mutation() != 7053) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_preferences() != 16444) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation() != 6234) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_application_token() != 11452) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_entry_editor_field() != 12238) {
return InitializationResult.apiChecksumMismatch
}
@@ -7245,12 +7569,18 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page() != 13608) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_authentication_timeout() != 48083) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity() != 62371) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_mobile_appearance() != 12133) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
return InitializationResult.apiChecksumMismatch
}

View File

@@ -318,16 +318,31 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_git_identity(
void uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_mobile_appearance(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_perform_entry_mutation(uint64_t ptr, RustBuffer request, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREFERENCES
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREFERENCES
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_preferences(uint64_t ptr, int8_t watch_supported, int8_t watch_paired, int8_t watch_app_installed, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_entry_mutation(uint64_t ptr, RustBuffer path, RustBuffer action, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
void uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_entry_editor_field(uint64_t ptr, uint64_t editor, uint64_t field, RustCallStatus *_Nonnull out_status
@@ -358,6 +373,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_save_entry_ed
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_search_cached_totp_page(uint64_t ptr, RustBuffer query, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
void uniffi_ironstorage_apple_fn_method_mobileauthentication_set_authentication_timeout(uint64_t ptr, uint64_t seconds, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(uint64_t ptr, int8_t enabled, RustCallStatus *_Nonnull out_status
@@ -368,6 +388,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_git_identity(uint64_t ptr, RustBuffer name, RustBuffer email, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
void uniffi_ironstorage_apple_fn_method_mobileauthentication_set_mobile_appearance(uint64_t ptr, RustBuffer appearance, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(uint64_t ptr, RustBuffer path, int8_t shared, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
@@ -1026,18 +1051,36 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_git_ident
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_mobile_appearance(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_perform_entry_mutation(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREFERENCES
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREFERENCES
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_preferences(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_application_token(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
@@ -1074,6 +1117,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_save_entr
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SEARCH_CACHED_TOTP_PAGE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_authentication_timeout(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
@@ -1086,6 +1135,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biome
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_GIT_IDENTITY
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_mobile_appearance(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED

View File

@@ -2,6 +2,7 @@ import UIKit
import AVFoundation
import Vision
import VisionKit
import WatchConnectivity
extension Notification.Name {
static let ironStorageLocalStoreDidChange = Notification.Name(
@@ -107,6 +108,13 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
}
tabs.viewControllers = navigationControllers
tabs.delegate = self
if let appearance = try? authentication?.mobileAppearance() {
tabs.overrideUserInterfaceStyle = switch appearance {
case .system: .unspecified
case .light: .light
case .dark: .dark
}
}
restoreSelectedTab()
monitorAuthentication()
return tabs
@@ -844,13 +852,17 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
}
@MainActor
private final class PreferencesViewController: UITableViewController, MobileTabRoot {
private final class PreferencesViewController: UITableViewController, MobileTabRoot,
WCSessionDelegate
{
fileprivate let shellTab = MobileTab.preferences
private var page: MobilePage
private let authentication: MobileAuthentication?
private let keyTransfer = try? mobileKeyTransfer()
private var state: MobileAuthenticationState?
private var preferences: MobilePreferences?
private var gitIdentity: MobileGitIdentity?
private var watchSession: WCSession?
private var preferenceTask: Task<Void, Never>?
private var loadTask: Task<Void, Never>?
private var loadGeneration = 0
@@ -859,6 +871,12 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
self.page = page
self.authentication = authentication
super.init(style: .insetGrouped)
if WCSession.isSupported() {
let session = WCSession.default
watchSession = session
session.delegate = self
session.activate()
}
title = page.title
navigationItem.largeTitleDisplayMode = .always
navigationItem.rightBarButtonItem = UIBarButtonItem(
@@ -881,6 +899,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
}
deinit {
watchSession?.delegate = nil
preferenceTask?.cancel()
loadTask?.cancel()
NotificationCenter.default.removeObserver(self)
@@ -892,14 +911,18 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
}
override func numberOfSections(in tableView: UITableView) -> Int {
page.state == .ready ? 4 : 0
page.state == .ready ? 7 : 0
}
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int
) -> Int {
section == 1 ? 2 : (section == 3 ? 2 : 1)
switch section {
case 0: 2
case 1, 2, 3: 3
default: 1
}
}
override func tableView(
@@ -907,10 +930,13 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
titleForHeaderInSection section: Int
) -> String? {
switch section {
case 0: "Git Commit Identity"
case 1: "GPG Key Transfer"
case 2: "Secure Unlock"
default: "Authentication Session"
case 0: "Password Store"
case 1: "Application Token"
case 2: "GPG Key & Recovery"
case 3: "Authentication"
case 4: "Appearance"
case 5: "Apple Watch"
default: "Git Commit Identity"
}
}
@@ -918,16 +944,22 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
_ tableView: UITableView,
titleForFooterInSection section: Int
) -> String? {
if section == 0 {
return "Used as the author for commits created by the iPhone app."
switch section {
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."
case 2:
"Import provides initial or recovery key setup. Private-key transfers require explicit confirmation and passphrase validation."
case 3:
"Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
case 5:
"Pairing is read from the device; TOTP selection and synchronization state come from Rust."
case 6:
"Used as the author for commits created by the iPhone app."
default:
nil
}
if section == 1 {
return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
}
if section == 2 {
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
}
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
}
override func tableView(
@@ -936,24 +968,58 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
var content = cell.defaultContentConfiguration()
if indexPath.section == 0 {
content.image = UIImage(systemName: "person.crop.circle")
content.text = gitIdentity?.name ?? "Git Commit Identity"
content.secondaryText = gitIdentity?.email ?? "Set the commit author name and email"
switch (indexPath.section, indexPath.row) {
case (0, 0):
content.image = UIImage(systemName: "externaldrive.fill")
content.text = preferences?.repositoryTitle ?? "Password Store"
content.secondaryText = preferences?.repositoryUrl ?? "Repository unavailable"
cell.selectionStyle = .none
case (0, 1):
content.image = UIImage(systemName: "server.rack")
content.text = preferences?.serverTitle ?? "HTTPS Server"
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"
cell.selectionStyle = .none
case (1, 1):
content.image = UIImage(systemName: "arrow.triangle.2.circlepath")
content.text = "Replace Token"
content.secondaryText = "Store a new HTTPS application token"
cell.accessoryType = .disclosureIndicator
cell.isUserInteractionEnabled = authentication != nil
cell.contentView.alpha = authentication == nil ? 0.45 : 1
} else if indexPath.section == 1 {
let importing = indexPath.row == 0
case (1, 2):
content.image = UIImage(systemName: "trash")
content.text = "Remove Token"
content.textProperties.color = .systemRed
content.secondaryText = "Disable authenticated Git network operations"
cell.accessoryType = .disclosureIndicator
cell.isUserInteractionEnabled = preferences?.applicationAccount != nil
cell.contentView.alpha = preferences?.applicationAccount == nil ? 0.45 : 1
case (2, 0):
content.image = UIImage(systemName: "person.badge.key.fill")
content.text = preferences?.defaultKeyTitle ?? "Default GPG Key"
content.secondaryText = preferences?.defaultKeyFingerprint ?? "Key unavailable"
cell.selectionStyle = .none
case (2, 1), (2, 2):
let importing = indexPath.row == 1
content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
content.text = importing ? "Import GPG Key" : "Export GPG Key"
content.secondaryText = importing
? "Scan one or more transfer QR codes"
? "Scan an initial or recovery key transfer"
: "Display public or private key armor"
cell.accessoryType = .disclosureIndicator
cell.isUserInteractionEnabled = keyTransfer != nil
cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
} else if indexPath.section == 2 {
case (3, 0):
content.image = UIImage(systemName: "timer")
content.text = "Inactivity Timeout"
content.secondaryText = "\(preferences?.authenticationTimeoutSeconds ?? 0) seconds"
cell.accessoryType = .disclosureIndicator
case (3, 1):
content.image = UIImage(systemName: "faceid")
content.text = "Biometric Unlock"
content.secondaryText = state?.biometricUnlockEnabled == true
@@ -966,37 +1032,51 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
toggle.accessibilityLabel = "Biometric Unlock"
cell.accessoryView = toggle
cell.selectionStyle = .none
} else if indexPath.row == 0 {
case (3, 2):
let unlocked = state?.unlocked == true
content.image = UIImage(systemName: unlocked ? "lock.open.fill" : "lock.fill")
content.text = unlocked ? "Unlocked" : "Locked"
content.secondaryText = unlocked
? "Locks in \(state?.remainingSeconds ?? 0) seconds without activity"
: "Protected content is masked"
cell.selectionStyle = .none
} else {
content.image = UIImage(systemName: "lock.fill")
content.text = "Lock Now"
content.textProperties.color = .systemRed
content.secondaryText = "Revoke all active authentication handles"
content.secondaryText = unlocked
? "Revoke all active authentication handles"
: "IronStorage is already locked"
cell.accessoryType = .disclosureIndicator
cell.isUserInteractionEnabled = unlocked
cell.contentView.alpha = unlocked ? 1 : 0.45
case (4, 0):
content.image = UIImage(systemName: "circle.lefthalf.filled")
content.text = "App Appearance"
content.secondaryText = appearanceTitle(preferences?.appearance ?? .system)
cell.accessoryType = .disclosureIndicator
case (5, 0):
content.image = UIImage(systemName: watchImage(preferences?.watchState))
content.text = preferences?.watchTitle ?? "Checking Apple Watch"
content.secondaryText = preferences?.watchDetail ?? "Reading pairing state"
cell.accessoryType = .disclosureIndicator
default:
content.image = UIImage(systemName: "person.crop.circle")
content.text = gitIdentity?.name ?? "Git Commit Identity"
content.secondaryText = gitIdentity?.email ?? "Set the commit author name and email"
cell.accessoryType = .disclosureIndicator
cell.isUserInteractionEnabled = state?.unlocked == true
cell.contentView.alpha = state?.unlocked == true ? 1 : 0.45
}
content.secondaryTextProperties.numberOfLines = 0
cell.contentConfiguration = content
cell.accessibilityLabel = [content.text, content.secondaryText]
.compactMap { $0 }
.joined(separator: ", ")
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.section == 0 {
editGitIdentity()
return
}
if indexPath.section == 1 {
switch (indexPath.section, indexPath.row) {
case (1, 1):
tokenUpdateRequested()
case (1, 2):
removeTokenRequested()
case (2, 1), (2, 2):
guard let keyTransfer else { return }
if indexPath.row == 0 {
if indexPath.row == 1 {
requestKeyScanner(keyTransfer)
} else {
navigationController?.pushViewController(
@@ -1004,9 +1084,23 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
animated: true
)
}
return
case (3, 0):
editAuthenticationTimeout()
case (3, 2):
lockNow()
case (4, 0):
chooseAppearance()
case (5, 0):
openWatchPreference()
case (6, 0):
editGitIdentity()
default:
break
}
guard indexPath.section == 3, indexPath.row == 1, let authentication else { return }
}
private func lockNow() {
guard let authentication else { return }
do {
try authentication.manualLock()
refreshState()
@@ -1188,7 +1282,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
switch result {
case let .success(identity):
gitIdentity = identity
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
tableView.reloadSections(IndexSet(integer: 6), with: .automatic)
UIAccessibility.post(
notification: .announcement,
argument: "Git commit identity updated"
@@ -1199,6 +1293,147 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
}
}
private func removeTokenRequested() {
guard preferences?.applicationAccount != nil else { return }
let alert = UIAlertController(
title: "Remove Application Token?",
message: "Local password browsing remains available, but Fetch, Pull, and Push will fail until a replacement token is stored.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Remove Token", style: .destructive) {
[weak self] _ in
self?.runPreferenceUpdate(announcement: "Application token removed") {
try self?.authentication?.removeApplicationToken()
}
})
present(alert, animated: true)
}
private func editAuthenticationTimeout() {
let alert = UIAlertController(
title: "Inactivity Timeout",
message: "Enter a shared timeout from 1 to 86400 seconds.",
preferredStyle: .alert
)
alert.addTextField { [preferences] field in
field.keyboardType = .numberPad
field.text = String(preferences?.authenticationTimeoutSeconds ?? 120)
field.accessibilityLabel = "Timeout in seconds"
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Save", style: .default) {
[weak self, weak alert] _ in
let seconds = UInt64(alert?.textFields?.first?.text ?? "") ?? 0
self?.runPreferenceUpdate(announcement: "Inactivity timeout updated") {
try self?.authentication?.setAuthenticationTimeout(seconds: seconds)
}
})
present(alert, animated: true)
}
private func chooseAppearance() {
let alert = UIAlertController(title: "App Appearance", message: nil, preferredStyle: .actionSheet)
for appearance in [MobileAppearance.system, .light, .dark] {
let selected = appearance == preferences?.appearance
alert.addAction(UIAlertAction(
title: "\(selected ? "" : "")\(appearanceTitle(appearance))",
style: .default
) { [weak self] _ in
self?.runPreferenceUpdate(announcement: "Appearance updated") {
try self?.authentication?.setMobileAppearance(appearance: appearance)
}
})
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.popoverPresentationController?.sourceView = tableView
alert.popoverPresentationController?.sourceRect = tableView.bounds
present(alert, animated: true)
}
private func runPreferenceUpdate(
announcement: String,
operation: @escaping @Sendable () throws -> Void
) {
preferenceTask?.cancel()
preferenceTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
try operation()
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:
refreshState()
UIAccessibility.post(notification: .announcement, argument: announcement)
case let .failure(failure):
presentAuthenticationFailure(failure)
}
}
}
private func openWatchPreference() {
switch preferences?.watchState {
case .ready, .pending:
selectTotpTab()
case .notPaired:
presentWatchGuidance(
title: "Pair an Apple Watch",
message: "Open the Watch app on this iPhone and pair an Apple Watch before configuring IronStorage synchronization."
)
case .appNotInstalled:
presentWatchGuidance(
title: "Install IronStorage on Apple Watch",
message: "Open the Watch app on this iPhone and install the IronStorage companion on the paired watch."
)
default:
presentWatchGuidance(
title: "Apple Watch Unavailable",
message: "Apple Watch connectivity is unavailable on this device."
)
}
}
private func presentWatchGuidance(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func selectTotpTab() {
guard let controllers = tabBarController?.viewControllers else { return }
tabBarController?.selectedIndex = controllers.firstIndex { controller in
guard
let navigation = controller as? UINavigationController,
let root = navigation.viewControllers.first as? MobileTabRoot
else { return false }
return root.shellTab == .totp
} ?? tabBarController?.selectedIndex ?? 0
}
private func appearanceTitle(_ appearance: MobileAppearance) -> String {
switch appearance {
case .system: "System"
case .light: "Light"
case .dark: "Dark"
}
}
private func watchImage(_ state: MobileWatchPreferenceState?) -> String {
switch state {
case .ready: "applewatch.radiowaves.left.and.right"
case .pending: "arrow.triangle.2.circlepath"
case .notPaired, .appNotInstalled: "applewatch.slash"
default: "applewatch"
}
}
@objc private func authenticationDidChange() {
refreshState()
}
@@ -1244,8 +1479,38 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
private func refreshState() {
state = try? authentication?.state()
gitIdentity = try? authentication?.gitIdentity()
preferences = try? authentication?.preferences(
watchSupported: WCSession.isSupported(),
watchPaired: watchSession?.isPaired ?? false,
watchAppInstalled: watchSession?.isWatchAppInstalled ?? false
)
if let appearance = preferences?.appearance {
tabBarController?.overrideUserInterfaceStyle = switch appearance {
case .system: .unspecified
case .light: .light
case .dark: .dark
}
}
tableView.reloadData()
}
nonisolated func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?
) {
Task { @MainActor [weak self] in self?.refreshState() }
}
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
nonisolated func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
nonisolated func sessionWatchStateDidChange(_ session: WCSession) {
Task { @MainActor [weak self] in self?.refreshState() }
}
}
@MainActor

View File

@@ -20,6 +20,8 @@
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>WKApplication</key>
<true/>
<key>WKCompanionAppBundleIdentifier</key>
<string>de.rfc1437.ironstorage</string>
</dict>

View File

@@ -31,6 +31,9 @@ targets:
- Assets.xcassets
- Sources/App
- Generated/ironstorage_apple.swift
dependencies:
- target: IronStorageWatch
embed: true
preBuildScripts:
- name: Build Rust core
basedOnDependencyAnalysis: false
@@ -78,6 +81,8 @@ targets:
path: Watch-Info.plist
properties:
CFBundleDisplayName: IronStorage
WKApplication: true
WKCompanionAppBundleIdentifier: de.rfc1437.ironstorage
sources:
- Assets.xcassets
- Sources/Watch