Implement iPhone password swipe actions
This commit is contained in:
@@ -625,6 +625,10 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
||||
|
||||
func manualLock() throws
|
||||
|
||||
func performEntryMutation(request: MobileMutationRequest) throws -> MobileMutationOutcome
|
||||
|
||||
func prepareEntryMutation(path: String, action: MobileMutationAction) throws -> MobileMutationPlan
|
||||
|
||||
func removeEntryEditorField(editor: UInt64, field: UInt64) throws -> MobileEntryEditorPage
|
||||
|
||||
func reorderEntryEditorField(editor: UInt64, field: UInt64, index: UInt32) throws -> MobileEntryEditorPage
|
||||
@@ -821,6 +825,27 @@ open func manualLock()throws {try rustCallWithError(FfiConverterTypeMobileAuth
|
||||
}
|
||||
}
|
||||
|
||||
open func performEntryMutation(request: MobileMutationRequest)throws -> MobileMutationOutcome {
|
||||
return try FfiConverterTypeMobileMutationOutcome_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_perform_entry_mutation(
|
||||
self.uniffiCloneHandle(),
|
||||
FfiConverterTypeMobileMutationRequest_lower(request),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
open func prepareEntryMutation(path: String, action: MobileMutationAction)throws -> MobileMutationPlan {
|
||||
return try FfiConverterTypeMobileMutationPlan_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_entry_mutation(
|
||||
self.uniffiCloneHandle(),
|
||||
FfiConverterString.lower(path),
|
||||
FfiConverterTypeMobileMutationAction_lower(action),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
open func removeEntryEditorField(editor: UInt64, field: UInt64)throws -> MobileEntryEditorPage {
|
||||
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
@@ -2908,6 +2933,282 @@ public func FfiConverterTypeMobileKeyTransferProgress_lower(_ value: MobileKeyTr
|
||||
}
|
||||
|
||||
|
||||
public struct MobileMutationDestination: Equatable, Hashable {
|
||||
public var path: String
|
||||
public var title: String
|
||||
public var detail: String
|
||||
public var requiresOverwrite: Bool
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(path: String, title: String, detail: String, requiresOverwrite: Bool) {
|
||||
self.path = path
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
self.requiresOverwrite = requiresOverwrite
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileMutationDestination: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileMutationDestination: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileMutationDestination {
|
||||
return
|
||||
try MobileMutationDestination(
|
||||
path: FfiConverterString.read(from: &buf),
|
||||
title: FfiConverterString.read(from: &buf),
|
||||
detail: FfiConverterString.read(from: &buf),
|
||||
requiresOverwrite: FfiConverterBool.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileMutationDestination, into buf: inout [UInt8]) {
|
||||
FfiConverterString.write(value.path, into: &buf)
|
||||
FfiConverterString.write(value.title, into: &buf)
|
||||
FfiConverterString.write(value.detail, into: &buf)
|
||||
FfiConverterBool.write(value.requiresOverwrite, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationDestination_lift(_ buf: RustBuffer) throws -> MobileMutationDestination {
|
||||
return try FfiConverterTypeMobileMutationDestination.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationDestination_lower(_ value: MobileMutationDestination) -> RustBuffer {
|
||||
return FfiConverterTypeMobileMutationDestination.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct MobileMutationOutcome: Equatable, Hashable {
|
||||
public var action: MobileMutationAction
|
||||
public var source: String
|
||||
public var destination: String?
|
||||
public var title: String
|
||||
public var detail: String
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(action: MobileMutationAction, source: String, destination: String?, title: String, detail: String) {
|
||||
self.action = action
|
||||
self.source = source
|
||||
self.destination = destination
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileMutationOutcome: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileMutationOutcome: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileMutationOutcome {
|
||||
return
|
||||
try MobileMutationOutcome(
|
||||
action: FfiConverterTypeMobileMutationAction.read(from: &buf),
|
||||
source: FfiConverterString.read(from: &buf),
|
||||
destination: FfiConverterOptionString.read(from: &buf),
|
||||
title: FfiConverterString.read(from: &buf),
|
||||
detail: FfiConverterString.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileMutationOutcome, into buf: inout [UInt8]) {
|
||||
FfiConverterTypeMobileMutationAction.write(value.action, into: &buf)
|
||||
FfiConverterString.write(value.source, into: &buf)
|
||||
FfiConverterOptionString.write(value.destination, into: &buf)
|
||||
FfiConverterString.write(value.title, into: &buf)
|
||||
FfiConverterString.write(value.detail, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationOutcome_lift(_ buf: RustBuffer) throws -> MobileMutationOutcome {
|
||||
return try FfiConverterTypeMobileMutationOutcome.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationOutcome_lower(_ value: MobileMutationOutcome) -> RustBuffer {
|
||||
return FfiConverterTypeMobileMutationOutcome.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct MobileMutationPlan: Equatable, Hashable {
|
||||
public var action: MobileMutationAction
|
||||
public var source: String
|
||||
public var sourceTitle: String
|
||||
public var revision: String
|
||||
public var destinations: [MobileMutationDestination]
|
||||
public var hasOpenEditor: Bool
|
||||
public var hasDirtyEditor: Bool
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(action: MobileMutationAction, source: String, sourceTitle: String, revision: String, destinations: [MobileMutationDestination], hasOpenEditor: Bool, hasDirtyEditor: Bool) {
|
||||
self.action = action
|
||||
self.source = source
|
||||
self.sourceTitle = sourceTitle
|
||||
self.revision = revision
|
||||
self.destinations = destinations
|
||||
self.hasOpenEditor = hasOpenEditor
|
||||
self.hasDirtyEditor = hasDirtyEditor
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileMutationPlan: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileMutationPlan: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileMutationPlan {
|
||||
return
|
||||
try MobileMutationPlan(
|
||||
action: FfiConverterTypeMobileMutationAction.read(from: &buf),
|
||||
source: FfiConverterString.read(from: &buf),
|
||||
sourceTitle: FfiConverterString.read(from: &buf),
|
||||
revision: FfiConverterString.read(from: &buf),
|
||||
destinations: FfiConverterSequenceTypeMobileMutationDestination.read(from: &buf),
|
||||
hasOpenEditor: FfiConverterBool.read(from: &buf),
|
||||
hasDirtyEditor: FfiConverterBool.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileMutationPlan, into buf: inout [UInt8]) {
|
||||
FfiConverterTypeMobileMutationAction.write(value.action, into: &buf)
|
||||
FfiConverterString.write(value.source, into: &buf)
|
||||
FfiConverterString.write(value.sourceTitle, into: &buf)
|
||||
FfiConverterString.write(value.revision, into: &buf)
|
||||
FfiConverterSequenceTypeMobileMutationDestination.write(value.destinations, into: &buf)
|
||||
FfiConverterBool.write(value.hasOpenEditor, into: &buf)
|
||||
FfiConverterBool.write(value.hasDirtyEditor, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationPlan_lift(_ buf: RustBuffer) throws -> MobileMutationPlan {
|
||||
return try FfiConverterTypeMobileMutationPlan.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationPlan_lower(_ value: MobileMutationPlan) -> RustBuffer {
|
||||
return FfiConverterTypeMobileMutationPlan.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct MobileMutationRequest: Equatable, Hashable {
|
||||
public var action: MobileMutationAction
|
||||
public var source: String
|
||||
public var revision: String
|
||||
public var destination: String?
|
||||
public var confirmed: Bool
|
||||
public var overwrite: Bool
|
||||
public var discardEditor: Bool
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(action: MobileMutationAction, source: String, revision: String, destination: String?, confirmed: Bool, overwrite: Bool, discardEditor: Bool) {
|
||||
self.action = action
|
||||
self.source = source
|
||||
self.revision = revision
|
||||
self.destination = destination
|
||||
self.confirmed = confirmed
|
||||
self.overwrite = overwrite
|
||||
self.discardEditor = discardEditor
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileMutationRequest: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileMutationRequest: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileMutationRequest {
|
||||
return
|
||||
try MobileMutationRequest(
|
||||
action: FfiConverterTypeMobileMutationAction.read(from: &buf),
|
||||
source: FfiConverterString.read(from: &buf),
|
||||
revision: FfiConverterString.read(from: &buf),
|
||||
destination: FfiConverterOptionString.read(from: &buf),
|
||||
confirmed: FfiConverterBool.read(from: &buf),
|
||||
overwrite: FfiConverterBool.read(from: &buf),
|
||||
discardEditor: FfiConverterBool.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileMutationRequest, into buf: inout [UInt8]) {
|
||||
FfiConverterTypeMobileMutationAction.write(value.action, into: &buf)
|
||||
FfiConverterString.write(value.source, into: &buf)
|
||||
FfiConverterString.write(value.revision, into: &buf)
|
||||
FfiConverterOptionString.write(value.destination, into: &buf)
|
||||
FfiConverterBool.write(value.confirmed, into: &buf)
|
||||
FfiConverterBool.write(value.overwrite, into: &buf)
|
||||
FfiConverterBool.write(value.discardEditor, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationRequest_lift(_ buf: RustBuffer) throws -> MobileMutationRequest {
|
||||
return try FfiConverterTypeMobileMutationRequest.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationRequest_lower(_ value: MobileMutationRequest) -> RustBuffer {
|
||||
return FfiConverterTypeMobileMutationRequest.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct MobileOnboardingDiscovery: Equatable, Hashable {
|
||||
public var branches: [String]
|
||||
public var selectedBranch: UInt32
|
||||
@@ -4611,6 +4912,79 @@ public func FfiConverterTypeMobileKeyTransferKind_lower(_ value: MobileKeyTransf
|
||||
|
||||
|
||||
|
||||
public enum MobileMutationAction: Equatable, Hashable {
|
||||
|
||||
case move
|
||||
case copy
|
||||
case delete
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileMutationAction: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileMutationAction: FfiConverterRustBuffer {
|
||||
typealias SwiftType = MobileMutationAction
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileMutationAction {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
switch variant {
|
||||
|
||||
case 1: return .move
|
||||
|
||||
case 2: return .copy
|
||||
|
||||
case 3: return .delete
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileMutationAction, into buf: inout [UInt8]) {
|
||||
switch value {
|
||||
|
||||
|
||||
case .move:
|
||||
writeInt(&buf, Int32(1))
|
||||
|
||||
|
||||
case .copy:
|
||||
writeInt(&buf, Int32(2))
|
||||
|
||||
|
||||
case .delete:
|
||||
writeInt(&buf, Int32(3))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationAction_lift(_ buf: RustBuffer) throws -> MobileMutationAction {
|
||||
return try FfiConverterTypeMobileMutationAction.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileMutationAction_lower(_ value: MobileMutationAction) -> RustBuffer {
|
||||
return FfiConverterTypeMobileMutationAction.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public enum MobileOnboardingErrorKind: Equatable, Hashable {
|
||||
|
||||
case invalidInput
|
||||
@@ -5807,6 +6181,31 @@ fileprivate struct FfiConverterSequenceTypeMobileKeyTransferKey: FfiConverterRus
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
fileprivate struct FfiConverterSequenceTypeMobileMutationDestination: FfiConverterRustBuffer {
|
||||
typealias SwiftType = [MobileMutationDestination]
|
||||
|
||||
public static func write(_ value: [MobileMutationDestination], into buf: inout [UInt8]) {
|
||||
let len = Int32(value.count)
|
||||
writeInt(&buf, len)
|
||||
for item in value {
|
||||
FfiConverterTypeMobileMutationDestination.write(item, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobileMutationDestination] {
|
||||
let len: Int32 = try readInt(&buf)
|
||||
var seq = [MobileMutationDestination]()
|
||||
seq.reserveCapacity(Int(len))
|
||||
for _ in 0 ..< len {
|
||||
seq.append(try FfiConverterTypeMobileMutationDestination.read(from: &buf))
|
||||
}
|
||||
return seq
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
@@ -6048,6 +6447,12 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock() != 57220) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_perform_entry_mutation() != 7053) {
|
||||
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_entry_editor_field() != 12238) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
|
||||
@@ -308,6 +308,16 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_generate_entr
|
||||
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_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_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_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
|
||||
@@ -943,6 +953,18 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_generate_
|
||||
#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_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_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_ENTRY_EDITOR_FIELD
|
||||
|
||||
@@ -2309,6 +2309,483 @@ private extension UInt64 {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PasswordMutationCoordinator {
|
||||
private weak var presenter: UIViewController?
|
||||
private let authentication: MobileAuthentication?
|
||||
private var operationTask: Task<Void, Never>?
|
||||
private var working = false
|
||||
private var contextualCompletion: ((Bool) -> Void)?
|
||||
|
||||
init(presenter: UIViewController, authentication: MobileAuthentication?) {
|
||||
self.presenter = presenter
|
||||
self.authentication = authentication
|
||||
}
|
||||
|
||||
deinit {
|
||||
operationTask?.cancel()
|
||||
}
|
||||
|
||||
func accessoryView(for row: MobilePasswordRow) -> UIView {
|
||||
let button = UIButton(type: .system)
|
||||
button.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
|
||||
button.configuration = .plain()
|
||||
button.configuration?.image = UIImage(systemName: "ellipsis.circle")
|
||||
button.configuration?.buttonSize = .medium
|
||||
button.menu = menu(for: row)
|
||||
button.showsMenuAsPrimaryAction = true
|
||||
button.accessibilityLabel = "Actions for \(row.title)"
|
||||
return button
|
||||
}
|
||||
|
||||
func accessibilityActions(for row: MobilePasswordRow) -> [UIAccessibilityCustomAction] {
|
||||
[
|
||||
accessibilityAction("Move", image: "folder", action: .move, row: row),
|
||||
accessibilityAction("Copy", image: "doc.on.doc", action: .copy, row: row),
|
||||
accessibilityAction("Delete", image: "trash", action: .delete, row: row),
|
||||
]
|
||||
}
|
||||
|
||||
func swipeConfiguration(for row: MobilePasswordRow) -> UISwipeActionsConfiguration {
|
||||
let delete = UIContextualAction(style: .destructive, title: "Delete") {
|
||||
[weak self] _, _, completion in
|
||||
self?.begin(.delete, row: row, completion: completion)
|
||||
}
|
||||
delete.image = UIImage(systemName: "trash")
|
||||
|
||||
let copy = UIContextualAction(style: .normal, title: "Copy") {
|
||||
[weak self] _, _, completion in
|
||||
self?.begin(.copy, row: row, completion: completion)
|
||||
}
|
||||
copy.image = UIImage(systemName: "doc.on.doc")
|
||||
copy.backgroundColor = .systemBlue
|
||||
|
||||
let move = UIContextualAction(style: .normal, title: "Move") {
|
||||
[weak self] _, _, completion in
|
||||
self?.begin(.move, row: row, completion: completion)
|
||||
}
|
||||
move.image = UIImage(systemName: "folder")
|
||||
move.backgroundColor = .systemOrange
|
||||
|
||||
let configuration = UISwipeActionsConfiguration(actions: [delete, copy, move])
|
||||
configuration.performsFirstActionWithFullSwipe = true
|
||||
return configuration
|
||||
}
|
||||
|
||||
private func menu(for row: MobilePasswordRow) -> UIMenu {
|
||||
UIMenu(children: [
|
||||
UIAction(title: "Move", image: UIImage(systemName: "folder")) { [weak self] _ in
|
||||
self?.begin(.move, row: row)
|
||||
},
|
||||
UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in
|
||||
self?.begin(.copy, row: row)
|
||||
},
|
||||
UIAction(
|
||||
title: "Delete",
|
||||
image: UIImage(systemName: "trash"),
|
||||
attributes: .destructive
|
||||
) { [weak self] _ in
|
||||
self?.begin(.delete, row: row)
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
private func accessibilityAction(
|
||||
_ title: String,
|
||||
image: String,
|
||||
action: MobileMutationAction,
|
||||
row: MobilePasswordRow
|
||||
) -> UIAccessibilityCustomAction {
|
||||
UIAccessibilityCustomAction(
|
||||
name: title,
|
||||
image: UIImage(systemName: image)
|
||||
) { [weak self] _ in
|
||||
self?.begin(action, row: row)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func begin(
|
||||
_ action: MobileMutationAction,
|
||||
row: MobilePasswordRow,
|
||||
completion: ((Bool) -> Void)? = nil
|
||||
) {
|
||||
guard !working else {
|
||||
completion?(false)
|
||||
return
|
||||
}
|
||||
guard let authentication else {
|
||||
completion?(false)
|
||||
presenter?.presentAuthenticationFailure(.unavailable)
|
||||
return
|
||||
}
|
||||
working = true
|
||||
contextualCompletion = completion
|
||||
operationTask = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobileMutationPlan, AuthenticationFailure>.success(
|
||||
try authentication.prepareEntryMutation(path: row.path, action: action)
|
||||
)
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
return .failure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
operationTask = nil
|
||||
switch result {
|
||||
case let .success(plan):
|
||||
present(plan)
|
||||
case let .failure(failure):
|
||||
fail(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func present(_ plan: MobileMutationPlan) {
|
||||
switch plan.action {
|
||||
case .delete:
|
||||
confirmDelete(plan)
|
||||
case .move, .copy:
|
||||
guard !plan.destinations.isEmpty else {
|
||||
fail(AuthenticationFailure(
|
||||
kind: .entry,
|
||||
title: "No Destination Available",
|
||||
detail: "Create another password folder before moving or copying this entry."
|
||||
))
|
||||
return
|
||||
}
|
||||
let destinations = PasswordDestinationViewController(
|
||||
plan: plan,
|
||||
selected: { [weak self] destination in
|
||||
self?.presenter?.dismiss(animated: true) {
|
||||
self?.destinationSelected(destination, plan: plan)
|
||||
}
|
||||
},
|
||||
cancelled: { [weak self] in
|
||||
self?.presenter?.dismiss(animated: true) { self?.finish(false) }
|
||||
}
|
||||
)
|
||||
let navigation = UINavigationController(rootViewController: destinations)
|
||||
navigation.modalPresentationStyle = .formSheet
|
||||
presenter?.present(navigation, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func destinationSelected(
|
||||
_ destination: MobileMutationDestination,
|
||||
plan: MobileMutationPlan
|
||||
) {
|
||||
if destination.requiresOverwrite {
|
||||
let verb = plan.action == .move ? "Move" : "Copy"
|
||||
let alert = UIAlertController(
|
||||
title: "Replace Existing Password?",
|
||||
message: "\(destination.title) already contains “\(plan.sourceTitle)”. \(verb) and replace it?",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
|
||||
[weak self] _ in self?.finish(false)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Replace", style: .destructive) {
|
||||
[weak self] _ in
|
||||
self?.confirmEditorIfNeeded(plan, destination: destination.path, overwrite: true)
|
||||
})
|
||||
presenter?.present(alert, animated: true)
|
||||
} else {
|
||||
confirmEditorIfNeeded(plan, destination: destination.path, overwrite: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func confirmDelete(_ plan: MobileMutationPlan) {
|
||||
let suffix = plan.hasDirtyEditor
|
||||
? " Unsaved changes in its open editor will also be discarded."
|
||||
: " This can’t be undone."
|
||||
let alert = UIAlertController(
|
||||
title: "Delete “\(plan.sourceTitle)”?",
|
||||
message: "The encrypted password entry will be permanently deleted.\(suffix)",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
|
||||
[weak self] _ in self?.finish(false)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) {
|
||||
[weak self] _ in
|
||||
self?.perform(
|
||||
plan,
|
||||
destination: nil,
|
||||
overwrite: false,
|
||||
discardEditor: plan.hasOpenEditor,
|
||||
mayAuthenticate: true
|
||||
)
|
||||
})
|
||||
presenter?.present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func confirmEditorIfNeeded(
|
||||
_ plan: MobileMutationPlan,
|
||||
destination: String,
|
||||
overwrite: Bool
|
||||
) {
|
||||
guard plan.hasOpenEditor else {
|
||||
perform(
|
||||
plan,
|
||||
destination: destination,
|
||||
overwrite: overwrite,
|
||||
discardEditor: false,
|
||||
mayAuthenticate: true
|
||||
)
|
||||
return
|
||||
}
|
||||
let verb = plan.action == .move ? "Move" : "Copy"
|
||||
let alert = UIAlertController(
|
||||
title: plan.hasDirtyEditor ? "Discard Changes and \(verb)?" : "Close Editor and \(verb)?",
|
||||
message: plan.hasDirtyEditor
|
||||
? "Unsaved changes in the open editor for “\(plan.sourceTitle)” will be discarded."
|
||||
: "The open editor for “\(plan.sourceTitle)” must close before this action.",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
|
||||
[weak self] _ in self?.finish(false)
|
||||
})
|
||||
alert.addAction(UIAlertAction(
|
||||
title: plan.hasDirtyEditor ? "Discard and \(verb)" : "Close and \(verb)",
|
||||
style: plan.hasDirtyEditor ? .destructive : .default
|
||||
) { [weak self] _ in
|
||||
self?.perform(
|
||||
plan,
|
||||
destination: destination,
|
||||
overwrite: overwrite,
|
||||
discardEditor: true,
|
||||
mayAuthenticate: true
|
||||
)
|
||||
})
|
||||
presenter?.present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func perform(
|
||||
_ plan: MobileMutationPlan,
|
||||
destination: String?,
|
||||
overwrite: Bool,
|
||||
discardEditor: Bool,
|
||||
mayAuthenticate: Bool
|
||||
) {
|
||||
guard let authentication else {
|
||||
fail(.unavailable)
|
||||
return
|
||||
}
|
||||
let request = MobileMutationRequest(
|
||||
action: plan.action,
|
||||
source: plan.source,
|
||||
revision: plan.revision,
|
||||
destination: destination,
|
||||
confirmed: plan.action == .delete,
|
||||
overwrite: overwrite,
|
||||
discardEditor: discardEditor
|
||||
)
|
||||
operationTask = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobileMutationOutcome, AuthenticationFailure>.success(
|
||||
try authentication.performEntryMutation(request: request)
|
||||
)
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
return .failure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
operationTask = nil
|
||||
switch result {
|
||||
case let .success(outcome):
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
UIAccessibility.post(notification: .announcement, argument: outcome.detail)
|
||||
NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
|
||||
finish(true)
|
||||
case let .failure(failure)
|
||||
where mayAuthenticate && failure.kind == .expired:
|
||||
unlockAndRetry(
|
||||
plan,
|
||||
destination: destination,
|
||||
overwrite: overwrite,
|
||||
discardEditor: discardEditor,
|
||||
passphrase: nil
|
||||
)
|
||||
case let .failure(failure):
|
||||
fail(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func unlockAndRetry(
|
||||
_ plan: MobileMutationPlan,
|
||||
destination: String?,
|
||||
overwrite: Bool,
|
||||
discardEditor: Bool,
|
||||
passphrase: String?
|
||||
) {
|
||||
guard let authentication else {
|
||||
fail(.unavailable)
|
||||
return
|
||||
}
|
||||
operationTask = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
_ = try authentication.unlockEntry(path: plan.source, passphrase: passphrase)
|
||||
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 }
|
||||
operationTask = nil
|
||||
switch result {
|
||||
case .success:
|
||||
perform(
|
||||
plan,
|
||||
destination: destination,
|
||||
overwrite: overwrite,
|
||||
discardEditor: discardEditor,
|
||||
mayAuthenticate: false
|
||||
)
|
||||
case let .failure(failure)
|
||||
where passphrase == nil
|
||||
&& (failure.kind == .passphraseRequired
|
||||
|| failure.kind == .biometryUnavailable):
|
||||
promptForPassphrase(
|
||||
plan,
|
||||
destination: destination,
|
||||
overwrite: overwrite,
|
||||
discardEditor: discardEditor,
|
||||
message: failure.detail
|
||||
)
|
||||
case let .failure(failure):
|
||||
fail(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func promptForPassphrase(
|
||||
_ plan: MobileMutationPlan,
|
||||
destination: String?,
|
||||
overwrite: Bool,
|
||||
discardEditor: Bool,
|
||||
message: String
|
||||
) {
|
||||
let alert = UIAlertController(
|
||||
title: "GPG Key Passphrase",
|
||||
message: message,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addTextField { field in
|
||||
field.isSecureTextEntry = true
|
||||
field.textContentType = .password
|
||||
field.placeholder = "Passphrase"
|
||||
field.returnKeyType = .go
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
|
||||
[weak self] _ in self?.finish(false)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Unlock", style: .default) {
|
||||
[weak self, weak alert] _ in
|
||||
guard let value = alert?.textFields?.first?.text, !value.isEmpty else {
|
||||
self?.finish(false)
|
||||
return
|
||||
}
|
||||
alert?.textFields?.first?.text = nil
|
||||
self?.unlockAndRetry(
|
||||
plan,
|
||||
destination: destination,
|
||||
overwrite: overwrite,
|
||||
discardEditor: discardEditor,
|
||||
passphrase: value
|
||||
)
|
||||
})
|
||||
presenter?.present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func fail(_ failure: AuthenticationFailure) {
|
||||
presenter?.presentAuthenticationFailure(failure)
|
||||
finish(false)
|
||||
}
|
||||
|
||||
private func finish(_ applied: Bool) {
|
||||
contextualCompletion?(applied)
|
||||
contextualCompletion = nil
|
||||
working = false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PasswordDestinationViewController: UITableViewController {
|
||||
private let plan: MobileMutationPlan
|
||||
private let selected: (MobileMutationDestination) -> Void
|
||||
private let cancelled: () -> Void
|
||||
|
||||
init(
|
||||
plan: MobileMutationPlan,
|
||||
selected: @escaping (MobileMutationDestination) -> Void,
|
||||
cancelled: @escaping () -> Void
|
||||
) {
|
||||
self.plan = plan
|
||||
self.selected = selected
|
||||
self.cancelled = cancelled
|
||||
super.init(style: .insetGrouped)
|
||||
title = plan.action == .move ? "Move to Folder" : "Copy to Folder"
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
systemItem: .cancel,
|
||||
primaryAction: UIAction { [weak self] _ in self?.cancelled() }
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
plan.destinations.count
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
titleForHeaderInSection section: Int
|
||||
) -> String? {
|
||||
plan.sourceTitle
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let destination = plan.destinations[indexPath.row]
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.image = UIImage(systemName: "folder")
|
||||
content.text = destination.title
|
||||
content.secondaryText = destination.requiresOverwrite
|
||||
? "\(destination.detail) · Replaces existing password"
|
||||
: destination.detail
|
||||
content.secondaryTextProperties.numberOfLines = 2
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.accessibilityHint = destination.requiresOverwrite
|
||||
? "Requires confirmation before replacing the existing password."
|
||||
: "Selects this destination folder."
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
selected(plan.destinations[indexPath.row])
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PasswordSearchViewController: UITableViewController, MobileTabRoot,
|
||||
UISearchResultsUpdating
|
||||
@@ -2321,6 +2798,10 @@ private final class PasswordSearchViewController: UITableViewController, MobileT
|
||||
private var searchTask: Task<Void, Never>?
|
||||
private var shellTask: Task<Void, Never>?
|
||||
private var generation = 0
|
||||
private lazy var mutationCoordinator = PasswordMutationCoordinator(
|
||||
presenter: self,
|
||||
authentication: authentication
|
||||
)
|
||||
|
||||
init(shellPage: MobilePage, authentication: MobileAuthentication?) {
|
||||
self.shellPage = shellPage
|
||||
@@ -2395,12 +2876,22 @@ private final class PasswordSearchViewController: UITableViewController, MobileT
|
||||
content.textProperties.numberOfLines = 2
|
||||
content.secondaryTextProperties.numberOfLines = 2
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.accessoryView = mutationCoordinator.accessoryView(for: row)
|
||||
cell.accessibilityCustomActions = mutationCoordinator.accessibilityActions(for: row)
|
||||
cell.accessibilityLabel = "\(row.title), in \(row.detail)"
|
||||
cell.accessibilityHint = "Opens the locked password viewer."
|
||||
cell.accessibilityHint = "Opens the locked password viewer. More actions follow."
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
|
||||
) -> UISwipeActionsConfiguration? {
|
||||
guard let rows = searchPage?.rows, rows.indices.contains(indexPath.row) else { return nil }
|
||||
let row = rows[indexPath.row]
|
||||
return mutationCoordinator.swipeConfiguration(for: row)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let row = searchPage?.rows[indexPath.row] else { return }
|
||||
@@ -2534,6 +3025,10 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
|
||||
private var loadTask: Task<Void, Never>?
|
||||
private var createTask: Task<Void, Never>?
|
||||
private var loadGeneration = 0
|
||||
private lazy var mutationCoordinator = PasswordMutationCoordinator(
|
||||
presenter: self,
|
||||
authentication: authentication
|
||||
)
|
||||
|
||||
init(
|
||||
shellPage: MobilePage,
|
||||
@@ -2626,14 +3121,29 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
|
||||
content.textProperties.numberOfLines = 2
|
||||
content.secondaryTextProperties.numberOfLines = 1
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
if row.kind == .entry {
|
||||
cell.accessoryView = mutationCoordinator.accessoryView(for: row)
|
||||
cell.accessibilityCustomActions = mutationCoordinator.accessibilityActions(for: row)
|
||||
} else {
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
}
|
||||
cell.accessibilityLabel = "\(row.title), \(row.detail)"
|
||||
cell.accessibilityHint = row.kind == .directory
|
||||
? "Opens this password folder."
|
||||
: "Opens the locked password viewer."
|
||||
: "Opens the locked password viewer. More actions follow."
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
|
||||
) -> UISwipeActionsConfiguration? {
|
||||
guard let rows = directoryPage?.rows, rows.indices.contains(indexPath.row) else { return nil }
|
||||
let row = rows[indexPath.row]
|
||||
guard row.kind == .entry else { return nil }
|
||||
return mutationCoordinator.swipeConfiguration(for: row)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let row = directoryPage?.rows[indexPath.row] else { return }
|
||||
let controller: UIViewController = switch row.kind {
|
||||
@@ -4125,7 +4635,7 @@ private struct AuthenticationFailure: Error, Sendable {
|
||||
detail: "Finish password-store setup before unlocking entries."
|
||||
)
|
||||
|
||||
private init(kind: MobileAuthenticationErrorKind, title: String, detail: String) {
|
||||
fileprivate init(kind: MobileAuthenticationErrorKind, title: String, detail: String) {
|
||||
self.kind = kind
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
|
||||
Reference in New Issue
Block a user