diff --git a/RadixWallet/Clients/P2PLinksClient/P2PLinksClient+Interface.swift b/RadixWallet/Clients/P2PLinksClient/P2PLinksClient+Interface.swift index 294e464019..dce0fd99a0 100644 --- a/RadixWallet/Clients/P2PLinksClient/P2PLinksClient+Interface.swift +++ b/RadixWallet/Clients/P2PLinksClient/P2PLinksClient+Interface.swift @@ -32,3 +32,9 @@ extension P2PLinksClient { public typealias GetP2PLinkPrivateKey = @Sendable () async throws -> (privateKey: Curve25519.PrivateKey, isNew: Bool) public typealias StoreP2PLinkPrivateKey = @Sendable (Curve25519.PrivateKey) async throws -> Void } + +extension P2PLinksClient { + func hasP2PLinks() async -> Bool { + await !getP2PLinks().isEmpty + } +} diff --git a/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess+View.swift b/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess+View.swift index e1b66ecb80..d0a6dc2778 100644 --- a/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess+View.swift +++ b/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess+View.swift @@ -30,6 +30,7 @@ public extension FactorSourceAccess { .onFirstTask { @MainActor in await store.send(.view(.onFirstTask)).finish() } + .destinations(with: store) } } @@ -86,3 +87,24 @@ public extension FactorSourceAccess { } } } + +private extension StoreOf { + var destination: PresentationStoreOf { + func scopeState(state: State) -> PresentationState { + state.$destination + } + return scope(state: scopeState, action: Action.destination) + } +} + +@MainActor +private extension View { + func destinations(with store: StoreOf) -> some View { + let destinationStore = store.destination + return noP2PLinkAlert(with: destinationStore) + } + + private func noP2PLinkAlert(with destinationStore: PresentationStoreOf) -> some View { + alert(store: destinationStore.scope(state: \.noP2PLink, action: \.noP2PLink)) + } +} diff --git a/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess.swift b/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess.swift index 532bd1e785..ca04a4ddc0 100644 --- a/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess.swift +++ b/RadixWallet/Features/FactorSourceAccess/FactorSourceAccess.swift @@ -4,33 +4,108 @@ public struct FactorSourceAccess: Sendable, FeatureReducer { public let kind: Kind public let purpose: Purpose + @PresentationState + public var destination: Destination.State? = nil + public init(kind: Kind, purpose: Purpose) { self.kind = kind self.purpose = purpose } } - public enum ViewAction: Sendable, Equatable { + public enum ViewAction: Sendable, Hashable { case onFirstTask case retryButtonTapped case closeButtonTapped } - public enum DelegateAction: Sendable, Equatable { + public enum InternalAction: Sendable, Hashable { + case hasP2PLinks(Bool) + } + + public enum DelegateAction: Sendable, Hashable { case perform case cancel } + public struct Destination: DestinationReducer { + @CasePathable + public enum State: Sendable, Hashable { + case noP2PLink(AlertState) + } + + @CasePathable + public enum Action: Sendable, Hashable { + case noP2PLink(NoP2PLinkAlert) + } + + public var body: some ReducerOf { + EmptyReducer() + } + + public enum NoP2PLinkAlert: Sendable, Hashable { + case okTapped + } + } + + @Dependency(\.p2pLinksClient) var p2pLinksClient + + public var body: some ReducerOf { + Reduce(core) + .ifLet(destinationPath, action: /Action.destination) { + Destination() + } + } + public init() {} - public func reduce(into _: inout State, viewAction: ViewAction) -> Effect { + private let destinationPath: WritableKeyPath> = \.$destination + + public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { switch viewAction { - case .onFirstTask, .retryButtonTapped: + case .onFirstTask: + .send(.delegate(.perform)) + .merge(with: checkP2PLinksEffect(state: state)) + case .retryButtonTapped: .send(.delegate(.perform)) case .closeButtonTapped: .send(.delegate(.cancel)) } } + + public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { + switch internalAction { + case let .hasP2PLinks(hasP2PLinks): + if !hasP2PLinks { + state.destination = .noP2PLink(.noP2Plink) + } + return .none + } + } + + public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect { + switch presentedAction { + case .noP2PLink(.okTapped): + state.destination = nil + return .run { send in + // Dispatching this in an async process is enough for it to take place after alert has been dismissed. + // No need to place an actual delay with continuousClock. + await send(.delegate(.cancel)) + } + } + } + + private func checkP2PLinksEffect(state: State) -> Effect { + guard case .ledger = state.kind else { + return .none + } + return .run { send in + let result = await p2pLinksClient.hasP2PLinks() + await send(.internal(.hasP2PLinks(result))) + } catch: { error, _ in + loggerGlobal.error("failed to check if has p2p links, error: \(error)") + } + } } extension FactorSourceAccess.State { @@ -62,3 +137,17 @@ extension FactorSourceAccess.State { case createKey } } + +private extension AlertState { + static var noP2Plink: AlertState { + AlertState { + TextState(L10n.LedgerHardwareDevices.LinkConnectorAlert.title) + } actions: { + ButtonState(action: .okTapped) { + TextState(L10n.Common.ok) + } + } message: { + TextState(L10n.LedgerHardwareDevices.LinkConnectorAlert.message) + } + } +} diff --git a/RadixWallet/Features/Signing/Children/SignWithFactorSource.swift b/RadixWallet/Features/Signing/Children/SignWithFactorSource.swift index 1cff57cae6..2335207093 100644 --- a/RadixWallet/Features/Signing/Children/SignWithFactorSource.swift +++ b/RadixWallet/Features/Signing/Children/SignWithFactorSource.swift @@ -24,7 +24,8 @@ public struct SignWithFactorSource: Sendable, FeatureReducer { self.factorSourceAccess = .init(kind: .device, purpose: .signature) case .ledger: assert(signingFactors.allSatisfy { $0.factorSource.kind == LedgerHardwareWalletFactorSource.kind }) - self.factorSourceAccess = .init(kind: .ledger(nil), purpose: .signature) + let ledger: LedgerHardwareWalletFactorSource? = signingFactors.first?.factorSource.extract() + self.factorSourceAccess = .init(kind: .ledger(ledger), purpose: .signature) } } }