Build native iPhone application shell
This commit is contained in:
BIN
apple/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
BIN
apple/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
14
apple/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
14
apple/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
6
apple/Assets.xcassets/Contents.json
Normal file
6
apple/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -507,6 +507,416 @@ fileprivate struct FfiConverterString: FfiConverter {
|
|||||||
writeBytes(&buf, value.utf8)
|
writeBytes(&buf, value.utf8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobilePage: Equatable, Hashable {
|
||||||
|
public var tab: MobileTab
|
||||||
|
public var title: String
|
||||||
|
public var systemImage: String
|
||||||
|
public var selectedSystemImage: String
|
||||||
|
public var state: MobileShellState
|
||||||
|
public var stateTitle: String
|
||||||
|
public var stateDetail: String
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(tab: MobileTab, title: String, systemImage: String, selectedSystemImage: String, state: MobileShellState, stateTitle: String, stateDetail: String) {
|
||||||
|
self.tab = tab
|
||||||
|
self.title = title
|
||||||
|
self.systemImage = systemImage
|
||||||
|
self.selectedSystemImage = selectedSystemImage
|
||||||
|
self.state = state
|
||||||
|
self.stateTitle = stateTitle
|
||||||
|
self.stateDetail = stateDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePage: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePage: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePage {
|
||||||
|
return
|
||||||
|
try MobilePage(
|
||||||
|
tab: FfiConverterTypeMobileTab.read(from: &buf),
|
||||||
|
title: FfiConverterString.read(from: &buf),
|
||||||
|
systemImage: FfiConverterString.read(from: &buf),
|
||||||
|
selectedSystemImage: FfiConverterString.read(from: &buf),
|
||||||
|
state: FfiConverterTypeMobileShellState.read(from: &buf),
|
||||||
|
stateTitle: FfiConverterString.read(from: &buf),
|
||||||
|
stateDetail: FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePage, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterTypeMobileTab.write(value.tab, into: &buf)
|
||||||
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
|
FfiConverterString.write(value.systemImage, into: &buf)
|
||||||
|
FfiConverterString.write(value.selectedSystemImage, into: &buf)
|
||||||
|
FfiConverterTypeMobileShellState.write(value.state, into: &buf)
|
||||||
|
FfiConverterString.write(value.stateTitle, into: &buf)
|
||||||
|
FfiConverterString.write(value.stateDetail, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePage_lift(_ buf: RustBuffer) throws -> MobilePage {
|
||||||
|
return try FfiConverterTypeMobilePage.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePage_lower(_ value: MobilePage) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePage.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileShell: Equatable, Hashable {
|
||||||
|
public var selectedTab: MobileTab
|
||||||
|
public var pages: [MobilePage]
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(selectedTab: MobileTab, pages: [MobilePage]) {
|
||||||
|
self.selectedTab = selectedTab
|
||||||
|
self.pages = pages
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileShell: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileShell: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileShell {
|
||||||
|
return
|
||||||
|
try MobileShell(
|
||||||
|
selectedTab: FfiConverterTypeMobileTab.read(from: &buf),
|
||||||
|
pages: FfiConverterSequenceTypeMobilePage.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileShell, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterTypeMobileTab.write(value.selectedTab, into: &buf)
|
||||||
|
FfiConverterSequenceTypeMobilePage.write(value.pages, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileShell_lift(_ buf: RustBuffer) throws -> MobileShell {
|
||||||
|
return try FfiConverterTypeMobileShell.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileShell_lower(_ value: MobileShell) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileShell.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public
|
||||||
|
enum MobilePreferenceError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case Configuration(message: String
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
String(reflecting: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePreferenceError: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePreferenceError: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobilePreferenceError
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePreferenceError {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case 1: return .Configuration(
|
||||||
|
message: try FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePreferenceError, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case let .Configuration(message):
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
FfiConverterString.write(message, into: &buf)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePreferenceError_lift(_ buf: RustBuffer) throws -> MobilePreferenceError {
|
||||||
|
return try FfiConverterTypeMobilePreferenceError.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePreferenceError_lower(_ value: MobilePreferenceError) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePreferenceError.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public enum MobileShellState: Equatable, Hashable {
|
||||||
|
|
||||||
|
case loading
|
||||||
|
case empty
|
||||||
|
case ready
|
||||||
|
case locked
|
||||||
|
case error
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileShellState: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileShellState: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobileShellState
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileShellState {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
case 1: return .loading
|
||||||
|
|
||||||
|
case 2: return .empty
|
||||||
|
|
||||||
|
case 3: return .ready
|
||||||
|
|
||||||
|
case 4: return .locked
|
||||||
|
|
||||||
|
case 5: return .error
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileShellState, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
case .loading:
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
|
case .empty:
|
||||||
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
|
||||||
|
case .ready:
|
||||||
|
writeInt(&buf, Int32(3))
|
||||||
|
|
||||||
|
|
||||||
|
case .locked:
|
||||||
|
writeInt(&buf, Int32(4))
|
||||||
|
|
||||||
|
|
||||||
|
case .error:
|
||||||
|
writeInt(&buf, Int32(5))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileShellState_lift(_ buf: RustBuffer) throws -> MobileShellState {
|
||||||
|
return try FfiConverterTypeMobileShellState.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileShellState_lower(_ value: MobileShellState) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileShellState.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public enum MobileTab: Equatable, Hashable {
|
||||||
|
|
||||||
|
case home
|
||||||
|
case passwords
|
||||||
|
case totp
|
||||||
|
case preferences
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileTab: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileTab: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobileTab
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileTab {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
case 1: return .home
|
||||||
|
|
||||||
|
case 2: return .passwords
|
||||||
|
|
||||||
|
case 3: return .totp
|
||||||
|
|
||||||
|
case 4: return .preferences
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileTab, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
case .home:
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
|
case .passwords:
|
||||||
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
|
||||||
|
case .totp:
|
||||||
|
writeInt(&buf, Int32(3))
|
||||||
|
|
||||||
|
|
||||||
|
case .preferences:
|
||||||
|
writeInt(&buf, Int32(4))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileTab_lift(_ buf: RustBuffer) throws -> MobileTab {
|
||||||
|
return try FfiConverterTypeMobileTab.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileTab_lower(_ value: MobileTab) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileTab.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterSequenceTypeMobilePage: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = [MobilePage]
|
||||||
|
|
||||||
|
public static func write(_ value: [MobilePage], into buf: inout [UInt8]) {
|
||||||
|
let len = Int32(value.count)
|
||||||
|
writeInt(&buf, len)
|
||||||
|
for item in value {
|
||||||
|
FfiConverterTypeMobilePage.write(item, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobilePage] {
|
||||||
|
let len: Int32 = try readInt(&buf)
|
||||||
|
var seq = [MobilePage]()
|
||||||
|
seq.reserveCapacity(Int(len))
|
||||||
|
for _ in 0 ..< len {
|
||||||
|
seq.append(try FfiConverterTypeMobilePage.read(from: &buf))
|
||||||
|
}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func mobileShell() -> MobileShell {
|
||||||
|
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_func_mobile_shell(uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
public func mobileShellFixture(state: MobileShellState) -> MobileShell {
|
||||||
|
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_func_mobile_shell_fixture(
|
||||||
|
FfiConverterTypeMobileShellState_lower(state),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
public func productName() -> String {
|
public func productName() -> String {
|
||||||
return try! FfiConverterString.lift(try! rustCall() {
|
return try! FfiConverterString.lift(try! rustCall() {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -514,6 +924,13 @@ public func productName() -> String {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
public func setSelectedMobileTab(tab: MobileTab)throws {try rustCallWithError(FfiConverterTypeMobilePreferenceError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_func_set_selected_mobile_tab(
|
||||||
|
FfiConverterTypeMobileTab_lower(tab),uniffiCallStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum InitializationResult {
|
private enum InitializationResult {
|
||||||
case ok
|
case ok
|
||||||
@@ -530,9 +947,18 @@ private let initializationResult: InitializationResult = {
|
|||||||
if bindings_contract_version != scaffolding_contract_version {
|
if bindings_contract_version != scaffolding_contract_version {
|
||||||
return InitializationResult.contractVersionMismatch
|
return InitializationResult.contractVersionMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_func_mobile_shell() != 42687) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_func_mobile_shell_fixture() != 1649) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_func_product_name() != 43533) {
|
if (uniffi_ironstorage_apple_checksum_func_product_name() != 43533) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab() != 65280) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
|
||||||
return InitializationResult.ok
|
return InitializationResult.ok
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -242,11 +242,27 @@ typedef struct UniffiForeignFutureResultVoid {
|
|||||||
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
|
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_shell(RustCallStatus *_Nonnull out_status
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL_FIXTURE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL_FIXTURE
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_shell_fixture(RustBuffer state, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_PRODUCT_NAME
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_PRODUCT_NAME
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_PRODUCT_NAME
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_PRODUCT_NAME
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_func_product_name(RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_func_product_name(RustCallStatus *_Nonnull out_status
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_SET_SELECTED_MOBILE_TAB
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_SET_SELECTED_MOBILE_TAB
|
||||||
|
void uniffi_ironstorage_apple_fn_func_set_selected_mobile_tab(RustBuffer tab, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUSTBUFFER_ALLOC
|
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUSTBUFFER_ALLOC
|
||||||
@@ -507,12 +523,30 @@ void ffi_ironstorage_apple_rust_future_free_void(uint64_t handle
|
|||||||
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUST_FUTURE_COMPLETE_VOID
|
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUST_FUTURE_COMPLETE_VOID
|
||||||
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUST_FUTURE_COMPLETE_VOID
|
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUST_FUTURE_COMPLETE_VOID
|
||||||
void ffi_ironstorage_apple_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
|
void ffi_ironstorage_apple_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_shell(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL_FIXTURE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL_FIXTURE
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_shell_fixture(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_PRODUCT_NAME
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_PRODUCT_NAME
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_PRODUCT_NAME
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_PRODUCT_NAME
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_func_product_name(void
|
uint16_t uniffi_ironstorage_apple_checksum_func_product_name(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_SET_SELECTED_MOBILE_TAB
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_SET_SELECTED_MOBILE_TAB
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_UNIFFI_CONTRACT_VERSION
|
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_UNIFFI_CONTRACT_VERSION
|
||||||
|
|||||||
@@ -1,20 +1,195 @@
|
|||||||
import SwiftUI
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct IronStorageApp: App {
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
var body: some Scene {
|
var window: UIWindow?
|
||||||
WindowGroup {
|
private var context: AppContext?
|
||||||
ContentView()
|
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||||
|
) -> Bool {
|
||||||
|
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
|
let context = AppContext()
|
||||||
|
self.context = context
|
||||||
|
window.rootViewController = context.makeRootController()
|
||||||
|
window.makeKeyAndVisible()
|
||||||
|
self.window = window
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||||
|
private let tabs = UITabBarController()
|
||||||
|
private var navigationControllers: [UINavigationController] = []
|
||||||
|
private var restoreTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
func makeRootController() -> UIViewController {
|
||||||
|
let shell = mobileShellFixture(state: .loading)
|
||||||
|
navigationControllers = shell.pages.map { page in
|
||||||
|
let root = ShellViewController(page: page)
|
||||||
|
let navigation = UINavigationController(rootViewController: root)
|
||||||
|
navigation.navigationBar.prefersLargeTitles = true
|
||||||
|
navigation.tabBarItem = UITabBarItem(
|
||||||
|
title: page.title,
|
||||||
|
image: UIImage(systemName: page.systemImage),
|
||||||
|
selectedImage: UIImage(systemName: page.selectedSystemImage)
|
||||||
|
)
|
||||||
|
return navigation
|
||||||
|
}
|
||||||
|
tabs.viewControllers = navigationControllers
|
||||||
|
tabs.delegate = self
|
||||||
|
restoreSelectedTab()
|
||||||
|
return tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabBarController(
|
||||||
|
_ tabBarController: UITabBarController,
|
||||||
|
didSelect viewController: UIViewController
|
||||||
|
) {
|
||||||
|
guard
|
||||||
|
let navigation = viewController as? UINavigationController,
|
||||||
|
let root = navigation.viewControllers.first as? ShellViewController
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try setSelectedMobileTab(tab: root.shellTab)
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Selection Was Not Saved",
|
||||||
|
message: error.localizedDescription,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
tabs.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreSelectedTab() {
|
||||||
|
restoreTask?.cancel()
|
||||||
|
restoreTask = Task { [weak self] in
|
||||||
|
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
||||||
|
guard !Task.isCancelled, let self else { return }
|
||||||
|
if let index = shell.pages.firstIndex(where: { $0.tab == shell.selectedTab }) {
|
||||||
|
tabs.selectedIndex = index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ContentView: View {
|
@MainActor
|
||||||
var body: some View {
|
private final class ShellViewController: UITableViewController {
|
||||||
ContentUnavailableView(
|
fileprivate let shellTab: MobileTab
|
||||||
"No Password Store",
|
private var page: MobilePage
|
||||||
systemImage: "lock.shield",
|
private var loadTask: Task<Void, Never>?
|
||||||
description: Text("Open or clone a store in \(productName()) to begin.")
|
private var loadGeneration = 0
|
||||||
)
|
|
||||||
|
init(page: MobilePage) {
|
||||||
|
shellTab = page.tab
|
||||||
|
self.page = page
|
||||||
|
super.init(style: .insetGrouped)
|
||||||
|
title = page.title
|
||||||
|
navigationItem.largeTitleDisplayMode = .always
|
||||||
|
refreshControl = UIRefreshControl()
|
||||||
|
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
loadTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
apply(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
reloadShell()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
loadTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
page.state == .ready ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(
|
||||||
|
_ tableView: UITableView,
|
||||||
|
cellForRowAt indexPath: IndexPath
|
||||||
|
) -> UITableViewCell {
|
||||||
|
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||||
|
var content = cell.defaultContentConfiguration()
|
||||||
|
content.image = UIImage(systemName: page.systemImage)
|
||||||
|
content.text = page.stateTitle
|
||||||
|
content.secondaryText = page.stateDetail
|
||||||
|
content.secondaryTextProperties.numberOfLines = 0
|
||||||
|
cell.contentConfiguration = content
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
cell.accessibilityLabel = "\(page.stateTitle). \(page.stateDetail)"
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refreshRequested() {
|
||||||
|
reloadShell()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadShell() {
|
||||||
|
loadGeneration += 1
|
||||||
|
let generation = loadGeneration
|
||||||
|
loadTask?.cancel()
|
||||||
|
loadTask = Task { [weak self] in
|
||||||
|
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
||||||
|
guard
|
||||||
|
!Task.isCancelled,
|
||||||
|
let self,
|
||||||
|
generation == loadGeneration,
|
||||||
|
let page = shell.pages.first(where: { $0.tab == self.shellTab })
|
||||||
|
else { return }
|
||||||
|
apply(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func apply(_ page: MobilePage) {
|
||||||
|
self.page = page
|
||||||
|
title = page.title
|
||||||
|
refreshControl?.endRefreshing()
|
||||||
|
tableView.reloadData()
|
||||||
|
|
||||||
|
guard page.state != .ready else {
|
||||||
|
contentUnavailableConfiguration = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var configuration = page.state == .loading
|
||||||
|
? UIContentUnavailableConfiguration.loading()
|
||||||
|
: UIContentUnavailableConfiguration.empty()
|
||||||
|
configuration.text = page.stateTitle
|
||||||
|
configuration.secondaryText = page.stateDetail
|
||||||
|
configuration.image = UIImage(systemName: stateImage(page.state))
|
||||||
|
contentUnavailableConfiguration = configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stateImage(_ state: MobileShellState) -> String {
|
||||||
|
switch state {
|
||||||
|
case .loading: "hourglass"
|
||||||
|
case .empty: "lock.shield"
|
||||||
|
case .ready: page.systemImage
|
||||||
|
case .locked: "lock.fill"
|
||||||
|
case .error: "exclamationmark.triangle"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,9 @@ targets:
|
|||||||
UISupportedInterfaceOrientations:
|
UISupportedInterfaceOrientations:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
sources:
|
sources:
|
||||||
|
- Assets.xcassets
|
||||||
- Sources/App
|
- Sources/App
|
||||||
- Generated/ironstorage_apple.swift
|
- Generated/ironstorage_apple.swift
|
||||||
dependencies:
|
|
||||||
- target: IronStorageAutoFill
|
|
||||||
embed: true
|
|
||||||
- target: IronStorageWatch
|
|
||||||
embed: true
|
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
- name: Build Rust core
|
- name: Build Rust core
|
||||||
basedOnDependencyAnalysis: false
|
basedOnDependencyAnalysis: false
|
||||||
|
|||||||
@@ -3,17 +3,181 @@
|
|||||||
|
|
||||||
//! Mechanical UniFFI exports for Apple presentation code.
|
//! Mechanical UniFFI exports for Apple presentation code.
|
||||||
|
|
||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
use ironstorage::{
|
||||||
|
config::ConfigError,
|
||||||
|
mobile::{self, MobileShellState as StorageShellState, MobileTab as StorageTab},
|
||||||
|
};
|
||||||
|
|
||||||
uniffi::setup_scaffolding!();
|
uniffi::setup_scaffolding!();
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
|
pub enum MobileTab {
|
||||||
|
Home,
|
||||||
|
Passwords,
|
||||||
|
Totp,
|
||||||
|
Preferences,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageTab> for MobileTab {
|
||||||
|
fn from(tab: StorageTab) -> Self {
|
||||||
|
match tab {
|
||||||
|
StorageTab::Home => Self::Home,
|
||||||
|
StorageTab::Passwords => Self::Passwords,
|
||||||
|
StorageTab::Totp => Self::Totp,
|
||||||
|
StorageTab::Preferences => Self::Preferences,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MobileTab> for StorageTab {
|
||||||
|
fn from(tab: MobileTab) -> Self {
|
||||||
|
match tab {
|
||||||
|
MobileTab::Home => Self::Home,
|
||||||
|
MobileTab::Passwords => Self::Passwords,
|
||||||
|
MobileTab::Totp => Self::Totp,
|
||||||
|
MobileTab::Preferences => Self::Preferences,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
|
pub enum MobileShellState {
|
||||||
|
Loading,
|
||||||
|
Empty,
|
||||||
|
Ready,
|
||||||
|
Locked,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageShellState> for MobileShellState {
|
||||||
|
fn from(state: StorageShellState) -> Self {
|
||||||
|
match state {
|
||||||
|
StorageShellState::Loading => Self::Loading,
|
||||||
|
StorageShellState::Empty => Self::Empty,
|
||||||
|
StorageShellState::Ready => Self::Ready,
|
||||||
|
StorageShellState::Locked => Self::Locked,
|
||||||
|
StorageShellState::Error => Self::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MobileShellState> for StorageShellState {
|
||||||
|
fn from(state: MobileShellState) -> Self {
|
||||||
|
match state {
|
||||||
|
MobileShellState::Loading => Self::Loading,
|
||||||
|
MobileShellState::Empty => Self::Empty,
|
||||||
|
MobileShellState::Ready => Self::Ready,
|
||||||
|
MobileShellState::Locked => Self::Locked,
|
||||||
|
MobileShellState::Error => Self::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobilePage {
|
||||||
|
pub tab: MobileTab,
|
||||||
|
pub title: String,
|
||||||
|
pub system_image: String,
|
||||||
|
pub selected_system_image: String,
|
||||||
|
pub state: MobileShellState,
|
||||||
|
pub state_title: String,
|
||||||
|
pub state_detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileShell {
|
||||||
|
pub selected_tab: MobileTab,
|
||||||
|
pub pages: Vec<MobilePage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<mobile::MobileShell> for MobileShell {
|
||||||
|
fn from(shell: mobile::MobileShell) -> Self {
|
||||||
|
Self {
|
||||||
|
selected_tab: shell.selected_tab().into(),
|
||||||
|
pages: shell
|
||||||
|
.pages()
|
||||||
|
.iter()
|
||||||
|
.map(|page| MobilePage {
|
||||||
|
tab: page.tab().into(),
|
||||||
|
title: page.title().to_owned(),
|
||||||
|
system_image: page.system_image().to_owned(),
|
||||||
|
selected_system_image: page.selected_system_image().to_owned(),
|
||||||
|
state: page.state().into(),
|
||||||
|
state_title: page.state_title().to_owned(),
|
||||||
|
state_detail: page.state_detail().to_owned(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, uniffi::Error)]
|
||||||
|
pub enum MobilePreferenceError {
|
||||||
|
Configuration { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MobilePreferenceError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Configuration { message } => formatter.write_str(message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for MobilePreferenceError {}
|
||||||
|
|
||||||
|
impl From<ConfigError> for MobilePreferenceError {
|
||||||
|
fn from(error: ConfigError) -> Self {
|
||||||
|
Self::Configuration {
|
||||||
|
message: error.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
pub fn product_name() -> String {
|
pub fn product_name() -> String {
|
||||||
ironstorage::PRODUCT_NAME.to_owned()
|
ironstorage::PRODUCT_NAME.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
pub fn mobile_shell() -> MobileShell {
|
||||||
|
mobile::MobileShell::load().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
pub fn mobile_shell_fixture(state: MobileShellState) -> MobileShell {
|
||||||
|
mobile::MobileShell::fixture(state.into()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
pub fn set_selected_mobile_tab(tab: MobileTab) -> Result<(), MobilePreferenceError> {
|
||||||
|
mobile::store_selected_tab(tab.into()).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::{MobileShellState, MobileTab};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bridge_reads_product_name_from_storage_crate() {
|
fn bridge_reads_product_name_from_storage_crate() {
|
||||||
assert_eq!(super::product_name(), ironstorage::PRODUCT_NAME);
|
assert_eq!(super::product_name(), ironstorage::PRODUCT_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bridge_exposes_every_view_ready_shell_fixture() {
|
||||||
|
for state in [
|
||||||
|
MobileShellState::Loading,
|
||||||
|
MobileShellState::Empty,
|
||||||
|
MobileShellState::Ready,
|
||||||
|
MobileShellState::Locked,
|
||||||
|
MobileShellState::Error,
|
||||||
|
] {
|
||||||
|
let shell = super::mobile_shell_fixture(state);
|
||||||
|
assert_eq!(shell.selected_tab, MobileTab::Home);
|
||||||
|
assert_eq!(shell.pages.len(), 4);
|
||||||
|
assert!(shell.pages.iter().all(|page| page.state == state));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use serde::Deserialize;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT};
|
use crate::authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT};
|
||||||
|
use crate::mobile::MobileTab;
|
||||||
use crate::presentation::{ClipboardTimeout, DEFAULT_CLIPBOARD_TIMEOUT};
|
use crate::presentation::{ClipboardTimeout, DEFAULT_CLIPBOARD_TIMEOUT};
|
||||||
|
|
||||||
const APPLICATION_DIRECTORY: &str = "ironstorage";
|
const APPLICATION_DIRECTORY: &str = "ironstorage";
|
||||||
@@ -33,6 +34,7 @@ pub struct Config {
|
|||||||
editor: Option<EditorCommand>,
|
editor: Option<EditorCommand>,
|
||||||
clipboard_timeout: ClipboardTimeout,
|
clipboard_timeout: ClipboardTimeout,
|
||||||
authentication_timeout: AuthenticationTimeout,
|
authentication_timeout: AuthenticationTimeout,
|
||||||
|
mobile_tab: MobileTab,
|
||||||
git_remotes: Vec<GitRemote>,
|
git_remotes: Vec<GitRemote>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +120,10 @@ impl Config {
|
|||||||
self.authentication_timeout
|
self.authentication_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mobile_tab(&self) -> MobileTab {
|
||||||
|
self.mobile_tab
|
||||||
|
}
|
||||||
|
|
||||||
pub fn git_remotes(&self) -> &[GitRemote] {
|
pub fn git_remotes(&self) -> &[GitRemote] {
|
||||||
&self.git_remotes
|
&self.git_remotes
|
||||||
}
|
}
|
||||||
@@ -136,6 +142,31 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_mobile_tab(&self, tab: MobileTab) -> Result<(), ConfigError> {
|
||||||
|
let mut document = self.document.clone();
|
||||||
|
let root = document
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or_else(|| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
let ui = root
|
||||||
|
.entry("ui")
|
||||||
|
.or_insert_with(|| toml::Value::Table(toml::Table::new()))
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or(ConfigError::InvalidField { field: "ui" })?;
|
||||||
|
ui.insert(
|
||||||
|
"selected_mobile_tab".to_owned(),
|
||||||
|
toml::Value::String(tab.config_value().to_owned()),
|
||||||
|
);
|
||||||
|
let raw = document
|
||||||
|
.clone()
|
||||||
|
.try_into::<RawConfig>()
|
||||||
|
.map_err(|_| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
validate_config(self.source.clone(), document, raw)?.persist()
|
||||||
|
}
|
||||||
|
|
||||||
/// Select a configured remote by name, or the configured default (first
|
/// Select a configured remote by name, or the configured default (first
|
||||||
/// remote) when no name was requested.
|
/// remote) when no name was requested.
|
||||||
pub fn git_remote(&self, requested: Option<&str>) -> Option<&GitRemote> {
|
pub fn git_remote(&self, requested: Option<&str>) -> Option<&GitRemote> {
|
||||||
@@ -605,6 +636,8 @@ struct RawConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
security: RawSecurity,
|
security: RawSecurity,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
ui: RawUi,
|
||||||
|
#[serde(default)]
|
||||||
git: RawGit,
|
git: RawGit,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,6 +647,12 @@ struct RawSecurity {
|
|||||||
inactivity_timeout_seconds: Option<u64>,
|
inactivity_timeout_seconds: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct RawUi {
|
||||||
|
selected_mobile_tab: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum RawEditor {
|
enum RawEditor {
|
||||||
@@ -686,6 +725,13 @@ fn validate_config(
|
|||||||
.map_err(|_| ConfigError::InvalidField {
|
.map_err(|_| ConfigError::InvalidField {
|
||||||
field: "security.inactivity_timeout_seconds",
|
field: "security.inactivity_timeout_seconds",
|
||||||
})?;
|
})?;
|
||||||
|
let mobile_tab = raw
|
||||||
|
.ui
|
||||||
|
.selected_mobile_tab
|
||||||
|
.as_deref()
|
||||||
|
.map(MobileTab::from_config)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
let git_remotes = validate_remotes(raw.git.remotes)?;
|
let git_remotes = validate_remotes(raw.git.remotes)?;
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
@@ -697,6 +743,7 @@ fn validate_config(
|
|||||||
editor,
|
editor,
|
||||||
clipboard_timeout,
|
clipboard_timeout,
|
||||||
authentication_timeout,
|
authentication_timeout,
|
||||||
|
mobile_tab,
|
||||||
git_remotes,
|
git_remotes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -840,6 +887,7 @@ fn validate_known_fields(value: &toml::Value, source: &Path) -> Result<(), Confi
|
|||||||
"editor",
|
"editor",
|
||||||
"clipboard_timeout_seconds",
|
"clipboard_timeout_seconds",
|
||||||
"security",
|
"security",
|
||||||
|
"ui",
|
||||||
"git",
|
"git",
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
@@ -849,6 +897,12 @@ fn validate_known_fields(value: &toml::Value, source: &Path) -> Result<(), Confi
|
|||||||
})?;
|
})?;
|
||||||
validate_table(security, "security", &["inactivity_timeout_seconds"])?;
|
validate_table(security, "security", &["inactivity_timeout_seconds"])?;
|
||||||
}
|
}
|
||||||
|
if let Some(ui) = root.get("ui") {
|
||||||
|
let ui = ui.as_table().ok_or_else(|| ConfigError::Malformed {
|
||||||
|
path: source.to_owned(),
|
||||||
|
})?;
|
||||||
|
validate_table(ui, "ui", &["selected_mobile_tab"])?;
|
||||||
|
}
|
||||||
let Some(git) = root.get("git") else {
|
let Some(git) = root.get("git") else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod document;
|
|||||||
pub mod generate;
|
pub mod generate;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod kdbx;
|
pub mod kdbx;
|
||||||
|
pub mod mobile;
|
||||||
pub mod mutation;
|
pub mod mutation;
|
||||||
pub mod otp;
|
pub mod otp;
|
||||||
pub mod presentation;
|
pub mod presentation;
|
||||||
|
|||||||
279
crates/storage/src/mobile.rs
Normal file
279
crates/storage/src/mobile.rs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
//! View-ready state for native mobile presentation shells.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{Config, ConfigError},
|
||||||
|
repository::{Repository, RepositoryError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum MobileTab {
|
||||||
|
#[default]
|
||||||
|
Home,
|
||||||
|
Passwords,
|
||||||
|
Totp,
|
||||||
|
Preferences,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileTab {
|
||||||
|
pub const ALL: [Self; 4] = [Self::Home, Self::Passwords, Self::Totp, Self::Preferences];
|
||||||
|
|
||||||
|
pub const fn title(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Home => "Home",
|
||||||
|
Self::Passwords => "Passwords",
|
||||||
|
Self::Totp => "TOTP",
|
||||||
|
Self::Preferences => "Preferences",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn system_image(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Home => "house",
|
||||||
|
Self::Passwords => "key",
|
||||||
|
Self::Totp => "timer",
|
||||||
|
Self::Preferences => "gearshape",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn selected_system_image(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Home => "house.fill",
|
||||||
|
Self::Passwords => "key.fill",
|
||||||
|
Self::Totp => "timer",
|
||||||
|
Self::Preferences => "gearshape.fill",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn config_value(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Home => "home",
|
||||||
|
Self::Passwords => "passwords",
|
||||||
|
Self::Totp => "totp",
|
||||||
|
Self::Preferences => "preferences",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_config(value: &str) -> Result<Self, ConfigError> {
|
||||||
|
match value {
|
||||||
|
"home" => Ok(Self::Home),
|
||||||
|
"passwords" => Ok(Self::Passwords),
|
||||||
|
"totp" => Ok(Self::Totp),
|
||||||
|
"preferences" => Ok(Self::Preferences),
|
||||||
|
_ => Err(ConfigError::InvalidField {
|
||||||
|
field: "ui.selected_mobile_tab",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MobileShellState {
|
||||||
|
Loading,
|
||||||
|
Empty,
|
||||||
|
Ready,
|
||||||
|
Locked,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobilePage {
|
||||||
|
tab: MobileTab,
|
||||||
|
title: String,
|
||||||
|
system_image: String,
|
||||||
|
selected_system_image: String,
|
||||||
|
state: MobileShellState,
|
||||||
|
state_title: String,
|
||||||
|
state_detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobilePage {
|
||||||
|
pub fn tab(&self) -> MobileTab {
|
||||||
|
self.tab
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system_image(&self) -> &str {
|
||||||
|
&self.system_image
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_system_image(&self) -> &str {
|
||||||
|
&self.selected_system_image
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self) -> MobileShellState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_title(&self) -> &str {
|
||||||
|
&self.state_title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_detail(&self) -> &str {
|
||||||
|
&self.state_detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobileShell {
|
||||||
|
selected_tab: MobileTab,
|
||||||
|
pages: Vec<MobilePage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileShell {
|
||||||
|
pub fn load() -> Self {
|
||||||
|
match Config::load(None) {
|
||||||
|
Ok(config) => match Repository::open(config.vault()) {
|
||||||
|
Ok(_) => Self::new(config.mobile_tab(), MobileShellState::Ready, None),
|
||||||
|
Err(RepositoryError::Io {
|
||||||
|
source: std::io::ErrorKind::NotFound,
|
||||||
|
..
|
||||||
|
}) => Self::new(config.mobile_tab(), MobileShellState::Empty, None),
|
||||||
|
Err(error) => Self::new(
|
||||||
|
config.mobile_tab(),
|
||||||
|
MobileShellState::Error,
|
||||||
|
Some(format!("Password store could not be opened: {error}")),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Err(ConfigError::NotFound { .. }) => {
|
||||||
|
Self::new(MobileTab::Home, MobileShellState::Empty, None)
|
||||||
|
}
|
||||||
|
Err(error) => Self::new(
|
||||||
|
MobileTab::Home,
|
||||||
|
MobileShellState::Error,
|
||||||
|
Some(format!("Configuration could not be loaded: {error}")),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixture(state: MobileShellState) -> Self {
|
||||||
|
Self::new(MobileTab::Home, state, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_tab(&self) -> MobileTab {
|
||||||
|
self.selected_tab
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pages(&self) -> &[MobilePage] {
|
||||||
|
&self.pages
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(selected_tab: MobileTab, state: MobileShellState, error: Option<String>) -> Self {
|
||||||
|
let pages = MobileTab::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|tab| {
|
||||||
|
let (state_title, state_detail) = state_copy(tab, state, error.as_deref());
|
||||||
|
MobilePage {
|
||||||
|
tab,
|
||||||
|
title: tab.title().to_owned(),
|
||||||
|
system_image: tab.system_image().to_owned(),
|
||||||
|
selected_system_image: tab.selected_system_image().to_owned(),
|
||||||
|
state,
|
||||||
|
state_title,
|
||||||
|
state_detail,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
selected_tab,
|
||||||
|
pages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_selected_tab(tab: MobileTab) -> Result<(), ConfigError> {
|
||||||
|
store_selected_tab_from(Config::load(None), tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store_selected_tab_from(
|
||||||
|
config: Result<Config, ConfigError>,
|
||||||
|
tab: MobileTab,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
match config {
|
||||||
|
Ok(config) => config.update_mobile_tab(tab),
|
||||||
|
Err(ConfigError::NotFound { .. }) => Ok(()),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_copy(tab: MobileTab, state: MobileShellState, error: Option<&str>) -> (String, String) {
|
||||||
|
match state {
|
||||||
|
MobileShellState::Loading => (
|
||||||
|
format!("Loading {}", tab.title()),
|
||||||
|
"Preparing the password-store presentation.".to_owned(),
|
||||||
|
),
|
||||||
|
MobileShellState::Empty => (
|
||||||
|
"No Password Store".to_owned(),
|
||||||
|
"Set up a local store to use IronStorage.".to_owned(),
|
||||||
|
),
|
||||||
|
MobileShellState::Ready => (
|
||||||
|
tab.title().to_owned(),
|
||||||
|
"The password store is ready.".to_owned(),
|
||||||
|
),
|
||||||
|
MobileShellState::Locked => (
|
||||||
|
"IronStorage is Locked".to_owned(),
|
||||||
|
"Authenticate to reveal protected content.".to_owned(),
|
||||||
|
),
|
||||||
|
MobileShellState::Error => (
|
||||||
|
"IronStorage Is Unavailable".to_owned(),
|
||||||
|
error
|
||||||
|
.unwrap_or("The password-store state could not be loaded.")
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::config::ConfigError;
|
||||||
|
|
||||||
|
use super::{MobilePage, MobileShell, MobileShellState, MobileTab, store_selected_tab_from};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_fixture_is_view_ready_for_all_four_tabs() {
|
||||||
|
for state in [
|
||||||
|
MobileShellState::Loading,
|
||||||
|
MobileShellState::Empty,
|
||||||
|
MobileShellState::Ready,
|
||||||
|
MobileShellState::Locked,
|
||||||
|
MobileShellState::Error,
|
||||||
|
] {
|
||||||
|
let shell = MobileShell::fixture(state);
|
||||||
|
assert_eq!(shell.selected_tab(), MobileTab::Home);
|
||||||
|
assert_eq!(shell.pages().len(), 4);
|
||||||
|
assert_eq!(
|
||||||
|
shell
|
||||||
|
.pages()
|
||||||
|
.iter()
|
||||||
|
.map(MobilePage::tab)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
MobileTab::ALL
|
||||||
|
);
|
||||||
|
assert!(shell.pages().iter().all(|page| {
|
||||||
|
page.state() == state
|
||||||
|
&& !page.title().is_empty()
|
||||||
|
&& !page.system_image().is_empty()
|
||||||
|
&& !page.selected_system_image().is_empty()
|
||||||
|
&& !page.state_title().is_empty()
|
||||||
|
&& !page.state_detail().is_empty()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_selection_before_onboarding_is_a_supported_no_op() {
|
||||||
|
assert_eq!(
|
||||||
|
store_selected_tab_from(
|
||||||
|
Err(ConfigError::NotFound {
|
||||||
|
path: PathBuf::from("config.toml"),
|
||||||
|
}),
|
||||||
|
MobileTab::Preferences,
|
||||||
|
),
|
||||||
|
Ok(())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use ironstorage::{
|
|||||||
authentication::{DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT},
|
authentication::{DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT},
|
||||||
config::{ConfigError, ConfigLoader, EditorSource},
|
config::{ConfigError, ConfigLoader, EditorSource},
|
||||||
desktop::DesktopStorage,
|
desktop::DesktopStorage,
|
||||||
|
mobile::MobileTab,
|
||||||
};
|
};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -178,6 +179,37 @@ fn native_default_path_is_used_without_an_explicit_path() -> TestResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mobile_tab_defaults_and_persists_through_storage_configuration() -> TestResult {
|
||||||
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
fs::create_dir_all(fixture.temporary.path().join("cwd/vault"))?;
|
||||||
|
fixture.write_explicit(fixture.valid_contents())?;
|
||||||
|
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||||
|
assert_eq!(config.mobile_tab(), MobileTab::Home);
|
||||||
|
|
||||||
|
config.update_mobile_tab(MobileTab::Totp)?;
|
||||||
|
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||||
|
assert_eq!(reloaded.mobile_tab(), MobileTab::Totp);
|
||||||
|
assert!(
|
||||||
|
fs::read_to_string(fixture.explicit_path())?.contains("selected_mobile_tab = \"totp\"")
|
||||||
|
);
|
||||||
|
|
||||||
|
fixture.write_explicit(&format!(
|
||||||
|
"{}\n[ui]\nselected_mobile_tab = \"unknown\"\n",
|
||||||
|
fixture.valid_contents()
|
||||||
|
))?;
|
||||||
|
assert_eq!(
|
||||||
|
fixture
|
||||||
|
.loader()
|
||||||
|
.load(Some(&fixture.explicit_path()))
|
||||||
|
.expect_err("unknown mobile tab"),
|
||||||
|
ConfigError::InvalidField {
|
||||||
|
field: "ui.selected_mobile_tab"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn desktop_vault_switch_preserves_and_reloads_the_shared_configuration() -> TestResult {
|
fn desktop_vault_switch_preserves_and_reloads_the_shared_configuration() -> TestResult {
|
||||||
let fixture = ConfigurationFixture::new()?;
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
|||||||
Reference in New Issue
Block a user