[146] [UI Component] multiple line textfield (#400)

- MultiLineTextField Store & View implemented
- memo char limit is set from the zcash sdk environment
- fixed tests
- unit tests for the multiline textfield
- send flow form validity unit tests extended to cover memo char limit
This commit is contained in:
Lukas Korba 2022-08-02 08:07:03 +02:00 committed by GitHub
parent 0494af83eb
commit 4f029e0ba4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 634 additions and 43 deletions

View File

@ -127,8 +127,11 @@
9E6612362878345000C75B70 /* endlessCircleProgress.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E6612352878345000C75B70 /* endlessCircleProgress.json */; };
9E66129B28884BFB00C75B70 /* LocalAuthenticationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66129A28884BFB00C75B70 /* LocalAuthenticationHandler.swift */; };
9E66129E288938A300C75B70 /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66129D288938A300C75B70 /* SettingsTests.swift */; };
9E6713F12897F81B00A6796F /* MultiLineTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */; };
9E69A24D27FB002800A55317 /* WelcomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* WelcomeStore.swift */; };
9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */; };
9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */; };
9E7225F6288AC71A00DF7F17 /* MultiLineTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */; };
9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */; };
9E7CB6152869E8C300A02233 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6142869E8C300A02233 /* CircularProgress.swift */; };
9E7CB6182872D3DF00A02233 /* URLRouting in Frameworks */ = {isa = PBXBuildFile; productRef = 9E7CB6172872D3DF00A02233 /* URLRouting */; };
@ -351,8 +354,11 @@
9E6612352878345000C75B70 /* endlessCircleProgress.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = endlessCircleProgress.json; sourceTree = "<group>"; };
9E66129A28884BFB00C75B70 /* LocalAuthenticationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationHandler.swift; sourceTree = "<group>"; };
9E66129D288938A300C75B70 /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = "<group>"; };
9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldTests.swift; sourceTree = "<group>"; };
9E69A24C27FB002800A55317 /* WelcomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStore.swift; sourceTree = "<group>"; };
9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = "<group>"; };
9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLineTextField.swift; sourceTree = "<group>"; };
9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldStore.swift; sourceTree = "<group>"; };
9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsSnapshotTests.swift; sourceTree = "<group>"; };
9E7CB6142869E8C300A02233 /* CircularProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = "<group>"; };
9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
@ -549,6 +555,7 @@
isa = PBXGroup;
children = (
9E391162284E3ECF0073DD9A /* SnapshotTests */,
9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */,
9E7CB6222874245400A02233 /* ProfileTests */,
9EAB4674285B5C68002904A0 /* DeeplinkTests */,
9E3911372848AD3A0073DD9A /* HomeTests */,
@ -671,6 +678,7 @@
2E35F99027B28E6800EB79CD /* TextFields */ = {
isa = PBXGroup;
children = (
9E7225F4288AC6F300DF7F17 /* MultiLineTextField */,
9E7FE0F0282E80C100C374E8 /* TCATextField */,
2E35F99127B28E7600EB79CD /* SingleLineTextField.swift */,
9E5BF64C2823E84300BA3F17 /* TransactionAddress */,
@ -915,6 +923,14 @@
path = SettingsTests;
sourceTree = "<group>";
};
9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */ = {
isa = PBXGroup;
children = (
9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */,
);
path = MultiLineTextFieldTests;
sourceTree = "<group>";
};
9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */ = {
isa = PBXGroup;
children = (
@ -923,6 +939,15 @@
path = SettingsSnapshotTests;
sourceTree = "<group>";
};
9E7225F4288AC6F300DF7F17 /* MultiLineTextField */ = {
isa = PBXGroup;
children = (
9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */,
9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */,
);
path = MultiLineTextField;
sourceTree = "<group>";
};
9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */ = {
isa = PBXGroup;
children = (
@ -1568,6 +1593,7 @@
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */,
9E3911482848EEB90073DD9A /* RecoveryPhraseRandomizer.swift in Sources */,
0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */,
9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */,
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */,
9E2F1C8F280EDE09004E65FE /* Drawer.swift in Sources */,
665C963F272C26E600BC04FB /* CircularFrameBackground.swift in Sources */,
@ -1586,6 +1612,7 @@
9E87ADF128363DE400122FCC /* WrappedAudioServices.swift in Sources */,
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */,
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */,
9E7225F6288AC71A00DF7F17 /* MultiLineTextFieldStore.swift in Sources */,
2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */,
9E6612332878338C00C75B70 /* LottieAnimation.swift in Sources */,
0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */,
@ -1660,6 +1687,7 @@
9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */,
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */,
9E6713F12897F81B00A6796F /* MultiLineTextFieldTests.swift in Sources */,
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */,
9E66129E288938A300C75B70 /* SettingsTests.swift in Sources */,
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */,

View File

@ -204,7 +204,7 @@
"location" : "https://github.com/zcash/ZcashLightClientKit",
"state" : {
"branch" : "master",
"revision" : "5c1e283837df46d734101885010185d4e093337c"
"revision" : "fba4cecbe61cce424ada9fe1f98b05b88d5c8920"
}
}
],

View File

@ -23,6 +23,7 @@ struct ZCashSDKEnvironment {
let endpoint: LightWalletEndpoint
let isMainnet: () -> Bool
let lightWalletService: LightWalletService
let memoCharLimit: Int
let mnemonicWordsMaxCount: Int
let network: ZcashNetwork
let requiredTransactionConfirmations: Int
@ -37,6 +38,7 @@ extension ZCashSDKEnvironment {
lightWalletService: LightWalletGRPCService(
endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointMainnetAddress, port: ZcashSDKConstants.endpointPort)
),
memoCharLimit: 512,
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
network: ZcashNetworkBuilder.network(for: .mainnet),
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
@ -50,6 +52,7 @@ extension ZCashSDKEnvironment {
lightWalletService: LightWalletGRPCService(
endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointTestnetAddress, port: ZcashSDKConstants.endpointPort)
),
memoCharLimit: 512,
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
network: ZcashNetworkBuilder.network(for: .testnet),
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,

View File

@ -339,7 +339,7 @@ extension AppReducer {
state.homeState.route = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address
state.homeState.sendState.memo = memo
state.homeState.sendState.memoState.text = memo
return .none
case .home(.walletEvents(.replyTo(let address))):

View File

@ -240,7 +240,8 @@ extension HomeReducer {
numberFormatter: .live(),
SDKSynchronizer: environment.SDKSynchronizer,
scheduler: environment.scheduler,
walletStorage: environment.walletStorage
walletStorage: environment.walletStorage,
zcashSDKEnvironment: environment.zcashSDKEnvironment
)
}
)

View File

@ -36,7 +36,8 @@ struct SandboxView: View {
numberFormatter: .live(),
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
walletStorage: .live()
walletStorage: .live(),
zcashSDKEnvironment: .mainnet
)
)
)

View File

@ -24,7 +24,7 @@ struct SendFlowState: Equatable {
}
var isSendingTransaction = false
var memo = ""
var memoState: MultiLineTextFieldState
var route: Route?
var totalBalance = Zatoshi.zero
var transactionAddressInputState: TransactionAddressTextFieldState
@ -59,6 +59,7 @@ struct SendFlowState: Equatable {
transactionAmountInputState.amount > 0
&& transactionAddressInputState.isValidAddress
&& !isInsufficientFunds
&& memoState.isValid
}
var isInsufficientFunds: Bool {
@ -73,6 +74,7 @@ struct SendFlowState: Equatable {
// MARK: - Action
enum SendFlowAction: Equatable {
case memo(MultiLineTextFieldAction)
case onAppear
case onDisappear
case sendConfirmationPressed
@ -81,8 +83,6 @@ enum SendFlowAction: Equatable {
case transactionAddressInput(TransactionAddressTextFieldAction)
case transactionAmountInput(TransactionAmountTextFieldAction)
case updateBalance(Zatoshi)
case updateMemo(String)
// case updateTransaction(SendFlowTransaction)
case updateRoute(SendFlowState.Route?)
}
@ -95,6 +95,7 @@ struct SendFlowEnvironment {
let SDKSynchronizer: WrappedSDKSynchronizer
let scheduler: AnySchedulerOf<DispatchQueue>
let walletStorage: WrappedWalletStorage
let zcashSDKEnvironment: ZCashSDKEnvironment
}
// MARK: - Reducer
@ -106,7 +107,8 @@ extension SendFlowReducer {
[
sendReducer,
transactionAddressInputReducer,
transactionAmountInputReducer
transactionAmountInputReducer,
memoReducer
]
)
@ -145,7 +147,7 @@ extension SendFlowReducer {
with: spendingKey,
zatoshi: state.amount,
to: state.address,
memo: state.memo,
memo: state.memoState.text,
from: 0
)
.receive(on: environment.scheduler)
@ -171,6 +173,7 @@ extension SendFlowReducer {
return .none
case .onAppear:
state.memoState.charLimit = environment.zcashSDKEnvironment.memoCharLimit
return environment.SDKSynchronizer.stateChanged
.map(SendFlowAction.synchronizerStateChanged)
.eraseToEffect()
@ -194,8 +197,7 @@ extension SendFlowReducer {
state.transactionAmountInputState.maxValue = balance.amount
return .none
case .updateMemo(let memo):
state.memo = memo
case .memo:
return .none
}
}
@ -219,7 +221,13 @@ extension SendFlowReducer {
)
}
)
private static let memoReducer: SendFlowReducer = MultiLineTextFieldReducer.default.pullback(
state: \SendFlowState.memoState,
action: /SendFlowAction.memo,
environment: { _ in MultiLineTextFieldEnvironment() }
)
static func `default`(whenDone: @escaping () -> Void) -> SendFlowReducer {
SendFlowReducer { state, action, environment in
switch action {
@ -232,6 +240,17 @@ extension SendFlowReducer {
}
}
// MARK: - Store
extension SendFlowStore {
func memoStore() -> MultiLineTextFieldStore {
self.scope(
state: \.memoState,
action: SendFlowAction.memo
)
}
}
// MARK: - ViewStore
extension SendFlowViewStore {
@ -269,13 +288,6 @@ extension SendFlowViewStore {
embed: { $0 ? SendFlowState.Route.done : SendFlowState.Route.confirmation }
)
}
var bindingForMemo: Binding<String> {
self.binding(
get: \.memo,
send: SendFlowAction.updateMemo
)
}
}
// MARK: Placeholders
@ -283,6 +295,7 @@ extension SendFlowViewStore {
extension SendFlowState {
static var placeholder: Self {
.init(
memoState: .placeholder,
route: nil,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .amount
@ -291,6 +304,7 @@ extension SendFlowState {
static var emptyPlaceholder: Self {
.init(
memoState: .placeholder,
route: nil,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
@ -303,6 +317,7 @@ extension SendFlowStore {
static var placeholder: SendFlowStore {
return SendFlowStore(
initialState: .init(
memoState: .placeholder,
route: nil,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
@ -314,7 +329,8 @@ extension SendFlowStore {
numberFormatter: .live(),
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
walletStorage: .live()
walletStorage: .live(),
zcashSDKEnvironment: .mainnet
)
)
}

View File

@ -34,6 +34,7 @@ struct SendFLowView_Previews: PreviewProvider {
SendFlowView(
store: .init(
initialState: .init(
memoState: .placeholder,
route: nil,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
@ -45,7 +46,8 @@ struct SendFLowView_Previews: PreviewProvider {
numberFormatter: .live(),
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
walletStorage: .live()
walletStorage: .live(),
zcashSDKEnvironment: .mainnet
)
)
)

View File

@ -62,13 +62,12 @@ struct CreateTransaction: View {
}
.padding()
VStack {
Text("Memo")
TextEditor(text: viewStore.bindingForMemo)
.frame(maxWidth: .infinity, maxHeight: 150, alignment: .center)
.importSeedEditorModifier(Asset.Colors.Text.activeButtonText.color)
}
MultipleLineTextField(
store: store.memoStore(),
title: "Memo",
titleAccessoryView: {}
)
.frame(height: 200)
.padding()
Button(

View File

@ -20,7 +20,7 @@ struct TransactionSent: View {
Text("amount: \(viewStore.amount.decimalString())")
+ Text(" address: \(viewStore.address)")
+ Text(" memo: \(viewStore.memo)")
+ Text(" memo: \(viewStore.memoState.text)")
Spacer()
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2E",
"green" : "0x2A",
"red" : "0xA7"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2E",
"green" : "0x2A",
"red" : "0xA7"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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" : "0xA0",
"green" : "0x81",
"red" : "0x6E"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -110,6 +110,7 @@ internal enum Asset {
internal static let drawerTabsText = ColorAsset(name: "DrawerTabsText")
internal static let heading = ColorAsset(name: "Heading")
internal static let importSeedEditor = ColorAsset(name: "ImportSeedEditor")
internal static let invalidEntry = ColorAsset(name: "InvalidEntry")
internal static let medium = ColorAsset(name: "Medium")
internal static let regular = ColorAsset(name: "Regular")
internal static let secondaryButtonText = ColorAsset(name: "SecondaryButtonText")
@ -124,6 +125,7 @@ internal enum Asset {
internal static let moreInfoText = ColorAsset(name: "moreInfoText")
}
internal enum TextField {
internal static let multilineOutline = ColorAsset(name: "MultilineOutline")
internal static let titleAccessoryButton = ColorAsset(name: "TitleAccessoryButton")
internal static let titleAccessoryButtonPressed = ColorAsset(name: "TitleAccessoryButtonPressed")
internal enum Underline {

View File

@ -0,0 +1,90 @@
//
// MultiLineTextFieldStore.swift
// secant-testnet
//
// Created by Lukáš Korba on 22.07.2022.
//
import Foundation
import ComposableArchitecture
typealias MultiLineTextFieldReducer = Reducer<MultiLineTextFieldState, MultiLineTextFieldAction, MultiLineTextFieldEnvironment>
typealias MultiLineTextFieldStore = Store<MultiLineTextFieldState, MultiLineTextFieldAction>
typealias MultiLineTextFieldViewStore = ViewStore<MultiLineTextFieldState, MultiLineTextFieldAction>
// MARK: - State
struct MultiLineTextFieldState: Equatable {
/// default 0, no char limit
var charLimit = 0
@BindableState var text = ""
var isCharLimited: Bool {
charLimit > 0
}
var textLength: Int {
text.count
}
var isValid: Bool {
charLimit > 0
? textLength <= charLimit
: true
}
var charLimitText: String {
charLimit > 0
? isValid
? "\(textLength)/\(charLimit)"
: "char limit exceeded \(textLength)/\(charLimit)"
: ""
}
}
// MARK: - Action
enum MultiLineTextFieldAction: Equatable, BindableAction {
case binding(BindingAction<MultiLineTextFieldState>)
}
// MARK: - Environment
struct MultiLineTextFieldEnvironment { }
extension MultiLineTextFieldEnvironment {
static let live = MultiLineTextFieldEnvironment()
static let mock = MultiLineTextFieldEnvironment()
}
// MARK: - Reducer
extension MultiLineTextFieldReducer {
static let `default` = MultiLineTextFieldReducer { _, action, _ in
switch action {
case .binding(\.$text):
return .none
case .binding:
return .none
}
}
.binding()
}
// MARK: - Store
extension MultiLineTextFieldStore {
static let placeholder = MultiLineTextFieldStore(
initialState: .placeholder,
reducer: .default,
environment: MultiLineTextFieldEnvironment()
)
}
// MARK: - Placeholders
extension MultiLineTextFieldState {
static let placeholder = MultiLineTextFieldState()
}

View File

@ -0,0 +1,98 @@
//
// MultipleLineTextField.swift
// secant-testnet
//
// Created by Lukáš Korba on 22.07.2022.
//
import SwiftUI
import ComposableArchitecture
struct MultipleLineTextField<TitleAccessoryContent>: View
where TitleAccessoryContent: View {
let store: MultiLineTextFieldStore
let title: String
@ViewBuilder let titleAccessoryView: TitleAccessoryContent
var body: some View {
WithViewStore(store) { viewStore in
VStack {
HStack {
Text(title)
.font(.custom(FontFamily.Rubik.regular.name, size: 13))
Spacer()
titleAccessoryView
}
TextEditor(text: viewStore.binding(\.$text))
.multilineTextEditorModifier(
Asset.Colors.Text.activeButtonText.color,
Asset.Colors.TextField.multilineOutline.color
)
if viewStore.isCharLimited {
HStack {
Spacer()
Text(viewStore.charLimitText)
.font(.custom(FontFamily.Rubik.regular.name, size: 14))
.foregroundColor(
viewStore.isValid
? Asset.Colors.TextField.multilineOutline.color
: Asset.Colors.Text.invalidEntry.color
)
}
}
}
}
.onAppear(perform: { UITextView.appearance().backgroundColor = .clear })
}
}
struct MultilineTextEditorModifier: ViewModifier {
var backgroundColor = Color.white
var outlineColor = Color.black
func body(content: Content) -> some View {
content
.foregroundColor(Asset.Colors.Text.importSeedEditor.color)
.padding()
.background(backgroundColor)
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(outlineColor, lineWidth: 2)
)
}
}
extension View {
func multilineTextEditorModifier(
_ backgroundColor: Color = .white,
_ outlineColor: Color = .black
) -> some View {
modifier(
MultilineTextEditorModifier(
backgroundColor: backgroundColor,
outlineColor: outlineColor
)
)
}
}
struct MultipleLineTextField_Previews: PreviewProvider {
static var previews: some View {
MultipleLineTextField(
store: .placeholder,
title: "Memo",
titleAccessoryView: {
Text("accessory")
.font(.custom(FontFamily.Rubik.regular.name, size: 13))
}
)
.frame(height: 200)
.padding()
.applyScreenBackground()
.preferredColorScheme(.dark)
}
}

View File

@ -69,7 +69,7 @@ class DeeplinkTests: XCTestCase {
state.homeState.route = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address
state.homeState.sendState.memo = memo
state.homeState.sendState.memoState.text = memo
}
}
@ -159,7 +159,7 @@ class DeeplinkTests: XCTestCase {
state.homeState.route = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address
state.homeState.sendState.memo = memo
state.homeState.sendState.memoState.text = memo
}
}
@ -208,7 +208,7 @@ class DeeplinkTests: XCTestCase {
state.homeState.route = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address
state.homeState.sendState.memo = memo
state.homeState.sendState.memoState.text = memo
}
}
}

View File

@ -0,0 +1,160 @@
//
// MultiLineTextFieldTests.swift
// secantTests
//
// Created by Lukáš Korba on 01.08.2022.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
class MultiLineTextFieldTests: XCTestCase {
func testIsCharLimited() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(charLimit: 1),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertTrue(
state.isCharLimited,
"Multiline TextFiler tests: `testIsCharLimited` is expected to be true but it is \(state.isCharLimited)"
)
}
}
func testIsNotCharLimited() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertFalse(
state.isCharLimited,
"Multiline TextFiler tests: `testIsNotCharLimited` is expected to be false but it is \(state.isCharLimited)"
)
}
}
func testTextLength() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertEqual(
4,
state.textLength,
"Multiline TextFiler tests: `testTextLength` is expected to be 4 but it is \(state.textLength)"
)
}
}
func testIsValid_CharLimit() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(charLimit: 4),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertTrue(
state.isValid,
"Multiline TextFiler tests: `testIsValid_CharLimit` is expected to be true but it is \(state.isValid)"
)
}
}
func testIsValid_NoCharLimit() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertTrue(
state.isValid,
"Multiline TextFiler tests: `testIsValid_NoCharLimit` is expected to be true but it is \(state.isValid)"
)
}
}
func testIsInvalid() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(charLimit: 3),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertFalse(
state.isValid,
"Multiline TextFiler tests: `testIsInvalid` is expected to be false but it is \(state.isValid)"
)
}
}
func testCharLimitText_NoCharLimit() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertEqual(
"",
state.charLimitText,
"Multiline TextFiler tests: `testCharLimitText_NoCharLimit` is expected to be \"\" but it is \(state.charLimitText)"
)
}
}
func testCharLimitText_CharLimit_LessCharacters() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(charLimit: 5),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertEqual(
"4/5",
state.charLimitText,
"Multiline TextFiler tests: `testCharLimitText_CharLimit_LessCharacters` is expected to be \"4/5\" but it is \(state.charLimitText)"
)
}
}
func testCharLimitText_CharLimit_Exceeded() throws {
let store = TestStore(
initialState: MultiLineTextFieldState(charLimit: 3),
reducer: MultiLineTextFieldReducer.default,
environment: MultiLineTextFieldEnvironment()
)
store.send(.binding(.set(\.$text, "test"))) { state in
state.text = "test"
XCTAssertEqual(
"char limit exceeded 4/3",
state.charLimitText,
"Multiline TextFiler tests: `testCharLimitText_CharLimit_Exceeded` is expected to be \"4/5\" but it is \(state.charLimitText)"
)
}
}
}

View File

@ -40,7 +40,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: MockWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: storage)
walletStorage: .live(walletStorage: storage),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -97,7 +98,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: storage)
walletStorage: .live(walletStorage: storage),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -137,7 +139,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -180,7 +183,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -205,7 +209,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -230,6 +235,7 @@ class SendTests: XCTestCase {
func testFundsSufficiency() throws {
let sendState = SendFlowState(
memoState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
@ -251,7 +257,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(numberFormatter: usNumberFormatter),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -300,11 +307,13 @@ class SendTests: XCTestCase {
numberFormatter: .live(numberFormatter: usNumberFormatter),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
initialState: .init(
memoState: .placeholder,
route: nil,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
@ -337,6 +346,7 @@ class SendTests: XCTestCase {
func testValidForm() throws {
let sendState = SendFlowState(
memoState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
@ -359,7 +369,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -383,6 +394,7 @@ class SendTests: XCTestCase {
func testInvalidForm_InsufficientFunds() throws {
let sendState = SendFlowState(
memoState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
@ -404,7 +416,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -428,6 +441,7 @@ class SendTests: XCTestCase {
func testInvalidForm_AddressFormat() throws {
let sendState = SendFlowState(
memoState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
@ -449,7 +463,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -473,6 +488,7 @@ class SendTests: XCTestCase {
func testInvalidForm_AmountFormat() throws {
let sendState = SendFlowState(
memoState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
@ -494,7 +510,8 @@ class SendTests: XCTestCase {
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
@ -515,6 +532,104 @@ class SendTests: XCTestCase {
)
}
}
func testInvalidForm_ExceededMemoCharLimit() throws {
let sendState = SendFlowState(
memoState: MultiLineTextFieldState(charLimit: 3),
totalBalance: Zatoshi(1),
transactionAddressInputState:
TransactionAddressTextFieldState(
isValidAddress: true,
textFieldState:
TCATextFieldState(
validationType: .none,
text: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
)
),
transactionAmountInputState:
TransactionAmountTextFieldState(
amount: 100,
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302,
textFieldState:
TCATextFieldState(
validationType: .floatingPoint,
text: "0.0.0501301"
)
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.memo(.binding(.set(\.$text, "test")))) { state in
state.memoState.text = "test"
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
func testMemoCharLimitSet() throws {
let sendState = SendFlowState(
memoState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302,
textFieldState:
TCATextFieldState(
validationType: .floatingPoint,
text: "0.0.0501301"
)
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
zcashSDKEnvironment: .testnet
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.onAppear) { state in
state.memoState.charLimit = 512
}
store.receive(.synchronizerStateChanged(.unknown))
// .onAppear action starts long living cancelable action .synchronizerStateChanged
// .onDisappear cancels it, must have for the test to pass
store.send(.onDisappear)
}
}
private extension SendTests {