[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:
parent
0494af83eb
commit
4f029e0ba4
|
@ -127,8 +127,11 @@
|
||||||
9E6612362878345000C75B70 /* endlessCircleProgress.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E6612352878345000C75B70 /* endlessCircleProgress.json */; };
|
9E6612362878345000C75B70 /* endlessCircleProgress.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E6612352878345000C75B70 /* endlessCircleProgress.json */; };
|
||||||
9E66129B28884BFB00C75B70 /* LocalAuthenticationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66129A28884BFB00C75B70 /* LocalAuthenticationHandler.swift */; };
|
9E66129B28884BFB00C75B70 /* LocalAuthenticationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66129A28884BFB00C75B70 /* LocalAuthenticationHandler.swift */; };
|
||||||
9E66129E288938A300C75B70 /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66129D288938A300C75B70 /* SettingsTests.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 */; };
|
9E69A24D27FB002800A55317 /* WelcomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* WelcomeStore.swift */; };
|
||||||
9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.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 */; };
|
9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */; };
|
||||||
9E7CB6152869E8C300A02233 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6142869E8C300A02233 /* CircularProgress.swift */; };
|
9E7CB6152869E8C300A02233 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6142869E8C300A02233 /* CircularProgress.swift */; };
|
||||||
9E7CB6182872D3DF00A02233 /* URLRouting in Frameworks */ = {isa = PBXBuildFile; productRef = 9E7CB6172872D3DF00A02233 /* URLRouting */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
|
||||||
|
@ -549,6 +555,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9E391162284E3ECF0073DD9A /* SnapshotTests */,
|
9E391162284E3ECF0073DD9A /* SnapshotTests */,
|
||||||
|
9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */,
|
||||||
9E7CB6222874245400A02233 /* ProfileTests */,
|
9E7CB6222874245400A02233 /* ProfileTests */,
|
||||||
9EAB4674285B5C68002904A0 /* DeeplinkTests */,
|
9EAB4674285B5C68002904A0 /* DeeplinkTests */,
|
||||||
9E3911372848AD3A0073DD9A /* HomeTests */,
|
9E3911372848AD3A0073DD9A /* HomeTests */,
|
||||||
|
@ -671,6 +678,7 @@
|
||||||
2E35F99027B28E6800EB79CD /* TextFields */ = {
|
2E35F99027B28E6800EB79CD /* TextFields */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
9E7225F4288AC6F300DF7F17 /* MultiLineTextField */,
|
||||||
9E7FE0F0282E80C100C374E8 /* TCATextField */,
|
9E7FE0F0282E80C100C374E8 /* TCATextField */,
|
||||||
2E35F99127B28E7600EB79CD /* SingleLineTextField.swift */,
|
2E35F99127B28E7600EB79CD /* SingleLineTextField.swift */,
|
||||||
9E5BF64C2823E84300BA3F17 /* TransactionAddress */,
|
9E5BF64C2823E84300BA3F17 /* TransactionAddress */,
|
||||||
|
@ -915,6 +923,14 @@
|
||||||
path = SettingsTests;
|
path = SettingsTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */,
|
||||||
|
);
|
||||||
|
path = MultiLineTextFieldTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */ = {
|
9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -923,6 +939,15 @@
|
||||||
path = SettingsSnapshotTests;
|
path = SettingsSnapshotTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
9E7225F4288AC6F300DF7F17 /* MultiLineTextField */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */,
|
||||||
|
9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */,
|
||||||
|
);
|
||||||
|
path = MultiLineTextField;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */ = {
|
9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1568,6 +1593,7 @@
|
||||||
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */,
|
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */,
|
||||||
9E3911482848EEB90073DD9A /* RecoveryPhraseRandomizer.swift in Sources */,
|
9E3911482848EEB90073DD9A /* RecoveryPhraseRandomizer.swift in Sources */,
|
||||||
0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */,
|
0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */,
|
||||||
|
9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */,
|
||||||
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */,
|
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */,
|
||||||
9E2F1C8F280EDE09004E65FE /* Drawer.swift in Sources */,
|
9E2F1C8F280EDE09004E65FE /* Drawer.swift in Sources */,
|
||||||
665C963F272C26E600BC04FB /* CircularFrameBackground.swift in Sources */,
|
665C963F272C26E600BC04FB /* CircularFrameBackground.swift in Sources */,
|
||||||
|
@ -1586,6 +1612,7 @@
|
||||||
9E87ADF128363DE400122FCC /* WrappedAudioServices.swift in Sources */,
|
9E87ADF128363DE400122FCC /* WrappedAudioServices.swift in Sources */,
|
||||||
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */,
|
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */,
|
||||||
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */,
|
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */,
|
||||||
|
9E7225F6288AC71A00DF7F17 /* MultiLineTextFieldStore.swift in Sources */,
|
||||||
2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */,
|
2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */,
|
||||||
9E6612332878338C00C75B70 /* LottieAnimation.swift in Sources */,
|
9E6612332878338C00C75B70 /* LottieAnimation.swift in Sources */,
|
||||||
0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */,
|
0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */,
|
||||||
|
@ -1660,6 +1687,7 @@
|
||||||
9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */,
|
9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */,
|
||||||
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
|
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
|
||||||
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */,
|
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */,
|
||||||
|
9E6713F12897F81B00A6796F /* MultiLineTextFieldTests.swift in Sources */,
|
||||||
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */,
|
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */,
|
||||||
9E66129E288938A300C75B70 /* SettingsTests.swift in Sources */,
|
9E66129E288938A300C75B70 /* SettingsTests.swift in Sources */,
|
||||||
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */,
|
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */,
|
||||||
|
|
|
@ -204,7 +204,7 @@
|
||||||
"location" : "https://github.com/zcash/ZcashLightClientKit",
|
"location" : "https://github.com/zcash/ZcashLightClientKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "5c1e283837df46d734101885010185d4e093337c"
|
"revision" : "fba4cecbe61cce424ada9fe1f98b05b88d5c8920"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -23,6 +23,7 @@ struct ZCashSDKEnvironment {
|
||||||
let endpoint: LightWalletEndpoint
|
let endpoint: LightWalletEndpoint
|
||||||
let isMainnet: () -> Bool
|
let isMainnet: () -> Bool
|
||||||
let lightWalletService: LightWalletService
|
let lightWalletService: LightWalletService
|
||||||
|
let memoCharLimit: Int
|
||||||
let mnemonicWordsMaxCount: Int
|
let mnemonicWordsMaxCount: Int
|
||||||
let network: ZcashNetwork
|
let network: ZcashNetwork
|
||||||
let requiredTransactionConfirmations: Int
|
let requiredTransactionConfirmations: Int
|
||||||
|
@ -37,6 +38,7 @@ extension ZCashSDKEnvironment {
|
||||||
lightWalletService: LightWalletGRPCService(
|
lightWalletService: LightWalletGRPCService(
|
||||||
endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointMainnetAddress, port: ZcashSDKConstants.endpointPort)
|
endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointMainnetAddress, port: ZcashSDKConstants.endpointPort)
|
||||||
),
|
),
|
||||||
|
memoCharLimit: 512,
|
||||||
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
|
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
|
||||||
network: ZcashNetworkBuilder.network(for: .mainnet),
|
network: ZcashNetworkBuilder.network(for: .mainnet),
|
||||||
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
|
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
|
||||||
|
@ -50,6 +52,7 @@ extension ZCashSDKEnvironment {
|
||||||
lightWalletService: LightWalletGRPCService(
|
lightWalletService: LightWalletGRPCService(
|
||||||
endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointTestnetAddress, port: ZcashSDKConstants.endpointPort)
|
endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointTestnetAddress, port: ZcashSDKConstants.endpointPort)
|
||||||
),
|
),
|
||||||
|
memoCharLimit: 512,
|
||||||
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
|
mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount,
|
||||||
network: ZcashNetworkBuilder.network(for: .testnet),
|
network: ZcashNetworkBuilder.network(for: .testnet),
|
||||||
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
|
requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations,
|
||||||
|
|
|
@ -339,7 +339,7 @@ extension AppReducer {
|
||||||
state.homeState.route = .send
|
state.homeState.route = .send
|
||||||
state.homeState.sendState.amount = amount
|
state.homeState.sendState.amount = amount
|
||||||
state.homeState.sendState.address = address
|
state.homeState.sendState.address = address
|
||||||
state.homeState.sendState.memo = memo
|
state.homeState.sendState.memoState.text = memo
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .home(.walletEvents(.replyTo(let address))):
|
case .home(.walletEvents(.replyTo(let address))):
|
||||||
|
|
|
@ -240,7 +240,8 @@ extension HomeReducer {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: environment.SDKSynchronizer,
|
SDKSynchronizer: environment.SDKSynchronizer,
|
||||||
scheduler: environment.scheduler,
|
scheduler: environment.scheduler,
|
||||||
walletStorage: environment.walletStorage
|
walletStorage: environment.walletStorage,
|
||||||
|
zcashSDKEnvironment: environment.zcashSDKEnvironment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,7 +36,8 @@ struct SandboxView: View {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||||
walletStorage: .live()
|
walletStorage: .live(),
|
||||||
|
zcashSDKEnvironment: .mainnet
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,7 +24,7 @@ struct SendFlowState: Equatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSendingTransaction = false
|
var isSendingTransaction = false
|
||||||
var memo = ""
|
var memoState: MultiLineTextFieldState
|
||||||
var route: Route?
|
var route: Route?
|
||||||
var totalBalance = Zatoshi.zero
|
var totalBalance = Zatoshi.zero
|
||||||
var transactionAddressInputState: TransactionAddressTextFieldState
|
var transactionAddressInputState: TransactionAddressTextFieldState
|
||||||
|
@ -59,6 +59,7 @@ struct SendFlowState: Equatable {
|
||||||
transactionAmountInputState.amount > 0
|
transactionAmountInputState.amount > 0
|
||||||
&& transactionAddressInputState.isValidAddress
|
&& transactionAddressInputState.isValidAddress
|
||||||
&& !isInsufficientFunds
|
&& !isInsufficientFunds
|
||||||
|
&& memoState.isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
var isInsufficientFunds: Bool {
|
var isInsufficientFunds: Bool {
|
||||||
|
@ -73,6 +74,7 @@ struct SendFlowState: Equatable {
|
||||||
// MARK: - Action
|
// MARK: - Action
|
||||||
|
|
||||||
enum SendFlowAction: Equatable {
|
enum SendFlowAction: Equatable {
|
||||||
|
case memo(MultiLineTextFieldAction)
|
||||||
case onAppear
|
case onAppear
|
||||||
case onDisappear
|
case onDisappear
|
||||||
case sendConfirmationPressed
|
case sendConfirmationPressed
|
||||||
|
@ -81,8 +83,6 @@ enum SendFlowAction: Equatable {
|
||||||
case transactionAddressInput(TransactionAddressTextFieldAction)
|
case transactionAddressInput(TransactionAddressTextFieldAction)
|
||||||
case transactionAmountInput(TransactionAmountTextFieldAction)
|
case transactionAmountInput(TransactionAmountTextFieldAction)
|
||||||
case updateBalance(Zatoshi)
|
case updateBalance(Zatoshi)
|
||||||
case updateMemo(String)
|
|
||||||
// case updateTransaction(SendFlowTransaction)
|
|
||||||
case updateRoute(SendFlowState.Route?)
|
case updateRoute(SendFlowState.Route?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ struct SendFlowEnvironment {
|
||||||
let SDKSynchronizer: WrappedSDKSynchronizer
|
let SDKSynchronizer: WrappedSDKSynchronizer
|
||||||
let scheduler: AnySchedulerOf<DispatchQueue>
|
let scheduler: AnySchedulerOf<DispatchQueue>
|
||||||
let walletStorage: WrappedWalletStorage
|
let walletStorage: WrappedWalletStorage
|
||||||
|
let zcashSDKEnvironment: ZCashSDKEnvironment
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Reducer
|
// MARK: - Reducer
|
||||||
|
@ -106,7 +107,8 @@ extension SendFlowReducer {
|
||||||
[
|
[
|
||||||
sendReducer,
|
sendReducer,
|
||||||
transactionAddressInputReducer,
|
transactionAddressInputReducer,
|
||||||
transactionAmountInputReducer
|
transactionAmountInputReducer,
|
||||||
|
memoReducer
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -145,7 +147,7 @@ extension SendFlowReducer {
|
||||||
with: spendingKey,
|
with: spendingKey,
|
||||||
zatoshi: state.amount,
|
zatoshi: state.amount,
|
||||||
to: state.address,
|
to: state.address,
|
||||||
memo: state.memo,
|
memo: state.memoState.text,
|
||||||
from: 0
|
from: 0
|
||||||
)
|
)
|
||||||
.receive(on: environment.scheduler)
|
.receive(on: environment.scheduler)
|
||||||
|
@ -171,6 +173,7 @@ extension SendFlowReducer {
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
|
state.memoState.charLimit = environment.zcashSDKEnvironment.memoCharLimit
|
||||||
return environment.SDKSynchronizer.stateChanged
|
return environment.SDKSynchronizer.stateChanged
|
||||||
.map(SendFlowAction.synchronizerStateChanged)
|
.map(SendFlowAction.synchronizerStateChanged)
|
||||||
.eraseToEffect()
|
.eraseToEffect()
|
||||||
|
@ -194,8 +197,7 @@ extension SendFlowReducer {
|
||||||
state.transactionAmountInputState.maxValue = balance.amount
|
state.transactionAmountInputState.maxValue = balance.amount
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .updateMemo(let memo):
|
case .memo:
|
||||||
state.memo = memo
|
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,6 +222,12 @@ 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 {
|
static func `default`(whenDone: @escaping () -> Void) -> SendFlowReducer {
|
||||||
SendFlowReducer { state, action, environment in
|
SendFlowReducer { state, action, environment in
|
||||||
switch action {
|
switch action {
|
||||||
|
@ -232,6 +240,17 @@ extension SendFlowReducer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Store
|
||||||
|
|
||||||
|
extension SendFlowStore {
|
||||||
|
func memoStore() -> MultiLineTextFieldStore {
|
||||||
|
self.scope(
|
||||||
|
state: \.memoState,
|
||||||
|
action: SendFlowAction.memo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - ViewStore
|
// MARK: - ViewStore
|
||||||
|
|
||||||
extension SendFlowViewStore {
|
extension SendFlowViewStore {
|
||||||
|
@ -269,13 +288,6 @@ extension SendFlowViewStore {
|
||||||
embed: { $0 ? SendFlowState.Route.done : SendFlowState.Route.confirmation }
|
embed: { $0 ? SendFlowState.Route.done : SendFlowState.Route.confirmation }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var bindingForMemo: Binding<String> {
|
|
||||||
self.binding(
|
|
||||||
get: \.memo,
|
|
||||||
send: SendFlowAction.updateMemo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Placeholders
|
// MARK: Placeholders
|
||||||
|
@ -283,6 +295,7 @@ extension SendFlowViewStore {
|
||||||
extension SendFlowState {
|
extension SendFlowState {
|
||||||
static var placeholder: Self {
|
static var placeholder: Self {
|
||||||
.init(
|
.init(
|
||||||
|
memoState: .placeholder,
|
||||||
route: nil,
|
route: nil,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState: .amount
|
transactionAmountInputState: .amount
|
||||||
|
@ -291,6 +304,7 @@ extension SendFlowState {
|
||||||
|
|
||||||
static var emptyPlaceholder: Self {
|
static var emptyPlaceholder: Self {
|
||||||
.init(
|
.init(
|
||||||
|
memoState: .placeholder,
|
||||||
route: nil,
|
route: nil,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState: .placeholder
|
transactionAmountInputState: .placeholder
|
||||||
|
@ -303,6 +317,7 @@ extension SendFlowStore {
|
||||||
static var placeholder: SendFlowStore {
|
static var placeholder: SendFlowStore {
|
||||||
return SendFlowStore(
|
return SendFlowStore(
|
||||||
initialState: .init(
|
initialState: .init(
|
||||||
|
memoState: .placeholder,
|
||||||
route: nil,
|
route: nil,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState: .placeholder
|
transactionAmountInputState: .placeholder
|
||||||
|
@ -314,7 +329,8 @@ extension SendFlowStore {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||||
walletStorage: .live()
|
walletStorage: .live(),
|
||||||
|
zcashSDKEnvironment: .mainnet
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ struct SendFLowView_Previews: PreviewProvider {
|
||||||
SendFlowView(
|
SendFlowView(
|
||||||
store: .init(
|
store: .init(
|
||||||
initialState: .init(
|
initialState: .init(
|
||||||
|
memoState: .placeholder,
|
||||||
route: nil,
|
route: nil,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState: .placeholder
|
transactionAmountInputState: .placeholder
|
||||||
|
@ -45,7 +46,8 @@ struct SendFLowView_Previews: PreviewProvider {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||||
walletStorage: .live()
|
walletStorage: .live(),
|
||||||
|
zcashSDKEnvironment: .mainnet
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -62,13 +62,12 @@ struct CreateTransaction: View {
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
VStack {
|
MultipleLineTextField(
|
||||||
Text("Memo")
|
store: store.memoStore(),
|
||||||
|
title: "Memo",
|
||||||
TextEditor(text: viewStore.bindingForMemo)
|
titleAccessoryView: {}
|
||||||
.frame(maxWidth: .infinity, maxHeight: 150, alignment: .center)
|
)
|
||||||
.importSeedEditorModifier(Asset.Colors.Text.activeButtonText.color)
|
.frame(height: 200)
|
||||||
}
|
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
|
|
|
@ -20,7 +20,7 @@ struct TransactionSent: View {
|
||||||
|
|
||||||
Text("amount: \(viewStore.amount.decimalString())")
|
Text("amount: \(viewStore.amount.decimalString())")
|
||||||
+ Text(" address: \(viewStore.address)")
|
+ Text(" address: \(viewStore.address)")
|
||||||
+ Text(" memo: \(viewStore.memo)")
|
+ Text(" memo: \(viewStore.memoState.text)")
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,6 +110,7 @@ internal enum Asset {
|
||||||
internal static let drawerTabsText = ColorAsset(name: "DrawerTabsText")
|
internal static let drawerTabsText = ColorAsset(name: "DrawerTabsText")
|
||||||
internal static let heading = ColorAsset(name: "Heading")
|
internal static let heading = ColorAsset(name: "Heading")
|
||||||
internal static let importSeedEditor = ColorAsset(name: "ImportSeedEditor")
|
internal static let importSeedEditor = ColorAsset(name: "ImportSeedEditor")
|
||||||
|
internal static let invalidEntry = ColorAsset(name: "InvalidEntry")
|
||||||
internal static let medium = ColorAsset(name: "Medium")
|
internal static let medium = ColorAsset(name: "Medium")
|
||||||
internal static let regular = ColorAsset(name: "Regular")
|
internal static let regular = ColorAsset(name: "Regular")
|
||||||
internal static let secondaryButtonText = ColorAsset(name: "SecondaryButtonText")
|
internal static let secondaryButtonText = ColorAsset(name: "SecondaryButtonText")
|
||||||
|
@ -124,6 +125,7 @@ internal enum Asset {
|
||||||
internal static let moreInfoText = ColorAsset(name: "moreInfoText")
|
internal static let moreInfoText = ColorAsset(name: "moreInfoText")
|
||||||
}
|
}
|
||||||
internal enum TextField {
|
internal enum TextField {
|
||||||
|
internal static let multilineOutline = ColorAsset(name: "MultilineOutline")
|
||||||
internal static let titleAccessoryButton = ColorAsset(name: "TitleAccessoryButton")
|
internal static let titleAccessoryButton = ColorAsset(name: "TitleAccessoryButton")
|
||||||
internal static let titleAccessoryButtonPressed = ColorAsset(name: "TitleAccessoryButtonPressed")
|
internal static let titleAccessoryButtonPressed = ColorAsset(name: "TitleAccessoryButtonPressed")
|
||||||
internal enum Underline {
|
internal enum Underline {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,7 +69,7 @@ class DeeplinkTests: XCTestCase {
|
||||||
state.homeState.route = .send
|
state.homeState.route = .send
|
||||||
state.homeState.sendState.amount = amount
|
state.homeState.sendState.amount = amount
|
||||||
state.homeState.sendState.address = address
|
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.route = .send
|
||||||
state.homeState.sendState.amount = amount
|
state.homeState.sendState.amount = amount
|
||||||
state.homeState.sendState.address = address
|
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.route = .send
|
||||||
state.homeState.sendState.amount = amount
|
state.homeState.sendState.amount = amount
|
||||||
state.homeState.sendState.address = address
|
state.homeState.sendState.address = address
|
||||||
state.homeState.sendState.memo = memo
|
state.homeState.sendState.memoState.text = memo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,7 +40,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: MockWrappedSDKSynchronizer(),
|
SDKSynchronizer: MockWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: storage)
|
walletStorage: .live(walletStorage: storage),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -97,7 +98,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: storage)
|
walletStorage: .live(walletStorage: storage),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -137,7 +139,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -180,7 +183,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -205,7 +209,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -230,6 +235,7 @@ class SendTests: XCTestCase {
|
||||||
|
|
||||||
func testFundsSufficiency() throws {
|
func testFundsSufficiency() throws {
|
||||||
let sendState = SendFlowState(
|
let sendState = SendFlowState(
|
||||||
|
memoState: .placeholder,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState:
|
transactionAmountInputState:
|
||||||
TransactionAmountTextFieldState(
|
TransactionAmountTextFieldState(
|
||||||
|
@ -251,7 +257,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(numberFormatter: usNumberFormatter),
|
numberFormatter: .live(numberFormatter: usNumberFormatter),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -300,11 +307,13 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(numberFormatter: usNumberFormatter),
|
numberFormatter: .live(numberFormatter: usNumberFormatter),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
initialState: .init(
|
initialState: .init(
|
||||||
|
memoState: .placeholder,
|
||||||
route: nil,
|
route: nil,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState:
|
transactionAmountInputState:
|
||||||
|
@ -337,6 +346,7 @@ class SendTests: XCTestCase {
|
||||||
|
|
||||||
func testValidForm() throws {
|
func testValidForm() throws {
|
||||||
let sendState = SendFlowState(
|
let sendState = SendFlowState(
|
||||||
|
memoState: .placeholder,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState:
|
transactionAmountInputState:
|
||||||
TransactionAmountTextFieldState(
|
TransactionAmountTextFieldState(
|
||||||
|
@ -359,7 +369,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -383,6 +394,7 @@ class SendTests: XCTestCase {
|
||||||
|
|
||||||
func testInvalidForm_InsufficientFunds() throws {
|
func testInvalidForm_InsufficientFunds() throws {
|
||||||
let sendState = SendFlowState(
|
let sendState = SendFlowState(
|
||||||
|
memoState: .placeholder,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState:
|
transactionAmountInputState:
|
||||||
TransactionAmountTextFieldState(
|
TransactionAmountTextFieldState(
|
||||||
|
@ -404,7 +416,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -428,6 +441,7 @@ class SendTests: XCTestCase {
|
||||||
|
|
||||||
func testInvalidForm_AddressFormat() throws {
|
func testInvalidForm_AddressFormat() throws {
|
||||||
let sendState = SendFlowState(
|
let sendState = SendFlowState(
|
||||||
|
memoState: .placeholder,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState:
|
transactionAmountInputState:
|
||||||
TransactionAmountTextFieldState(
|
TransactionAmountTextFieldState(
|
||||||
|
@ -449,7 +463,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
|
@ -473,6 +488,7 @@ class SendTests: XCTestCase {
|
||||||
|
|
||||||
func testInvalidForm_AmountFormat() throws {
|
func testInvalidForm_AmountFormat() throws {
|
||||||
let sendState = SendFlowState(
|
let sendState = SendFlowState(
|
||||||
|
memoState: .placeholder,
|
||||||
transactionAddressInputState: .placeholder,
|
transactionAddressInputState: .placeholder,
|
||||||
transactionAmountInputState:
|
transactionAmountInputState:
|
||||||
TransactionAmountTextFieldState(
|
TransactionAmountTextFieldState(
|
||||||
|
@ -494,7 +510,8 @@ class SendTests: XCTestCase {
|
||||||
numberFormatter: .live(),
|
numberFormatter: .live(),
|
||||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||||
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
|
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
|
||||||
|
zcashSDKEnvironment: .testnet
|
||||||
)
|
)
|
||||||
|
|
||||||
let store = TestStore(
|
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 {
|
private extension SendTests {
|
||||||
|
|
Loading…
Reference in New Issue