diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 93cac20..a0f6c66 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -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 = ""; }; 9E66129A28884BFB00C75B70 /* LocalAuthenticationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationHandler.swift; sourceTree = ""; }; 9E66129D288938A300C75B70 /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; + 9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldTests.swift; sourceTree = ""; }; 9E69A24C27FB002800A55317 /* WelcomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStore.swift; sourceTree = ""; }; 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = ""; }; + 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLineTextField.swift; sourceTree = ""; }; + 9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldStore.swift; sourceTree = ""; }; 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsSnapshotTests.swift; sourceTree = ""; }; 9E7CB6142869E8C300A02233 /* CircularProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = ""; }; 9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = ""; }; @@ -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 = ""; }; + 9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */ = { + isa = PBXGroup; + children = ( + 9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */, + ); + path = MultiLineTextFieldTests; + sourceTree = ""; + }; 9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */ = { isa = PBXGroup; children = ( @@ -923,6 +939,15 @@ path = SettingsSnapshotTests; sourceTree = ""; }; + 9E7225F4288AC6F300DF7F17 /* MultiLineTextField */ = { + isa = PBXGroup; + children = ( + 9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */, + 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */, + ); + path = MultiLineTextField; + sourceTree = ""; + }; 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 */, diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6f16ae6..255e8f4 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -204,7 +204,7 @@ "location" : "https://github.com/zcash/ZcashLightClientKit", "state" : { "branch" : "master", - "revision" : "5c1e283837df46d734101885010185d4e093337c" + "revision" : "fba4cecbe61cce424ada9fe1f98b05b88d5c8920" } } ], diff --git a/secant/Dependencies/ZCashSDKEnvironment.swift b/secant/Dependencies/ZCashSDKEnvironment.swift index 82bc17f..ae417f4 100644 --- a/secant/Dependencies/ZCashSDKEnvironment.swift +++ b/secant/Dependencies/ZCashSDKEnvironment.swift @@ -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, diff --git a/secant/Features/App/AppStore.swift b/secant/Features/App/AppStore.swift index 1d7d912..18c4f05 100644 --- a/secant/Features/App/AppStore.swift +++ b/secant/Features/App/AppStore.swift @@ -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))): diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index 969c93d..8bc676d 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -240,7 +240,8 @@ extension HomeReducer { numberFormatter: .live(), SDKSynchronizer: environment.SDKSynchronizer, scheduler: environment.scheduler, - walletStorage: environment.walletStorage + walletStorage: environment.walletStorage, + zcashSDKEnvironment: environment.zcashSDKEnvironment ) } ) diff --git a/secant/Features/Sandbox/SandboxView.swift b/secant/Features/Sandbox/SandboxView.swift index f5325d2..d8b0ae5 100644 --- a/secant/Features/Sandbox/SandboxView.swift +++ b/secant/Features/Sandbox/SandboxView.swift @@ -36,7 +36,8 @@ struct SandboxView: View { numberFormatter: .live(), SDKSynchronizer: LiveWrappedSDKSynchronizer(), scheduler: DispatchQueue.main.eraseToAnyScheduler(), - walletStorage: .live() + walletStorage: .live(), + zcashSDKEnvironment: .mainnet ) ) ) diff --git a/secant/Features/SendFlow/SendFlowStore.swift b/secant/Features/SendFlow/SendFlowStore.swift index e2e41c9..ab89ec6 100644 --- a/secant/Features/SendFlow/SendFlowStore.swift +++ b/secant/Features/SendFlow/SendFlowStore.swift @@ -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 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 { - 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 ) ) } diff --git a/secant/Features/SendFlow/SendFlowView.swift b/secant/Features/SendFlow/SendFlowView.swift index 74bed9f..b939135 100644 --- a/secant/Features/SendFlow/SendFlowView.swift +++ b/secant/Features/SendFlow/SendFlowView.swift @@ -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 ) ) ) diff --git a/secant/Features/SendFlow/Views/CreateTransactionView.swift b/secant/Features/SendFlow/Views/CreateTransactionView.swift index b8cc4dc..c20abcf 100644 --- a/secant/Features/SendFlow/Views/CreateTransactionView.swift +++ b/secant/Features/SendFlow/Views/CreateTransactionView.swift @@ -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( diff --git a/secant/Features/SendFlow/Views/TransactionSentView.swift b/secant/Features/SendFlow/Views/TransactionSentView.swift index 22f8129..165247d 100644 --- a/secant/Features/SendFlow/Views/TransactionSentView.swift +++ b/secant/Features/SendFlow/Views/TransactionSentView.swift @@ -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() } diff --git a/secant/Resources/Colors.xcassets/Text/InvalidEntry.colorset/Contents.json b/secant/Resources/Colors.xcassets/Text/InvalidEntry.colorset/Contents.json new file mode 100644 index 0000000..efd1acd --- /dev/null +++ b/secant/Resources/Colors.xcassets/Text/InvalidEntry.colorset/Contents.json @@ -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 + } +} diff --git a/secant/Resources/Colors.xcassets/TextField/MultilineOutline.colorset/Contents.json b/secant/Resources/Colors.xcassets/TextField/MultilineOutline.colorset/Contents.json new file mode 100644 index 0000000..4db274a --- /dev/null +++ b/secant/Resources/Colors.xcassets/TextField/MultilineOutline.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" : "0xA0", + "green" : "0x81", + "red" : "0x6E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/secant/Resources/Generated/XCAssets+Generated.swift b/secant/Resources/Generated/XCAssets+Generated.swift index 119ec1f..05c34c4 100644 --- a/secant/Resources/Generated/XCAssets+Generated.swift +++ b/secant/Resources/Generated/XCAssets+Generated.swift @@ -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 { diff --git a/secant/UI Components/TextFields/MultiLineTextField/MultiLineTextFieldStore.swift b/secant/UI Components/TextFields/MultiLineTextField/MultiLineTextFieldStore.swift new file mode 100644 index 0000000..e7c26b8 --- /dev/null +++ b/secant/UI Components/TextFields/MultiLineTextField/MultiLineTextFieldStore.swift @@ -0,0 +1,90 @@ +// +// MultiLineTextFieldStore.swift +// secant-testnet +// +// Created by Lukáš Korba on 22.07.2022. +// + +import Foundation +import ComposableArchitecture + +typealias MultiLineTextFieldReducer = Reducer +typealias MultiLineTextFieldStore = Store +typealias MultiLineTextFieldViewStore = ViewStore + +// 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) +} + +// 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() +} diff --git a/secant/UI Components/TextFields/MultiLineTextField/MultipleLineTextField.swift b/secant/UI Components/TextFields/MultiLineTextField/MultipleLineTextField.swift new file mode 100644 index 0000000..382c2b3 --- /dev/null +++ b/secant/UI Components/TextFields/MultiLineTextField/MultipleLineTextField.swift @@ -0,0 +1,98 @@ +// +// MultipleLineTextField.swift +// secant-testnet +// +// Created by Lukáš Korba on 22.07.2022. +// + +import SwiftUI +import ComposableArchitecture + +struct MultipleLineTextField: 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) + } +} diff --git a/secantTests/DeeplinkTests/DeeplinkTests.swift b/secantTests/DeeplinkTests/DeeplinkTests.swift index 2ee030d..02ce93b 100644 --- a/secantTests/DeeplinkTests/DeeplinkTests.swift +++ b/secantTests/DeeplinkTests/DeeplinkTests.swift @@ -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 } } } diff --git a/secantTests/MultiLineTextFieldTests/MultiLineTextFieldTests.swift b/secantTests/MultiLineTextFieldTests/MultiLineTextFieldTests.swift new file mode 100644 index 0000000..f3115ce --- /dev/null +++ b/secantTests/MultiLineTextFieldTests/MultiLineTextFieldTests.swift @@ -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)" + ) + } + } +} diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index 722b623..68bd871 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -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 {