Merge pull request #1248 from LukasKorba/receive-tab-tweaks
[#1230] Improve Receive screen UI by adding a toggle for addresses
This commit is contained in:
commit
98f79f17f3
|
@ -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)
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<Bool, Never>
|
||||
public var updateValue: @Sendable (Bool) -> Void
|
||||
}
|
|
@ -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<Bool, Never>(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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
)
|
||||
}
|
|
@ -13,12 +13,17 @@ import Pasteboard
|
|||
import Generated
|
||||
import Utils
|
||||
|
||||
public typealias AddressDetailsStore = Store<AddressDetailsReducer.State, AddressDetailsReducer.Action>
|
||||
public typealias AddressDetailsViewStore = ViewStore<AddressDetailsReducer.State, AddressDetailsReducer.Action>
|
||||
|
||||
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<AddressDetails.State>)
|
||||
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<Action> {
|
||||
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<State, Action> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,62 +22,64 @@ public struct AddressDetailsView: View {
|
|||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
let store: AddressDetailsStore
|
||||
@Perception.Bindable var store: StoreOf<AddressDetails>
|
||||
let networkType: NetworkType
|
||||
|
||||
public init(store: AddressDetailsStore, networkType: NetworkType) {
|
||||
public init(store: StoreOf<AddressDetails>, 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<AddressDetails>(
|
||||
initialState: .initial
|
||||
) {
|
||||
AddressDetails()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -23,6 +23,7 @@ import BackgroundTasks
|
|||
import RestoreWalletStorage
|
||||
import Utils
|
||||
import UserDefaults
|
||||
import HideBalances
|
||||
|
||||
public typealias RootStore = Store<RootReducer.State, RootReducer.Action>
|
||||
public typealias RootViewStore = ViewStore<RootReducer.State, RootReducer.Action>
|
||||
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -48,7 +48,8 @@ public struct AvailableBalanceView: View {
|
|||
fontName: FontFamily.Inter.bold.name,
|
||||
mostSignificantFontSize: 14,
|
||||
leastSignificantFontSize: 7,
|
||||
format: .expanded
|
||||
format: .expanded,
|
||||
couldBeHidden: true
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,8 @@ public struct BalanceWithIconView: View {
|
|||
fontName: FontFamily.Archivo.semiBold.name,
|
||||
mostSignificantFontSize: 42,
|
||||
leastSignificantFontSize: 10,
|
||||
format: .expanded
|
||||
format: .expanded,
|
||||
couldBeHidden: true
|
||||
)
|
||||
|
||||
Circle()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// ZashiPicker.swift
|
||||
//
|
||||
//
|
||||
// Created by Lukáš Korba on 03.05.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct ZashiPicker<Data, Content> : 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue