diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd64266..067be98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ directly impact users rather than highlighting other crucial architectural updat ### Changed - The confirmation button at recovery phrase screen changed its name from "I got it" to "I've saved it". +- Receive tab shows 1 QR code at a time with ability to switch between them. ## 1.0.6 build 4 (2024-04-30) diff --git a/modules/Package.swift b/modules/Package.swift index d6f9ce9b..e5d7b8ba 100644 --- a/modules/Package.swift +++ b/modules/Package.swift @@ -26,6 +26,7 @@ let package = Package( .library(name: "FeedbackGenerator", targets: ["FeedbackGenerator"]), .library(name: "FileManager", targets: ["FileManager"]), .library(name: "Generated", targets: ["Generated"]), + .library(name: "HideBalances", targets: ["HideBalances"]), .library(name: "Home", targets: ["Home"]), .library(name: "ImportWallet", targets: ["ImportWallet"]), .library(name: "LocalAuthenticationHandler", targets: ["LocalAuthenticationHandler"]), @@ -235,6 +236,14 @@ let package = Package( name: "Generated", resources: [.process("Resources")] ), + .target( + name: "HideBalances", + dependencies: [ + "UserDefaults", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + ], + path: "Sources/Dependencies/HideBalances" + ), .target( name: "Home", dependencies: [ @@ -419,6 +428,7 @@ let package = Package( "DiskSpaceChecker", "ExportLogs", "Generated", + "HideBalances", "MnemonicClient", "Models", "NotEnoughFreeSpace", @@ -596,6 +606,7 @@ let package = Package( "AddressDetails", "BalanceBreakdown", "Generated", + "HideBalances", "Home", "RestoreWalletStorage", "SendFlow", @@ -628,6 +639,7 @@ let package = Package( "BalanceFormatter", "DerivationTool", "Generated", + "HideBalances", "NumberFormatter", "SupportDataGenerator", "Utils", diff --git a/modules/Sources/Dependencies/HideBalances/HideBalancesInterface.swift b/modules/Sources/Dependencies/HideBalances/HideBalancesInterface.swift new file mode 100644 index 00000000..04fc5782 --- /dev/null +++ b/modules/Sources/Dependencies/HideBalances/HideBalancesInterface.swift @@ -0,0 +1,27 @@ +// +// HideBalancesInterface.swift +// +// +// Created by Lukáš Korba on 11.11.2023. +// + +import Foundation +import ComposableArchitecture +import Combine + +extension DependencyValues { + public var hideBalances: HideBalancesClient { + get { self[HideBalancesClient.self] } + set { self[HideBalancesClient.self] = newValue } + } +} + +public struct HideBalancesClient { + public enum Constants { + static let udHideBalances = "udHideBalances" + } + + public var prepare: @Sendable () -> Void + public var value: @Sendable () -> CurrentValueSubject + public var updateValue: @Sendable (Bool) -> Void +} diff --git a/modules/Sources/Dependencies/HideBalances/HideBalancesLiveKey.swift b/modules/Sources/Dependencies/HideBalances/HideBalancesLiveKey.swift new file mode 100644 index 00000000..f3ff5d6e --- /dev/null +++ b/modules/Sources/Dependencies/HideBalances/HideBalancesLiveKey.swift @@ -0,0 +1,35 @@ +// +// HideBalancesLiveKey.swift +// +// +// Created by Lukáš Korba on 11.11.2023. +// + +import Foundation +import ComposableArchitecture +import Combine +import UserDefaults + +extension HideBalancesClient: DependencyKey { + public static var liveValue: Self { + let storage = CurrentValueSubject(false) + + return .init( + prepare: { + @Dependency(\.userDefaults) var userDefaults + + if let value = userDefaults.objectForKey(Constants.udHideBalances) as? Bool { + storage.value = value + } + }, + value: { storage }, + updateValue: { + @Dependency(\.userDefaults) var userDefaults + + userDefaults.setValue($0, Constants.udHideBalances) + + storage.value = $0 + } + ) + } +} diff --git a/modules/Sources/Dependencies/HideBalances/HideBalancesTestKey.swift b/modules/Sources/Dependencies/HideBalances/HideBalancesTestKey.swift new file mode 100644 index 00000000..a006dd65 --- /dev/null +++ b/modules/Sources/Dependencies/HideBalances/HideBalancesTestKey.swift @@ -0,0 +1,26 @@ +// +// HideBalancesTestKey.swift +// +// +// Created by Lukáš Korba on 11.11.2023. +// + +import ComposableArchitecture +import XCTestDynamicOverlay +import Combine + +extension HideBalancesClient: TestDependencyKey { + public static let testValue = Self( + prepare: XCTUnimplemented("\(Self.self).prepare"), + value: XCTUnimplemented("\(Self.self).value", placeholder: .init(false)), + updateValue: XCTUnimplemented("\(Self.self).updateValue") + ) +} + +extension HideBalancesClient { + public static let noOp = Self( + prepare: { }, + value: { .init(false) }, + updateValue: { _ in } + ) +} diff --git a/modules/Sources/Features/AddressDetails/AddressDetailsStore.swift b/modules/Sources/Features/AddressDetails/AddressDetailsStore.swift index 0f541107..2543b85f 100644 --- a/modules/Sources/Features/AddressDetails/AddressDetailsStore.swift +++ b/modules/Sources/Features/AddressDetails/AddressDetailsStore.swift @@ -13,12 +13,17 @@ import Pasteboard import Generated import Utils -public typealias AddressDetailsStore = Store -public typealias AddressDetailsViewStore = ViewStore - -public struct AddressDetailsReducer: Reducer { +@Reducer +public struct AddressDetails { + @ObservableState public struct State: Equatable { + public enum Selection: Equatable, Hashable, CaseIterable { + case ua + case transparent + } + public var addressToShare: RedactableString? + public var selection: Selection public var uAddress: UnifiedAddress? public var uaQR: CGImage? public var taQR: CGImage? @@ -47,14 +52,17 @@ public struct AddressDetailsReducer: Reducer { public init( addressToShare: RedactableString? = nil, + selection: Selection = .ua, uAddress: UnifiedAddress? = nil ) { self.addressToShare = addressToShare + self.selection = selection self.uAddress = uAddress } } - public enum Action: Equatable { + public enum Action: BindableAction, Equatable { + case binding(BindingAction) case copyToPastboard(RedactableString) case rememberQR(CGImage?, Bool) case shareFinished @@ -65,47 +73,34 @@ public struct AddressDetailsReducer: Reducer { public init() { } - public func reduce(into state: inout State, action: Action) -> ComposableArchitecture.Effect { - switch action { - case .copyToPastboard(let text): - pasteboard.setString(text) - return .none - - case let .rememberQR(image, uaQR): - if uaQR { - state.uaQR = image - } else { - state.taQR = image - } - return .none - - case .shareFinished: - state.addressToShare = nil - return .none + public var body: some Reducer { + BindingReducer() - case .shareQR(let text): - state.addressToShare = text - return .none + Reduce { state, action in + switch action { + case .binding: + return .none + + case let .rememberQR(image, uaQR): + if uaQR { + state.uaQR = image + } else { + state.taQR = image + } + return .none + + case .copyToPastboard(let text): + pasteboard.setString(text) + return .none + + case .shareFinished: + state.addressToShare = nil + return .none + + case .shareQR(let text): + state.addressToShare = text + return .none + } } } } - -// MARK: - Placeholders - -extension AddressDetailsReducer.State { - public static let initial = AddressDetailsReducer.State() - - public static let demo = AddressDetailsReducer.State( - uAddress: try! UnifiedAddress( - encoding: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3", - network: .testnet) - ) -} - -extension AddressDetailsStore { - public static let placeholder = AddressDetailsStore( - initialState: .initial - ) { - AddressDetailsReducer() - } -} diff --git a/modules/Sources/Features/AddressDetails/AddressDetailsView.swift b/modules/Sources/Features/AddressDetails/AddressDetailsView.swift index 0def6ad6..cd66e41d 100644 --- a/modules/Sources/Features/AddressDetails/AddressDetailsView.swift +++ b/modules/Sources/Features/AddressDetails/AddressDetailsView.swift @@ -22,62 +22,64 @@ public struct AddressDetailsView: View { @Environment(\.colorScheme) var colorScheme - let store: AddressDetailsStore + @Perception.Bindable var store: StoreOf let networkType: NetworkType - public init(store: AddressDetailsStore, networkType: NetworkType) { + public init(store: StoreOf, networkType: NetworkType) { self.store = store self.networkType = networkType } public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - ScrollView { - addressBlock(type: .uaAddress, viewStore: viewStore, L10n.AddressDetails.ua, viewStore.unifiedAddress) { - viewStore.send(.copyToPastboard(viewStore.unifiedAddress.redacted)) - } shareAction: { - viewStore.send(.shareQR(viewStore.unifiedAddress.redacted)) - } - .frame(maxWidth: .infinity) - .padding(.top, 20) - -#if DEBUG - if networkType == .testnet { - addressBlock(type: .saplingAddress, viewStore: viewStore, L10n.AddressDetails.sa, viewStore.saplingAddress) { - viewStore.send(.copyToPastboard(viewStore.saplingAddress.redacted)) - } shareAction: { - viewStore.send(.shareQR(viewStore.saplingAddress.redacted)) + WithPerceptionTracking { + VStack { + zashiPicker() + + ScrollView { + Group { + if store.selection == .ua { + addressBlock(type: .uaAddress, L10n.AddressDetails.ua, store.unifiedAddress) { + store.send(.copyToPastboard(store.unifiedAddress.redacted)) + } shareAction: { + store.send(.shareQR(store.unifiedAddress.redacted)) + } + } else { + addressBlock(type: .tAddress, L10n.AddressDetails.ta, store.transparentAddress) { + store.send(.copyToPastboard(store.transparentAddress.redacted)) + } shareAction: { + store.send(.shareQR(store.transparentAddress.redacted)) + } + } + } + .frame(maxWidth: .infinity) + .padding(.top, 20) + +#if DEBUG + if networkType == .testnet { + addressBlock(type: .saplingAddress, L10n.AddressDetails.sa, store.saplingAddress) { + store.send(.copyToPastboard(store.saplingAddress.redacted)) + } shareAction: { + store.send(.shareQR(store.saplingAddress.redacted)) + } } - } #endif - - addressBlock(type: .tAddress, viewStore: viewStore, L10n.AddressDetails.ta, viewStore.transparentAddress) { - viewStore.send(.copyToPastboard(viewStore.transparentAddress.redacted)) - } shareAction: { - viewStore.send(.shareQR(viewStore.transparentAddress.redacted)) + + shareView() } - - shareLogsView(viewStore) } - .padding(.vertical, 1) .applyScreenBackground() } } @ViewBuilder private func addressBlock( type: AddressType, - viewStore: AddressDetailsViewStore, _ title: String, _ address: String, _ copyAction: @escaping () -> Void, shareAction: @escaping () -> Void ) -> some View { VStack { - Text(title) - .font(.custom(FontFamily.Archivo.semiBold.name, size: 16)) - .padding(.bottom, 20) - - qrCode(address, type: type, viewStore: viewStore) + qrCode(address, type: type) .frame(width: 270, height: 270) .padding(.bottom, 20) @@ -125,19 +127,58 @@ public struct AddressDetailsView: View { } .padding(.bottom, 40) } + + @ViewBuilder private func zashiPicker() -> some View { + ZashiPicker( + AddressDetails.State.Selection.allCases, + selection: store.selection, + itemBuilder: { item in + Text(item == .ua + ? L10n.AddressDetails.ua + :L10n.AddressDetails.ta + ).tag(item == .ua + ? AddressDetails.State.Selection.ua + : AddressDetails.State.Selection.transparent + ) + .foregroundColor( + store.selection == item + ? Asset.Colors.pickerTitleSelected.color + : Asset.Colors.pickerTitleUnselected.color + ) + .padding(.vertical, 12) + .padding(.horizontal, 8) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + .font(.custom(FontFamily.Archivo.semiBold.name, size: 12)) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.150)) { + store.selection = item + } + } + .background( + store.selection == item + ? Asset.Colors.pickerSelection.color + : Asset.Colors.pickerBcg.color + ) + } + ) + .border(Asset.Colors.pickerBcg.color, width: 4) + .frame(width: 270) + .padding(.top, 50) + } } extension AddressDetailsView { - public func qrCode(_ qrText: String, type: AddressType, viewStore: AddressDetailsViewStore) -> some View { + public func qrCode(_ qrText: String, type: AddressType) -> some View { var storedImg: CGImage? switch type { case .saplingAddress: storedImg = nil case .tAddress: - storedImg = viewStore.taQR + storedImg = store.taQR case .uaAddress: - storedImg = viewStore.uaQR + storedImg = store.uaQR } if let storedImg { @@ -156,14 +197,14 @@ extension AddressDetailsView { } } - @ViewBuilder func shareLogsView(_ viewStore: AddressDetailsViewStore) -> some View { - if let addressToShare = viewStore.addressToShare, + @ViewBuilder func shareView() -> some View { + if let addressToShare = store.addressToShare, let cgImg = QRCodeGenerator.generate( from: addressToShare.data, color: colorScheme == .dark ? Asset.Colors.shade85.systemColor : Asset.Colors.primary.systemColor ) { UIShareDialogView(activityItems: [UIImage(cgImage: cgImg)]) { - viewStore.send(.shareFinished) + store.send(.shareFinished) } // UIShareDialogView only wraps UIActivityViewController presentation // so frame is set to 0 to not break SwiftUIs layout @@ -176,6 +217,26 @@ extension AddressDetailsView { #Preview { NavigationView { - AddressDetailsView(store: .placeholder, networkType: .testnet) + AddressDetailsView(store: AddressDetails.placeholder, networkType: .testnet) + } +} + +// MARK: - Placeholders + +extension AddressDetails.State { + public static let initial = AddressDetails.State() + + public static let demo = AddressDetails.State( + uAddress: try! UnifiedAddress( + encoding: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3", + network: .testnet) + ) +} + +extension AddressDetails { + public static let placeholder = StoreOf( + initialState: .initial + ) { + AddressDetails() } } diff --git a/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownView.swift b/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownView.swift index 9fc49153..52f90046 100644 --- a/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownView.swift +++ b/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownView.swift @@ -105,7 +105,8 @@ extension BalanceBreakdownView { fontName: FontFamily.Archivo.semiBold.name, mostSignificantFontSize: 16, leastSignificantFontSize: 8, - format: .expanded + format: .expanded, + couldBeHidden: true ) Asset.Assets.shield.image @@ -127,7 +128,8 @@ extension BalanceBreakdownView { fontName: FontFamily.Archivo.semiBold.name, mostSignificantFontSize: 16, leastSignificantFontSize: 8, - format: .expanded + format: .expanded, + couldBeHidden: true ) .foregroundColor(Asset.Colors.shade55.color) .padding(.trailing, viewStore.changePending.amount > 0 ? 0 : 21) @@ -149,7 +151,8 @@ extension BalanceBreakdownView { fontName: FontFamily.Archivo.semiBold.name, mostSignificantFontSize: 16, leastSignificantFontSize: 8, - format: .expanded + format: .expanded, + couldBeHidden: true ) .foregroundColor(Asset.Colors.shade55.color) .padding(.trailing, viewStore.pendingTransactions.amount > 0 ? 0 : 21) @@ -199,7 +202,8 @@ extension BalanceBreakdownView { fontName: FontFamily.Archivo.semiBold.name, mostSignificantFontSize: 16, leastSignificantFontSize: 8, - format: .expanded + format: .expanded, + couldBeHidden: true ) .foregroundColor(Asset.Colors.shade55.color) } diff --git a/modules/Sources/Features/Root/RootInitialization.swift b/modules/Sources/Features/Root/RootInitialization.swift index a6d13164..a475bfa1 100644 --- a/modules/Sources/Features/Root/RootInitialization.swift +++ b/modules/Sources/Features/Root/RootInitialization.swift @@ -47,6 +47,7 @@ extension RootReducer { switch action { case .initialization(.appDelegate(.didFinishLaunching)): state.appStartState = .didFinishLaunching + hideBalances.prepare() // TODO: [#704], trigger the review request logic when approved by the team, // https://github.com/Electric-Coin-Company/zashi-ios/issues/704 return .concatenate( diff --git a/modules/Sources/Features/Root/RootStore.swift b/modules/Sources/Features/Root/RootStore.swift index 65ee2cbe..20989f90 100644 --- a/modules/Sources/Features/Root/RootStore.swift +++ b/modules/Sources/Features/Root/RootStore.swift @@ -23,6 +23,7 @@ import BackgroundTasks import RestoreWalletStorage import Utils import UserDefaults +import HideBalances public typealias RootStore = Store public typealias RootViewStore = ViewStore @@ -120,6 +121,7 @@ public struct RootReducer: Reducer { @Dependency(\.deeplink) var deeplink @Dependency(\.derivationTool) var derivationTool @Dependency(\.diskSpaceChecker) var diskSpaceChecker + @Dependency(\.hideBalances) var hideBalances @Dependency(\.mainQueue) var mainQueue @Dependency(\.mnemonic) var mnemonic @Dependency(\.numberFormatter) var numberFormatter diff --git a/modules/Sources/Features/Tabs/TabsStore.swift b/modules/Sources/Features/Tabs/TabsStore.swift index 78862e66..d7c78c87 100644 --- a/modules/Sources/Features/Tabs/TabsStore.swift +++ b/modules/Sources/Features/Tabs/TabsStore.swift @@ -47,7 +47,7 @@ public struct TabsReducer: Reducer { } } - public var addressDetailsState: AddressDetailsReducer.State + public var addressDetailsState: AddressDetails.State public var balanceBreakdownState: BalanceBreakdownReducer.State public var destination: Destination? public var homeState: HomeReducer.State @@ -57,7 +57,7 @@ public struct TabsReducer: Reducer { public var settingsState: SettingsReducer.State public init( - addressDetailsState: AddressDetailsReducer.State, + addressDetailsState: AddressDetails.State, balanceBreakdownState: BalanceBreakdownReducer.State, destination: Destination? = nil, homeState: HomeReducer.State, @@ -78,7 +78,7 @@ public struct TabsReducer: Reducer { } public enum Action: Equatable { - case addressDetails(AddressDetailsReducer.Action) + case addressDetails(AddressDetails.Action) case balanceBreakdown(BalanceBreakdownReducer.Action) case home(HomeReducer.Action) case restoreWalletTask @@ -99,7 +99,7 @@ public struct TabsReducer: Reducer { } Scope(state: \.addressDetailsState, action: /Action.addressDetails) { - AddressDetailsReducer() + AddressDetails() } Scope(state: \.balanceBreakdownState, action: /Action.balanceBreakdown) { diff --git a/modules/Sources/Features/Tabs/TabsView.swift b/modules/Sources/Features/Tabs/TabsView.swift index b1d494e4..d1e3a0cb 100644 --- a/modules/Sources/Features/Tabs/TabsView.swift +++ b/modules/Sources/Features/Tabs/TabsView.swift @@ -16,6 +16,7 @@ import Home import SendFlow import Settings import UIComponents +import HideBalances public struct TabsView: View { let networkType: NetworkType @@ -23,6 +24,9 @@ public struct TabsView: View { let tokenName: String @Namespace var tabsID + @Dependency(\.hideBalances) var hideBalances + @State var areBalancesHidden = false + public init(store: TabsStore, tokenName: String, networkType: NetworkType) { self.store = store self.tokenName = tokenName @@ -119,9 +123,13 @@ public struct TabsView: View { } .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: settingsButton(store)) + .navigationBarItems(leading: hideBalancesButton(store)) .zashiTitle { navBarView(tab.state) } .restoringWalletBadge(isOn: isRestoringWallet.state) .task { await store.send(.restoreWalletTask).finish() } + .onAppear { + areBalancesHidden = hideBalances.value().value + } } } } @@ -160,6 +168,23 @@ public struct TabsView: View { .tint(Asset.Colors.primary.color) } } + + func hideBalancesButton(_ store: TabsStore) -> some View { + WithViewStore(store, observe: { $0 }) { viewStore in + Button { + var prevValue = hideBalances.value().value + prevValue.toggle() + areBalancesHidden = prevValue + hideBalances.updateValue(areBalancesHidden) + } label: { + Image(systemName: areBalancesHidden ? "eye.slash" : "eye") + .resizable() + .frame(width: 21, height: 15) + .padding(15) + .tint(Asset.Colors.primary.color) + } + } + } } #Preview { diff --git a/modules/Sources/Features/TransactionList/Views/TransactionHeaderView.swift b/modules/Sources/Features/TransactionList/Views/TransactionHeaderView.swift index 5c44d354..2dbf7784 100644 --- a/modules/Sources/Features/TransactionList/Views/TransactionHeaderView.swift +++ b/modules/Sources/Features/TransactionList/Views/TransactionHeaderView.swift @@ -125,7 +125,8 @@ struct TransactionHeaderView: View { leastSignificantFontSize: 8, prefixSymbol: transaction.isSpending ? .minus : .plus, format: transaction.isExpanded ? .expanded : .abbreviated, - strikethrough: transaction.status == .failed + strikethrough: transaction.status == .failed, + couldBeHidden: true ) .foregroundColor(transaction.balanceColor) } diff --git a/modules/Sources/Generated/L10n.swift b/modules/Sources/Generated/L10n.swift index cf1b87ed..f5344428 100644 --- a/modules/Sources/Generated/L10n.swift +++ b/modules/Sources/Generated/L10n.swift @@ -131,6 +131,12 @@ public enum L10n { public static func fee(_ p1: Any) -> String { return L10n.tr("Localizable", "general.fee", String(describing: p1), fallback: "Typical Fee < %@") } + /// ____ + public static let hideBalancesLeast = L10n.tr("Localizable", "general.hideBalancesLeast", fallback: "____") + /// ---.---- + public static let hideBalancesMost = L10n.tr("Localizable", "general.hideBalancesMost", fallback: "---.----") + /// -.--- + public static let hideBalancesMostStandalone = L10n.tr("Localizable", "general.hideBalancesMostStandalone", fallback: "-.---") /// Max public static let max = L10n.tr("Localizable", "general.max", fallback: "Max") /// Next diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/pickerBcg.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/pickerBcg.colorset/Contents.json new file mode 100644 index 00000000..8cb5e729 --- /dev/null +++ b/modules/Sources/Generated/Resources/Colors.xcassets/pickerBcg.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0", + "green" : "0", + "red" : "0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x4D", + "green" : "0x4D", + "red" : "0x4D" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/pickerSelection.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/pickerSelection.colorset/Contents.json new file mode 100644 index 00000000..9c0e331e --- /dev/null +++ b/modules/Sources/Generated/Resources/Colors.xcassets/pickerSelection.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/pickerTitleSelected.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/pickerTitleSelected.colorset/Contents.json new file mode 100644 index 00000000..0c600f92 --- /dev/null +++ b/modules/Sources/Generated/Resources/Colors.xcassets/pickerTitleSelected.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/pickerTitleUnselected.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/pickerTitleUnselected.colorset/Contents.json new file mode 100644 index 00000000..791dd88f --- /dev/null +++ b/modules/Sources/Generated/Resources/Colors.xcassets/pickerTitleUnselected.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDD", + "green" : "0xDD", + "red" : "0xDD" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Localizable.strings b/modules/Sources/Generated/Resources/Localizable.strings index 99714a58..daaa4eb1 100644 --- a/modules/Sources/Generated/Resources/Localizable.strings +++ b/modules/Sources/Generated/Resources/Localizable.strings @@ -176,6 +176,9 @@ Sharing this private data is irrevocable — once you have shared this private d "qrCodeFor" = "QR Code for %@"; "general.tapToCopy" = "Tap to copy"; "general.fee" = "Typical Fee < %@"; +"general.hideBalancesMost" = "---.----"; +"general.hideBalancesMostStandalone" = "-.---"; +"general.hideBalancesLeast" = "____"; // MARK: - Transaction List "transactionList.collapse" = "Collapse transaction"; diff --git a/modules/Sources/Generated/XCAssets+Generated.swift b/modules/Sources/Generated/XCAssets+Generated.swift index b4f2aa3b..ccddc07c 100644 --- a/modules/Sources/Generated/XCAssets+Generated.swift +++ b/modules/Sources/Generated/XCAssets+Generated.swift @@ -49,6 +49,10 @@ public enum Asset { public static let btnSecondary = ColorAsset(name: "btnSecondary") public static let error = ColorAsset(name: "error") public static let messageBcgReceived = ColorAsset(name: "messageBcgReceived") + public static let pickerBcg = ColorAsset(name: "pickerBcg") + public static let pickerSelection = ColorAsset(name: "pickerSelection") + public static let pickerTitleSelected = ColorAsset(name: "pickerTitleSelected") + public static let pickerTitleUnselected = ColorAsset(name: "pickerTitleUnselected") public static let primary = ColorAsset(name: "primary") public static let primaryTint = ColorAsset(name: "primaryTint") public static let restoreUI = ColorAsset(name: "restoreUI") diff --git a/modules/Sources/UIComponents/Balance/AvailableBalanceView.swift b/modules/Sources/UIComponents/Balance/AvailableBalanceView.swift index ed9dfbfe..486ef062 100644 --- a/modules/Sources/UIComponents/Balance/AvailableBalanceView.swift +++ b/modules/Sources/UIComponents/Balance/AvailableBalanceView.swift @@ -48,7 +48,8 @@ public struct AvailableBalanceView: View { fontName: FontFamily.Inter.bold.name, mostSignificantFontSize: 14, leastSignificantFontSize: 7, - format: .expanded + format: .expanded, + couldBeHidden: true ) } diff --git a/modules/Sources/UIComponents/Balance/BalanceWithIconView.swift b/modules/Sources/UIComponents/Balance/BalanceWithIconView.swift index f7bd2af3..a4bd7582 100644 --- a/modules/Sources/UIComponents/Balance/BalanceWithIconView.swift +++ b/modules/Sources/UIComponents/Balance/BalanceWithIconView.swift @@ -23,7 +23,8 @@ public struct BalanceWithIconView: View { fontName: FontFamily.Archivo.semiBold.name, mostSignificantFontSize: 42, leastSignificantFontSize: 10, - format: .expanded + format: .expanded, + couldBeHidden: true ) Circle() diff --git a/modules/Sources/UIComponents/Balance/ZatoshiRepresentationView.swift b/modules/Sources/UIComponents/Balance/ZatoshiRepresentationView.swift index e112fd78..6a7a408d 100644 --- a/modules/Sources/UIComponents/Balance/ZatoshiRepresentationView.swift +++ b/modules/Sources/UIComponents/Balance/ZatoshiRepresentationView.swift @@ -12,6 +12,8 @@ import Utils import ComposableArchitecture import BalanceFormatter import XCTestDynamicOverlay +import HideBalances +import Combine public struct ZatoshiRepresentationView: View { let zatoshiStringRepresentation: ZatoshiStringRepresentation @@ -21,7 +23,12 @@ public struct ZatoshiRepresentationView: View { let format: ZatoshiStringRepresentation.Format let strikethrough: Bool let isFee: Bool + let couldBeHidden: Bool + @Dependency(\.hideBalances) var hideBalances + @State var isHidden = false + @State private var cancellable: AnyCancellable? + public init( balance: Zatoshi, fontName: String, @@ -30,7 +37,8 @@ public struct ZatoshiRepresentationView: View { leastSignificantFontSize: CGFloat = 0, prefixSymbol: ZatoshiStringRepresentation.PrefixSymbol = .none, format: ZatoshiStringRepresentation.Format = .abbreviated, - strikethrough: Bool = false + strikethrough: Bool = false, + couldBeHidden: Bool = false ) { if !_XCTIsTesting { @Dependency(\.balanceFormatter) var balanceFormatter @@ -53,6 +61,7 @@ public struct ZatoshiRepresentationView: View { self.format = format self.strikethrough = strikethrough self.isFee = isFee + self.couldBeHidden = couldBeHidden } public var body: some View { @@ -62,19 +71,36 @@ public struct ZatoshiRepresentationView: View { .font(.custom(fontName, size: mostSignificantFontSize)) } else { if format == .expanded { - Text(zatoshiStringRepresentation.mostSignificantDigits) - .font(.custom(fontName, size: mostSignificantFontSize)) - .conditionalStrikethrough(strikethrough) - + Text(zatoshiStringRepresentation.leastSignificantDigits) - .font(.custom(fontName, size: leastSignificantFontSize)) - .conditionalStrikethrough(strikethrough) + Text(couldBeHidden && isHidden + ? L10n.General.hideBalancesMost + : zatoshiStringRepresentation.mostSignificantDigits + ) + .font(.custom(fontName, size: mostSignificantFontSize)) + .conditionalStrikethrough(strikethrough) + + Text(couldBeHidden && isHidden + ? L10n.General.hideBalancesLeast + : zatoshiStringRepresentation.leastSignificantDigits + ) + .font(.custom(fontName, size: leastSignificantFontSize)) + .conditionalStrikethrough(strikethrough) } else { - Text(zatoshiStringRepresentation.mostSignificantDigits) - .font(.custom(fontName, size: mostSignificantFontSize)) - .conditionalStrikethrough(strikethrough) + Text(couldBeHidden && isHidden + ? L10n.General.hideBalancesMostStandalone + : zatoshiStringRepresentation.mostSignificantDigits + ) + .font(.custom(fontName, size: mostSignificantFontSize)) + .conditionalStrikethrough(strikethrough) } } } + .onAppear { + cancellable = hideBalances.value().sink { val in + isHidden = val + } + } + .onDisappear { + cancellable?.cancel() + } } } diff --git a/modules/Sources/UIComponents/Picker/ZashiPicker.swift b/modules/Sources/UIComponents/Picker/ZashiPicker.swift new file mode 100644 index 00000000..517eba98 --- /dev/null +++ b/modules/Sources/UIComponents/Picker/ZashiPicker.swift @@ -0,0 +1,34 @@ +// +// ZashiPicker.swift +// +// +// Created by Lukáš Korba on 03.05.2024. +// + +import SwiftUI + +public struct ZashiPicker : View where Data: Hashable, Content: View { + public let sources: [Data] + public let selection: Data? + private let itemBuilder: (Data) -> Content + + public init( + _ sources: [Data], + selection: Data?, + @ViewBuilder itemBuilder: @escaping (Data) -> Content + ) { + self.sources = sources + self.selection = selection + self.itemBuilder = itemBuilder + } + + public var body: some View { + ZStack(alignment: .center) { + HStack(spacing: 0) { + ForEach(sources, id: \.self) { item in + itemBuilder(item) + } + } + } + } +} diff --git a/secantTests/AddressDetailsTests/AddressDetailsTests.swift b/secantTests/AddressDetailsTests/AddressDetailsTests.swift index 12fcfbda..2f268e4c 100644 --- a/secantTests/AddressDetailsTests/AddressDetailsTests.swift +++ b/secantTests/AddressDetailsTests/AddressDetailsTests.swift @@ -22,9 +22,9 @@ class AddressDetailsTests: XCTestCase { let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) let store = TestStore( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() } store.dependencies.pasteboard = testPasteboard @@ -47,9 +47,9 @@ class AddressDetailsTests: XCTestCase { let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) let store = TestStore( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() } store.dependencies.pasteboard = testPasteboard @@ -70,9 +70,9 @@ class AddressDetailsTests: XCTestCase { let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) let store = TestStore( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() } store.dependencies.pasteboard = testPasteboard @@ -94,9 +94,9 @@ class AddressDetailsTests: XCTestCase { let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) let store = TestStore( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() } let expectedAddress = try uAddress.transparentReceiver().stringEncoded @@ -112,9 +112,9 @@ class AddressDetailsTests: XCTestCase { let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) let store = TestStore( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() } await store.send(.shareQR(uAddress.stringEncoded.redacted)) { state in @@ -128,9 +128,9 @@ class AddressDetailsTests: XCTestCase { let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet) let store = TestStore( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() } let expectedAddress = try uAddress.saplingReceiver().stringEncoded diff --git a/secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift b/secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift index a888872c..c456a30d 100644 --- a/secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift +++ b/secantTests/SnapshotTests/AddressDetailsSnapshotTests/AddressDetailsSnapshotTests.swift @@ -23,9 +23,9 @@ class AddressDetailsSnapshotTests: XCTestCase { let networkType = NetworkType.testnet let store = Store( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() .dependency(\.walletConfigProvider, .noOp) } @@ -42,9 +42,9 @@ class AddressDetailsSnapshotTests: XCTestCase { let networkType = NetworkType.mainnet let store = Store( - initialState: AddressDetailsReducer.State(uAddress: uAddress) + initialState: AddressDetails.State(uAddress: uAddress) ) { - AddressDetailsReducer() + AddressDetails() .dependency(\.walletConfigProvider, .noOp) }