Skip to content

Commit

Permalink
[ABW-3581] Inform users when signing with Ledger without P2PLinks (#1343
Browse files Browse the repository at this point in the history
)
  • Loading branch information
matiasbzurovski authored Oct 16, 2024
1 parent ef14f15 commit cda5737
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public extension FactorSourceAccess {
.onFirstTask { @MainActor in
await store.send(.view(.onFirstTask)).finish()
}
.destinations(with: store)
}
}

Expand Down Expand Up @@ -86,3 +87,24 @@ public extension FactorSourceAccess {
}
}
}

private extension StoreOf<FactorSourceAccess> {
var destination: PresentationStoreOf<FactorSourceAccess.Destination> {
func scopeState(state: State) -> PresentationState<FactorSourceAccess.Destination.State> {
state.$destination
}
return scope(state: scopeState, action: Action.destination)
}
}

@MainActor
private extension View {
func destinations(with store: StoreOf<FactorSourceAccess>) -> some View {
let destinationStore = store.destination
return noP2PLinkAlert(with: destinationStore)
}

private func noP2PLinkAlert(with destinationStore: PresentationStoreOf<FactorSourceAccess.Destination>) -> some View {
alert(store: destinationStore.scope(state: \.noP2PLink, action: \.noP2PLink))
}
}
97 changes: 93 additions & 4 deletions RadixWallet/Features/FactorSourceAccess/FactorSourceAccess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<NoP2PLinkAlert>)
}

@CasePathable
public enum Action: Sendable, Hashable {
case noP2PLink(NoP2PLinkAlert)
}

public var body: some ReducerOf<Self> {
EmptyReducer()
}

public enum NoP2PLinkAlert: Sendable, Hashable {
case okTapped
}
}

@Dependency(\.p2pLinksClient) var p2pLinksClient

public var body: some ReducerOf<Self> {
Reduce(core)
.ifLet(destinationPath, action: /Action.destination) {
Destination()
}
}

public init() {}

public func reduce(into _: inout State, viewAction: ViewAction) -> Effect<Action> {
private let destinationPath: WritableKeyPath<State, PresentationState<Destination.State>> = \.$destination

public func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> {
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<Action> {
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<Action> {
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<Action> {
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 {
Expand Down Expand Up @@ -62,3 +137,17 @@ extension FactorSourceAccess.State {
case createKey
}
}

private extension AlertState<FactorSourceAccess.Destination.NoP2PLinkAlert> {
static var noP2Plink: AlertState {
AlertState {
TextState(L10n.LedgerHardwareDevices.LinkConnectorAlert.title)
} actions: {
ButtonState(action: .okTapped) {
TextState(L10n.Common.ok)
}
} message: {
TextState(L10n.LedgerHardwareDevices.LinkConnectorAlert.message)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down

0 comments on commit cda5737

Please sign in to comment.