Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ABW-3918] Security Factors redesign #1413

Merged
merged 4 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions RadixWallet/Core/DesignSystem/Components/AssetIcon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct AssetIcon: View {
private let cornerRadius: CGFloat

enum Content: Equatable {
case asset(ImageAsset)
case asset(ImageResource)
case systemImage(String)
}

Expand All @@ -16,7 +16,7 @@ struct AssetIcon: View {
init(_ content: Content, size: HitTargetSize) {
switch content {
case let .asset(asset):
self.image = Image(asset: asset)
self.image = Image(asset)
case let .systemImage(systemName):
self.image = Image(systemName: systemName)
}
Expand Down
4 changes: 2 additions & 2 deletions RadixWallet/Core/DesignSystem/ToggleView.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// MARK: - ToggleView
struct ToggleView: SwiftUI.View {
let context: Context
let icon: ImageAsset?
let icon: ImageResource?
let title: String
let subtitle: String
let minHeight: CGFloat
let isOn: Binding<Bool>

init(
context: Context = .toggle,
icon: ImageAsset? = nil,
icon: ImageResource? = nil,
title: String,
subtitle: String,
minHeight: CGFloat = .largeButtonHeight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ extension PreferenceSection.Row where RowId == AccountPreferences.Section.Sectio
id: .personalize(.accountLabel),
title: L10n.AccountSettings.accountLabel,
subtitle: L10n.AccountSettings.accountLabelSubtitle,
icon: .asset(AssetResource.create)
icon: .asset(.create)
)
}

Expand All @@ -230,20 +230,20 @@ extension PreferenceSection.Row where RowId == AccountPreferences.Section.Sectio
id: .dev(.devPreferences),
title: L10n.AccountSettings.devPreferences,
subtitle: nil,
icon: .asset(AssetResource.appSettings)
icon: .asset(.appSettings)
)
}
}

extension DepositRule {
var icon: ImageAsset {
var icon: ImageResource {
switch self {
case .acceptAll:
AssetResource.iconAcceptAirdrop
.iconAcceptAirdrop
case .acceptKnown:
AssetResource.iconAcceptKnownAirdrop
.iconAcceptKnownAirdrop
case .denyAll:
AssetResource.iconDeclineAirdrop
.iconDeclineAirdrop
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ extension PreferenceSection.Row where SectionId == ManageThirdPartyDeposits.Sect
id: .depositRule(.acceptAll),
title: L10n.AccountSettings.ThirdPartyDeposits.acceptAll,
subtitle: L10n.AccountSettings.ThirdPartyDeposits.acceptAllSubtitle,
icon: .asset(AssetResource.iconAcceptAirdrop)
icon: .asset(.iconAcceptAirdrop)
)
}

Expand All @@ -98,7 +98,7 @@ extension PreferenceSection.Row where SectionId == ManageThirdPartyDeposits.Sect
id: .depositRule(.acceptKnown),
title: L10n.AccountSettings.ThirdPartyDeposits.onlyKnown,
subtitle: L10n.AccountSettings.ThirdPartyDeposits.onlyKnownSubtitle,
icon: .asset(AssetResource.iconAcceptKnownAirdrop)
icon: .asset(.iconAcceptKnownAirdrop)
)
}

