Skip to content

Commit

Permalink
[ABW-4080] Shields List + Change main shield (#1456)
Browse files Browse the repository at this point in the history
Co-authored-by: Matias Bzurovski <matias.bzurovski@rdx.works>
Co-authored-by: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 12, 2025
1 parent d6f510f commit caaed68
Show file tree
Hide file tree
Showing 49 changed files with 1,584 additions and 100 deletions.
155 changes: 139 additions & 16 deletions RadixWallet.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "d677a3f3c7f1bb8748f5f2ab1451a0f6eeeda833551d9d9fc93b0b185a4dc071",
"originHash" : "cdc115bb25c110d5ce0486734b8147ec53e0b1fd94eb0bdd237449443ec30408",
"pins" : [
{
"identity" : "anycodable",
Expand Down Expand Up @@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/radixdlt/sargon",
"state" : {
"revision" : "8d2441173d08fea70a29fd4d2e903127e3013df8",
"version" : "1.1.134"
"revision" : "0cf5b6fc5e7cce6d0847500eb7571724f21b1920",
"version" : "1.1.137"
}
},
{
Expand Down
10 changes: 10 additions & 0 deletions RadixWallet/Core/DesignSystem/Components/RadioButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ struct RadioButton: View {
self.state = state
self.isDisabled = disabled
}

init(
appearance: Appearance,
isSelected: Bool,
disabled: Bool = false
) {
self.appearance = appearance
self.state = isSelected ? .selected : .unselected
self.isDisabled = disabled
}
}

extension RadioButton {
Expand Down
82 changes: 52 additions & 30 deletions RadixWallet/Core/DesignSystem/Components/Selection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,25 @@ struct Selection<Value: Hashable, Content: View>: View {
var selectedValues: Set<Value>
let values: OrderedSet<Value>
let requirement: SelectionRequirement
var showSelectAll: Bool = false
let content: (Item) -> Content

private var haveSelectedAll: Bool {
selectedValues == Set(values)
}

init(
_ selection: Binding<[Value]?>,
from values: some Sequence<Value>,
requiring requirement: SelectionRequirement,
showSelectAll: Bool = false,
@ViewBuilder content: @escaping (Item) -> Content
) {
self._selection = selection
self.selectedValues = selection.wrappedValue.map(Set.init) ?? []
self.values = OrderedSet(values)
self.requirement = requirement
self.showSelectAll = showSelectAll
self.content = content
}

Expand Down Expand Up @@ -83,44 +90,59 @@ struct Selection<Value: Hashable, Content: View>: View {
}

var body: some View {
ForEach(values, id: \.self) { value in
let isDisabled: Bool = {
guard !selectedValues.contains(value) else {
return false
}
switch requirement {
case let .exactly(count):
if count == 1 {
return false
VStack(spacing: .medium2) {
if showSelectAll, requirement != .exactly(1), values.count > 1 {
Button(haveSelectedAll ? L10n.ShieldWizardApplyShield.ChooseEntities.deselectAllButton : L10n.ShieldWizardApplyShield.ChooseEntities.selectAllButton) {
if haveSelectedAll {
selectedValues = []
} else {
return selectedValues.count >= count
selectedValues = Set(values)
}
case .atLeast:
return false
}
}()
content(
Item(
value: value,
isSelected: selectedValues.contains(value),
isDisabled: isDisabled,
action: {
if requirement == .exactly(1) {
if !selectedValues.contains(value) {
selectedValues.removeAll()
selectedValues.insert(value)
}
.buttonStyle(.blueText(textStyle: .secondaryHeader))
.flushedRight
.padding(.trailing, .small1)
}

ForEach(values, id: \.self) { value in
let isDisabled: Bool = {
guard !selectedValues.contains(value) else {
return false
}
switch requirement {
case let .exactly(count):
if count == 1 {
return false
} else {
if selectedValues.contains(value) {
selectedValues.remove(value)
return selectedValues.count >= count
}
case .atLeast:
return false
}
}()
content(
Item(
value: value,
isSelected: selectedValues.contains(value),
isDisabled: isDisabled,
action: {
if requirement == .exactly(1) {
if !selectedValues.contains(value) {
selectedValues.removeAll()
selectedValues.insert(value)
}
} else {
selectedValues.insert(value)
if selectedValues.contains(value) {
selectedValues.remove(value)
} else {
selectedValues.insert(value)
}
}
}
}
)
)
)
.disabled(isDisabled)
.disabled(isDisabled)
}
}
.onAppear {
updateResult(with: selectedValues, in: values, requiring: requirement)
Expand Down
5 changes: 5 additions & 0 deletions RadixWallet/Core/DesignSystem/HitTargetSize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public enum HitTargetSize: CGFloat, Sendable {
/// 64
case medium = 64

/// 80
case large = 80

/// 104
case veryLarge = 104

Expand Down Expand Up @@ -55,6 +58,8 @@ public enum HitTargetSize: CGFloat, Sendable {
.small2
case .medium:
.small1
case .large:
.medium3
case .veryLarge:
.medium3
case .huge:
Expand Down
10 changes: 8 additions & 2 deletions RadixWallet/Core/DesignSystem/Styles/BlueTextButtonStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import SwiftUI
// MARK: - BlueButtonStyle

extension ButtonStyle where Self == BlueTextButtonStyle {
static var blueText: BlueTextButtonStyle { .init() }
static var blueText: Self { .blueText() }

static func blueText(textStyle: TextStyle = .body1StandaloneLink) -> Self {
Self(textStyle: textStyle)
}
}

// MARK: - BlueTextButtonStyle
struct BlueTextButtonStyle: ButtonStyle {
let textStyle: TextStyle

func makeBody(configuration: ButtonStyle.Configuration) -> some View {
configuration.label
.textStyle(.body1StandaloneLink)
.textStyle(textStyle)
.foregroundColor(.app.blue2)
.opacity(configuration.isPressed ? 0.5 : 1)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ struct FactorSourceCard: View {
case .radioButton:
RadioButton(
appearance: .dark,
state: isSelected ? .selected : .unselected
isSelected: isSelected
)
case .checkmark:
CheckmarkView(appearance: .dark, isChecked: isSelected)
Expand Down
2 changes: 1 addition & 1 deletion RadixWallet/Core/FeaturePrelude/LedgerRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct LedgerRowView: View {
if let isSelected {
RadioButton(
appearance: .light,
state: isSelected ? .selected : .unselected
isSelected: isSelected
)
}
}
Expand Down
167 changes: 167 additions & 0 deletions RadixWallet/Core/FeaturePrelude/ShieldCard/ShieldCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Sargon
import SwiftUI

// MARK: - ShieldCard
struct ShieldCard: View {
let shield: ShieldForDisplay
let mode: Mode

var body: some View {
card
}

private var card: some View {
HStack(spacing: .small2) {
Image(shield.status.image)
.resizable()
.frame(iconSize)
.padding(.vertical, iconVerticalPadding)

VStack(alignment: .leading, spacing: .small2) {
Text(shield.metadata.displayName.rawValue)
.textStyle(.body1Header)
.foregroundStyle(.app.gray1)

if mode == .display {
displayInfo
}
}

Spacer()

if case let .selection(isSelected) = mode {
RadioButton(
appearance: .dark,
isSelected: isSelected
)
}
}
.padding(.vertical, verticalPadding)
.padding(.leading, .medium2)
.padding(.trailing, .medium3)
.background(.app.white)
.roundedCorners(radius: .small1)
.cardShadow
}

private var displayInfo: some SwiftUI.View {
VStack(alignment: .leading, spacing: .small2) {
VStack(alignment: .leading, spacing: .zero) {
Text(L10n.SecurityShields.Assigned.title)
.textStyle(.body2HighImportance)

Text(assignedEntitiesText)
.textStyle(.body2Regular)
}
.foregroundStyle(.app.gray2)

if let statusMessage = shield.status.statusMessageInfo {
StatusMessageView(
text: statusMessage.text,
type: statusMessage.type,
useNarrowSpacing: true,
useSmallerFontSize: true
)
}
}
}
}

// MARK: ShieldCard.Mode
extension ShieldCard {
enum Mode: Equatable, Sendable {
case display
case selection(isSelected: Bool)
}
}

private extension ShieldCard {
var verticalPadding: CGFloat {
switch mode {
case .display:
.medium2
case .selection:
.medium3
}
}

var iconVerticalPadding: CGFloat {
switch mode {
case .display:
.small2
case .selection:
.zero
}
}

var iconSize: HitTargetSize {
switch mode {
case .display:
.large
case .selection:
.slightlySmaller
}
}

private var assignedEntitiesText: String {
typealias Assigned = L10n.SecurityShields.Assigned
let accountsCount = Int(shield.numberOfLinkedAccounts)
let personasCount = Int(shield.numberOfLinkedPersonas)

var accountsString: String?
if accountsCount > 0 {
accountsString = accountsCount == 1 ? Assigned.accountSingular : Assigned.accountPlural(accountsCount)
}

var personasString: String?
if personasCount > 0 {
personasString = personasCount == 1 ? Assigned.personaSingular : Assigned.personaPlural(personasCount)
}

let entitiesText = [accountsString, personasString]
.compactMap { $0 }
.joined(separator: "")

return entitiesText.isEmpty ? L10n.Common.none : entitiesText
}
}

// MARK: - ShieldCardStatus
// TODO: define in Sargon ------------------
enum ShieldCardStatus {
case applied
case actionRequired
case notApplied
}

extension ShieldForDisplay {
var status: ShieldCardStatus {
.notApplied
}
}

// -----------------------------------------

private extension ShieldCardStatus {
var image: ImageResource {
switch self {
case .applied:
.shieldStatusApplied
case .actionRequired:
.shieldStatusActionRequired
case .notApplied:
.shieldStatusNotApplied
}
}

var statusMessageInfo: ShieldStatusMessageInfo? {
switch self {
case .applied:
.general(type: .success, text: L10n.SecurityShields.Status.applied)
case .actionRequired:
.general(type: .warning, text: L10n.SecurityShields.Status.actionRequired)
case .notApplied:
nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal enum AssetResource {
internal static let iconDeclineAirdrop = ImageAsset(name: "icon-decline-airdrop")
internal static let iconMinusCircle = ImageAsset(name: "icon-minus-circle")
internal static let iconPlusCircle = ImageAsset(name: "icon-plus-circle")
internal static let applyShieldIntro = ImageAsset(name: "applyShieldIntro")
internal static let addAccount = ImageAsset(name: "addAccount")
internal static let addMessage = ImageAsset(name: "addMessage")
internal static let chooseAccount = ImageAsset(name: "chooseAccount")
Expand Down Expand Up @@ -147,6 +148,9 @@ internal enum AssetResource {
internal static let emergencyFallbackCalendar = ImageAsset(name: "emergencyFallbackCalendar")
internal static let recoverySetup = ImageAsset(name: "recoverySetup")
internal static let regularAccessSetup = ImageAsset(name: "regularAccessSetup")
internal static let shieldStatusActionRequired = ImageAsset(name: "shieldStatusActionRequired")
internal static let shieldStatusApplied = ImageAsset(name: "shieldStatusApplied")
internal static let shieldStatusNotApplied = ImageAsset(name: "shieldStatusNotApplied")
internal static let splash = ImageAsset(name: "Splash")
internal static let splashItem1 = ImageAsset(name: "splash-item-1")
internal static let splashItem2 = ImageAsset(name: "splash-item-2")
Expand Down
Loading

0 comments on commit caaed68

Please sign in to comment.