Expand All @@ -108,7 +108,7 @@ extension PreferenceSection.Row where SectionId == ManageThirdPartyDeposits.Sect
title: L10n.AccountSettings.ThirdPartyDeposits.denyAll,
subtitle: L10n.AccountSettings.ThirdPartyDeposits.denyAllSubtitle,
hint: L10n.AccountSettings.ThirdPartyDeposits.denyAllWarning,
icon: .asset(AssetResource.iconDeclineAirdrop)
icon: .asset(.iconDeclineAirdrop)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ extension InteractionReview {
pasteboardClient.copyString(manifest)
} label: {
HStack(spacing: .small3) {
AssetIcon(.asset(AssetResource.copy))
AssetIcon(.asset(.copy))
Text(L10n.Common.copy)
.textStyle(.body1Header)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

struct SecurityFactors: Sendable, FeatureReducer {
struct State: Sendable, Hashable {
var seedPhrasesCount: Int?
var ledgerWalletsCount: Int?
var securityProblems: [SecurityProblem] = []

@PresentationState
Expand All @@ -14,13 +12,10 @@ struct SecurityFactors: Sendable, FeatureReducer {

enum ViewAction: Sendable, Equatable {
case task
case seedPhrasesButtonTapped
case ledgerWalletsButtonTapped
case factorSourceRowTapped(FactorSourceKind)
}

enum InternalAction: Sendable, Equatable {
case loadedSeedPhrasesCount(Int)
case loadedLedgerWalletsCount(Int)
case setSecurityProblems([SecurityProblem])
}

Expand All @@ -29,12 +24,14 @@ struct SecurityFactors: Sendable, FeatureReducer {
enum State: Sendable, Hashable {
case seedPhrases(DisplayMnemonics.State)
case ledgerWallets(LedgerHardwareDevices.State)
case todo
}

@CasePathable
enum Action: Sendable, Equatable {
case seedPhrases(DisplayMnemonics.Action)
case ledgerWallets(LedgerHardwareDevices.Action)
case todo(Never)
}

var body: some ReducerOf<Self> {
Expand Down Expand Up @@ -64,52 +61,31 @@ struct SecurityFactors: Sendable, FeatureReducer {
func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> {
switch viewAction {
case .task:
return loadSeedPhrasesCount()
.merge(with: loadLedgerWalletsCount())
.merge(with: securityProblemsEffect())

case .seedPhrasesButtonTapped:
state.destination = .seedPhrases(.init())
return .none

case .ledgerWalletsButtonTapped:
state.destination = .ledgerWallets(.init(context: .settings))
return securityProblemsEffect()

case let .factorSourceRowTapped(kind):
switch kind {
case .device:
state.destination = .seedPhrases(.init())
case .ledgerHqHardwareWallet:
state.destination = .ledgerWallets(.init(context: .settings))
case .arculusCard, .password, .offDeviceMnemonic:
state.destination = .todo
case .trustedContact, .securityQuestions:
fatalError("Unsupported Factor Source")
}
return .none
}
}

func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> {
switch internalAction {
case let .loadedSeedPhrasesCount(count):
state.seedPhrasesCount = count
return .none

case let .loadedLedgerWalletsCount(count):
state.ledgerWalletsCount = count
return .none

case let .setSecurityProblems(problems):
state.securityProblems = problems
return .none
}
}

private func loadSeedPhrasesCount() -> Effect<Action> {
.run { send in
try await send(.internal(.loadedSeedPhrasesCount(
factorSourcesClient.getFactorSources(type: DeviceFactorSource.self).count
)))
}
}

private func loadLedgerWalletsCount() -> Effect<Action> {
.run { send in
try await send(.internal(.loadedLedgerWalletsCount(
factorSourcesClient.getFactorSources(type: LedgerHardwareWalletFactorSource.self).count
)))
}
}

private func securityProblemsEffect() -> Effect<Action> {
.run { send in
for try await problems in await securityCenterClient.problems(.securityFactors) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
extension SecurityFactors.State {
var viewState: SecurityFactors.ViewState {
.init(
seedPhrasesCount: seedPhrasesCount,
ledgerWalletsCount: ledgerWalletsCount,
securityProblems: securityProblems
)
}
}

// MARK: - SecurityFactors.View

extension SecurityFactors {
struct ViewState: Equatable {
let seedPhrasesCount: Int?
let ledgerWalletsCount: Int?
let securityProblems: [SecurityProblem]
}

@MainActor
struct View: SwiftUI.View {
private let store: StoreOf<SecurityFactors>
Expand All @@ -39,7 +22,7 @@ extension SecurityFactors {
@MainActor
private extension SecurityFactors.View {
var content: some View {
WithViewStore(store, observe: \.viewState, send: { .view($0) }) { viewStore in
WithViewStore(store, observe: { $0 }, send: { .view($0) }) { viewStore in
ScrollView {
VStack(spacing: .zero) {
ForEachStatic(rows(viewStore: viewStore)) { kind in
Expand All @@ -54,50 +37,38 @@ private extension SecurityFactors.View {
}
}

func rows(viewStore: ViewStoreOf<SecurityFactors>) -> [SettingsRow<SecurityFactors>.Kind] {
func rows(viewStore: ViewStore<SecurityFactors.State, SecurityFactors.ViewAction>) -> [SettingsRow<SecurityFactors>.Kind] {
[
.header(L10n.SecurityFactors.subtitle),
.model(
title: L10n.SecurityFactors.SeedPhrases.title,
subtitle: L10n.SecurityFactors.SeedPhrases.subtitle,
detail: viewStore.seedPhrasesDetail,
hints: viewStore.seedPhraseHints,
icon: .asset(AssetResource.seedPhrases),
action: .seedPhrasesButtonTapped
),
.model(
title: L10n.SecurityFactors.LedgerWallet.title,
subtitle: L10n.SecurityFactors.LedgerWallet.subtitle,
detail: viewStore.ledgerWalletsDetail,
icon: .asset(AssetResource.ledger),
action: .ledgerWalletsButtonTapped
),
model(kind: .device, hints: viewStore.deviceHints),
.header(L10n.SecurityFactors.hardware),
model(kind: .arculusCard),
model(kind: .ledgerHqHardwareWallet),
.header(L10n.SecurityFactors.information),
model(kind: .password),
model(kind: .offDeviceMnemonic),
]
}

func model(kind: FactorSourceKind, hints: [Hint.ViewState] = []) -> SettingsRow<SecurityFactors>.Kind {
.model(
title: kind.title,
subtitle: kind.details,
hints: hints,
icon: .asset(kind.icon),
action: .factorSourceRowTapped(kind)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind.icon is an ImageResource, and not an ImageAsset. This is what triggered all the changes that you can see on the PR (basically use ImageResource instead of ImageAsset in SettingsRow).
This is a good change anyway since we are using Swift's native type rather than a custom one from Swiftgen library

)
}
}

// MARK: - Extensions

private extension SecurityFactors.ViewState {
var seedPhrasesDetail: String? {
guard let seedPhrasesCount else {
return nil
}
return seedPhrasesCount == 1 ? L10n.SecurityFactors.SeedPhrases.counterSingular : L10n.SecurityFactors.SeedPhrases.counterPlural(seedPhrasesCount)
}

var seedPhraseHints: [Hint.ViewState] {
private extension SecurityFactors.State {
var deviceHints: [Hint.ViewState] {
securityProblems
.compactMap(\.securityFactors)
.map { .init(kind: .warning, text: $0) }
}

var ledgerWalletsDetail: String? {
guard let ledgerWalletsCount else {
return nil
}
return ledgerWalletsCount == 1 ? L10n.SecurityFactors.LedgerWallet.counterSingular : L10n.SecurityFactors.LedgerWallet.counterPlural(ledgerWalletsCount)
}
}

private extension StoreOf<SecurityFactors> {
Expand All @@ -115,6 +86,7 @@ private extension View {
let destinationStore = store.destination
return seedPhrases(with: destinationStore)
.ledgerHardwareWallets(with: destinationStore)
.todo(with: destinationStore)
}

private func seedPhrases(with destinationStore: PresentationStoreOf<SecurityFactors.Destination>) -> some View {
Expand All @@ -124,15 +96,16 @@ private extension View {
}

private func ledgerHardwareWallets(with destinationStore: PresentationStoreOf<SecurityFactors.Destination>) -> some View {
navigationDestination(
store: destinationStore,
state: /SecurityFactors.Destination.State.ledgerWallets,
action: SecurityFactors.Destination.Action.ledgerWallets,
destination: {
LedgerHardwareDevices.View(store: $0)
.background(.app.gray5)
.radixToolbar(title: L10n.AccountSecuritySettings.LedgerHardwareWallets.title)
}
)
navigationDestination(store: destinationStore.scope(state: \.ledgerWallets, action: \.ledgerWallets)) {
LedgerHardwareDevices.View(store: $0)
.background(.app.gray5)
.radixToolbar(title: L10n.AccountSecuritySettings.LedgerHardwareWallets.title)
}
}

private func todo(with destinationStore: PresentationStoreOf<SecurityFactors.Destination>) -> some View {
sheet(store: destinationStore.scope(state: \.todo, action: \.todo)) { _ in
TodoView(feature: "Add factor")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ extension Preferences.View {
private func rows(viewStore: ViewStoreOf<Preferences>) -> [SettingsRow<Preferences>.Kind] {
let advancedLockToggle: SettingsRow<Preferences>.Kind? = if #unavailable(iOS 18) {
.toggleModel(
icon: AssetResource.advancedLock,
icon: .advancedLock,
title: L10n.Preferences.AdvancedLock.title,
subtitle: L10n.Preferences.AdvancedLock.subtitle,
minHeight: .zero,
Expand All @@ -108,7 +108,7 @@ extension Preferences.View {
.model(
title: L10n.Preferences.DepositGuarantees.title,
subtitle: L10n.Preferences.DepositGuarantees.subtitle,
icon: .asset(AssetResource.depositGuarantees),
icon: .asset(.depositGuarantees),
action: .depositGuaranteesButtonTapped
),
.header(L10n.Preferences.displayPreferences),
Expand All @@ -128,11 +128,11 @@ extension Preferences.View {
.header(L10n.Preferences.advancedPreferences),
.model(
title: L10n.Preferences.gateways,
icon: .asset(AssetResource.gateway),
icon: .asset(.gateway),
action: .gatewaysButtonTapped
),
.toggleModel(
icon: AssetResource.developerMode,
icon: .developerMode,
title: L10n.Preferences.DeveloperMode.title,
subtitle: L10n.Preferences.DeveloperMode.subtitle,
minHeight: .zero,
Expand All @@ -151,7 +151,7 @@ extension Preferences.View {
kind: .model(
title: "Export logs",
subtitle: "Export and save debugging logs",
icon: .asset(AssetResource.appSettings),
icon: .asset(.appSettings),
action: .exportLogsButtonTapped
),
store: store
Expand Down
Loading
Loading