[#294] amount + address fields done (#308)

cleanup

validation

[#294] [Scaffold] Send Screen - transaction + address inputs (#308)

- zec amount refactored to be always Int64 - number formatter to zec string for the UI added
- cleaned up the invalid input conditions by providing computed properties
- isValidZcashAddress() added

[#294] [Scaffold] Send Screen - transaction + address inputs (#308)

- prefix input view support implemented
- zcash or $ symbol used as a prefix for the amount input
- $ computations and max out implemented

[#294] [Scaffold] Send Screen - transaction + address inputs (#308)

- insufficient funds logic (UI error + inability to send) added
- $ balance value visible at send screen
- cleanup

[#294] [Scaffold] Send Screen - transaction + address inputs (#308)

- send routing simplified and cleaned up

[#294] [Scaffold] Send Screen - transaction + address inputs (#308)

- unit tests

[#294] [Scaffold] Send Screen - transaction + address inputs (#308)

- cleanup

[#294] [Scaffold] Send Screen - transaction + address inputs (#308)

- review comments solved
This commit is contained in:
Lukas Korba 2022-05-11 17:26:39 +02:00 committed by GitHub
parent 10070c3c02
commit 2bb5451047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1467 additions and 463 deletions

View File

@ -59,13 +59,13 @@
2E35F99A27B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E35F99927B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift */; }; 2E35F99A27B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E35F99927B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift */; };
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */; }; 2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */; };
2E6CF8DD27D78319004DCD7A /* CurrencySelectionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */; }; 2E6CF8DD27D78319004DCD7A /* CurrencySelectionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */; };
2E8719CB27FB09990082C926 /* TransactionTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8719CA27FB09990082C926 /* TransactionTextField.swift */; }; 2E8719CB27FB09990082C926 /* TransactionAmountTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8719CA27FB09990082C926 /* TransactionAmountTextField.swift */; };
2E8719CD27FB0D3B0082C926 /* TransactionCurrencySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */; }; 2E8719CD27FB0D3B0082C926 /* TransactionCurrencySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */; };
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; }; 2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; };
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; }; 2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; };
2EB1C5E827D77F6100BC43D7 /* TextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */; }; 2EB1C5E827D77F6100BC43D7 /* TextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */; };
2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */; }; 2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */; };
2EB7758727FC67FD00269373 /* TransactionInputStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB7758627FC67FD00269373 /* TransactionInputStore.swift */; }; 2EB7758727FC67FD00269373 /* TransactionAmountInputStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB7758627FC67FD00269373 /* TransactionAmountInputStore.swift */; };
2EDA07A027EDE18C00D6F09B /* TextFieldInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */; }; 2EDA07A027EDE18C00D6F09B /* TextFieldInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */; };
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */; }; 2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */; };
2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */; }; 2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */; };
@ -107,6 +107,8 @@
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF643281FEC9900BA3F17 /* SendTests.swift */; }; 9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF643281FEC9900BA3F17 /* SendTests.swift */; };
9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */; }; 9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */; };
9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */; }; 9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */; };
9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */; };
9E5BF6502823E94900BA3F17 /* TransactionAddressInputStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64E2823E94900BA3F17 /* TransactionAddressInputStore.swift */; };
9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; }; 9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; };
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; }; 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; };
9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; }; 9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; };
@ -120,6 +122,10 @@
9EAFEB9228081E9400199FC9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874EF273C4DE200F0E875 /* HomeView.swift */; }; 9EAFEB9228081E9400199FC9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874EF273C4DE200F0E875 /* HomeView.swift */; };
9EBEF87A27CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */; }; 9EBEF87A27CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */; };
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */; }; 9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */; };
9EDDEA8C28250F9C00B4100C /* Double+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */; };
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */; };
9EDDEAA32829610D00B4100C /* TransactionAmountInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */; };
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */; };
9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */; }; 9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */; };
9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */; }; 9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */; };
9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135F27F043CC0075AF48 /* AppDelegate.swift */; }; 9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135F27F043CC0075AF48 /* AppDelegate.swift */; };
@ -231,12 +237,12 @@
2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeaderView.swift; sourceTree = "<group>"; }; 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeaderView.swift; sourceTree = "<group>"; };
2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; }; 2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySelectionStore.swift; sourceTree = "<group>"; }; 2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySelectionStore.swift; sourceTree = "<group>"; };
2E8719CA27FB09990082C926 /* TransactionTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionTextField.swift; sourceTree = "<group>"; }; 2E8719CA27FB09990082C926 /* TransactionAmountTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionAmountTextField.swift; sourceTree = "<group>"; };
2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionCurrencySelector.swift; sourceTree = "<group>"; }; 2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionCurrencySelector.swift; sourceTree = "<group>"; };
2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = "<group>"; }; 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = "<group>"; };
2EA11F5C27467F7700709571 /* OnboardingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingContentView.swift; sourceTree = "<group>"; }; 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingContentView.swift; sourceTree = "<group>"; };
2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldStore.swift; sourceTree = "<group>"; }; 2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldStore.swift; sourceTree = "<group>"; };
2EB7758627FC67FD00269373 /* TransactionInputStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInputStore.swift; sourceTree = "<group>"; }; 2EB7758627FC67FD00269373 /* TransactionAmountInputStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputStore.swift; sourceTree = "<group>"; };
2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldInput.swift; sourceTree = "<group>"; }; 2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldInput.swift; sourceTree = "<group>"; };
2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldFooter.swift; sourceTree = "<group>"; }; 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldFooter.swift; sourceTree = "<group>"; };
2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugFrame.swift; sourceTree = "<group>"; }; 2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugFrame.swift; sourceTree = "<group>"; };
@ -276,6 +282,8 @@
9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = "<group>"; }; 9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = "<group>"; };
9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedUserDefaults.swift; sourceTree = "<group>"; }; 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedUserDefaults.swift; sourceTree = "<group>"; };
9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedNotificationCenter.swift; sourceTree = "<group>"; }; 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedNotificationCenter.swift; sourceTree = "<group>"; };
9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextField.swift; sourceTree = "<group>"; };
9E5BF64E2823E94900BA3F17 /* TransactionAddressInputStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputStore.swift; sourceTree = "<group>"; };
9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; }; 9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; };
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; }; 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; };
9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; }; 9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; };
@ -287,6 +295,10 @@
9EAFEB8E2808183D00199FC9 /* SandboxStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxStore.swift; sourceTree = "<group>"; }; 9EAFEB8E2808183D00199FC9 /* SandboxStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxStore.swift; sourceTree = "<group>"; };
9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = "<group>"; }; 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = "<group>"; };
9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFiles.swift; sourceTree = "<group>"; }; 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFiles.swift; sourceTree = "<group>"; };
9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Zcash.swift"; sourceTree = "<group>"; };
9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencySelectionTests.swift; sourceTree = "<group>"; };
9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputTests.swift; sourceTree = "<group>"; };
9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputTests.swift; sourceTree = "<group>"; };
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; }; 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; };
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = "<group>"; }; 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = "<group>"; };
9EF8135F27F043CC0075AF48 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 9EF8135F27F043CC0075AF48 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -577,6 +589,7 @@
9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */, 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */,
9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */, 9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */,
9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */, 9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */,
9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */,
9E2F1C832809B606004E65FE /* DebugMenu.swift */, 9E2F1C832809B606004E65FE /* DebugMenu.swift */,
); );
path = Util; path = Util;
@ -630,10 +643,8 @@
2E35F99027B28E6800EB79CD /* TextFields */ = { 2E35F99027B28E6800EB79CD /* TextFields */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2E8719CA27FB09990082C926 /* TransactionTextField.swift */, 9E5BF64C2823E84300BA3F17 /* TransactionAddress */,
2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */, 9E5BF64B2823C91200BA3F17 /* TransactionAmount */,
2EB7758627FC67FD00269373 /* TransactionInputStore.swift */,
2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */,
2EDA07A527EDE31100D6F09B /* Components */, 2EDA07A527EDE31100D6F09B /* Components */,
); );
path = TextFields; path = TextFields;
@ -653,11 +664,11 @@
2EDA07A527EDE31100D6F09B /* Components */ = { 2EDA07A527EDE31100D6F09B /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */,
2E35F99127B28E7600EB79CD /* SingleLineTextField.swift */, 2E35F99127B28E7600EB79CD /* SingleLineTextField.swift */,
2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */, 2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */,
2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */, 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */,
2E35F99927B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift */, 2E35F99927B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift */,
2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -806,11 +817,34 @@
9E5BF642281FEC8700BA3F17 /* SendTests */ = { 9E5BF642281FEC8700BA3F17 /* SendTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */,
9E5BF643281FEC9900BA3F17 /* SendTests.swift */, 9E5BF643281FEC9900BA3F17 /* SendTests.swift */,
9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */,
9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */,
); );
path = SendTests; path = SendTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9E5BF64B2823C91200BA3F17 /* TransactionAmount */ = {
isa = PBXGroup;
children = (
2EB7758627FC67FD00269373 /* TransactionAmountInputStore.swift */,
2E8719CA27FB09990082C926 /* TransactionAmountTextField.swift */,
2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */,
2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */,
);
path = TransactionAmount;
sourceTree = "<group>";
};
9E5BF64C2823E84300BA3F17 /* TransactionAddress */ = {
isa = PBXGroup;
children = (
9E5BF64E2823E94900BA3F17 /* TransactionAddressInputStore.swift */,
9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */,
);
path = TransactionAddress;
sourceTree = "<group>";
};
9EAFEB802805791400199FC9 /* AppReducerTests */ = { 9EAFEB802805791400199FC9 /* AppReducerTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1224,7 +1258,7 @@
0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */, 0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */,
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */, F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
2EDA07A027EDE18C00D6F09B /* TextFieldInput.swift in Sources */, 2EDA07A027EDE18C00D6F09B /* TextFieldInput.swift in Sources */,
2EB7758727FC67FD00269373 /* TransactionInputStore.swift in Sources */, 2EB7758727FC67FD00269373 /* TransactionAmountInputStore.swift in Sources */,
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */, 669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */, 9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */,
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */, 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */,
@ -1254,6 +1288,7 @@
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */, 669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
2E8719CD27FB0D3B0082C926 /* TransactionCurrencySelector.swift in Sources */, 2E8719CD27FB0D3B0082C926 /* TransactionCurrencySelector.swift in Sources */,
F9971A6C27680E1000A2DB75 /* WalletInfoView.swift in Sources */, F9971A6C27680E1000A2DB75 /* WalletInfoView.swift in Sources */,
9E5BF6502823E94900BA3F17 /* TransactionAddressInputStore.swift in Sources */,
F9EEB8162742C2210032EEB8 /* WithStateBinding.swift in Sources */, F9EEB8162742C2210032EEB8 /* WithStateBinding.swift in Sources */,
F93673D62742CB840099C6AF /* Previews.swift in Sources */, F93673D62742CB840099C6AF /* Previews.swift in Sources */,
0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */, 0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */,
@ -1294,12 +1329,14 @@
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */, 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */, 9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */,
9E2AC10327DA28200042AA47 /* WalletStorage.swift in Sources */, 9E2AC10327DA28200042AA47 /* WalletStorage.swift in Sources */,
9EDDEA8C28250F9C00B4100C /* Double+Zcash.swift in Sources */,
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */, 9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */,
9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */, 9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */,
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */, F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
0D185819272723FF0046B928 /* ColoredChip.swift in Sources */, 0D185819272723FF0046B928 /* ColoredChip.swift in Sources */,
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */, 2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */, 2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */,
9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */,
2E35F99227B28E7600EB79CD /* SingleLineTextField.swift in Sources */, 2E35F99227B28E7600EB79CD /* SingleLineTextField.swift in Sources */,
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */, 0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */,
66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */, 66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */,
@ -1322,7 +1359,7 @@
F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */, F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */,
9EF8139C27F47AED0075AF48 /* InitializationState.swift in Sources */, 9EF8139C27F47AED0075AF48 /* InitializationState.swift in Sources */,
0D0781C9278776D20083ACD7 /* ZcashSymbol.swift in Sources */, 0D0781C9278776D20083ACD7 /* ZcashSymbol.swift in Sources */,
2E8719CB27FB09990082C926 /* TransactionTextField.swift in Sources */, 2E8719CB27FB09990082C926 /* TransactionAmountTextField.swift in Sources */,
6654C7412715A47300901167 /* Onboarding.swift in Sources */, 6654C7412715A47300901167 /* Onboarding.swift in Sources */,
F9C165C42740403600592F76 /* TransactionSentView.swift in Sources */, F9C165C42740403600592F76 /* TransactionSentView.swift in Sources */,
F9971A5927680DDE00A2DB75 /* RequestStore.swift in Sources */, F9971A5927680DDE00A2DB75 /* RequestStore.swift in Sources */,
@ -1334,7 +1371,10 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */, 0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */,
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */,
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */, 6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
9EDDEAA32829610D00B4100C /* TransactionAmountInputTests.swift in Sources */,
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */, 9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */,
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */, 9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */, 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,

View File

@ -1,184 +1,187 @@
{ {
"pins" : [ "object": {
{ "pins": [
"identity" : "combine-schedulers", {
"kind" : "remoteSourceControl", "package": "combine-schedulers",
"location" : "https://github.com/pointfreeco/combine-schedulers", "repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
"state" : { "state": {
"revision" : "4cf088c29a20f52be0f2ca54992b492c54e0076b", "branch": null,
"version" : "0.5.3" "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b",
"version": "0.5.3"
}
},
{
"package": "grpc-swift",
"repositoryURL": "https://github.com/grpc/grpc-swift.git",
"state": {
"branch": null,
"revision": "593fe0fe931f7e838969243cd137be48e8055b1d",
"version": "1.7.3"
}
},
{
"package": "MnemonicSwift",
"repositoryURL": "https://github.com/zcash-hackworks/MnemonicSwift",
"state": {
"branch": null,
"revision": "b10b0b8ee1f297e33ea5b1bc041ced49943b6582",
"version": "2.2.3"
}
},
{
"package": "SQLite.swift",
"repositoryURL": "https://github.com/stephencelis/SQLite.swift.git",
"state": {
"branch": null,
"revision": "4d543d811ee644fa4cc4bfa0be996b4dd6ba0f54",
"version": "0.13.3"
}
},
{
"package": "swift-case-paths",
"repositoryURL": "https://github.com/pointfreeco/swift-case-paths",
"state": {
"branch": null,
"revision": "d226d167bd4a68b51e352af5655c92bce8ee0463",
"version": "0.7.0"
}
},
{
"package": "swift-collections",
"repositoryURL": "https://github.com/apple/swift-collections",
"state": {
"branch": null,
"revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e",
"version": "1.0.1"
}
},
{
"package": "swift-composable-architecture",
"repositoryURL": "https://github.com/pointfreeco/swift-composable-architecture",
"state": {
"branch": null,
"revision": "599a2398adaaa7a4e3f5420cde7728c39e33677e",
"version": "0.28.1"
}
},
{
"package": "swift-crypto",
"repositoryURL": "https://github.com/apple/swift-crypto.git",
"state": {
"branch": null,
"revision": "067254c79435de759aeef4a6a03e43d087d61312",
"version": "2.0.5"
}
},
{
"package": "swift-custom-dump",
"repositoryURL": "https://github.com/pointfreeco/swift-custom-dump",
"state": {
"branch": null,
"revision": "21f8fdbb3226e5e28a1a2fffac3e0f3deec34bf0",
"version": "0.2.1"
}
},
{
"package": "swift-identified-collections",
"repositoryURL": "https://github.com/pointfreeco/swift-identified-collections",
"state": {
"branch": null,
"revision": "f76e7d3fe4265ee09216044ec3780d74f546ca82",
"version": "0.3.1"
}
},
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
"revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
"version": "1.4.2"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "d6e3762e0a5f7ede652559f53623baf11006e17c",
"version": "2.39.0"
}
},
{
"package": "swift-nio-extras",
"repositoryURL": "https://github.com/apple/swift-nio-extras.git",
"state": {
"branch": null,
"revision": "f73ca5ee9c6806800243f1ac415fcf82de9a4c91",
"version": "1.10.2"
}
},
{
"package": "swift-nio-http2",
"repositoryURL": "https://github.com/apple/swift-nio-http2.git",
"state": {
"branch": null,
"revision": "50c25c132b140e62b45e90b5a76f13ded02c8a46",
"version": "1.20.1"
}
},
{
"package": "swift-nio-ssl",
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
"state": {
"branch": null,
"revision": "b5260a31c2a72a89fa684f5efb3054d8725a2316",
"version": "2.18.0"
}
},
{
"package": "swift-nio-transport-services",
"repositoryURL": "https://github.com/apple/swift-nio-transport-services.git",
"state": {
"branch": null,
"revision": "8ab824b140d0ebcd87e9149266ddc353e3705a3e",
"version": "1.11.4"
}
},
{
"package": "SwiftProtobuf",
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
"revision": "e1499bc69b9040b29184f7f2996f7bab467c1639",
"version": "1.19.0"
}
},
{
"package": "xctest-dynamic-overlay",
"repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state": {
"branch": null,
"revision": "50a70a9d3583fe228ce672e8923010c8df2deddd",
"version": "0.2.1"
}
},
{
"package": "libzcashlc",
"repositoryURL": "https://github.com/zcash-hackworks/zcash-light-client-ffi.git",
"state": {
"branch": "main",
"revision": "8d4cff1ac9afccd7d7b6c4317dfe5e30c5c5bb42",
"version": null
}
},
{
"package": "ZcashLightClientKit",
"repositoryURL": "https://github.com/zcash/ZcashLightClientKit",
"state": {
"branch": null,
"revision": "f3150072f5cafd53fa064b7cfc80ef3a84460fb2",
"version": null
}
} }
}, ]
{ },
"identity" : "grpc-swift", "version": 1
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift.git",
"state" : {
"revision" : "593fe0fe931f7e838969243cd137be48e8055b1d",
"version" : "1.7.3"
}
},
{
"identity" : "mnemonicswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/MnemonicSwift",
"state" : {
"revision" : "716a2c32ac2bbd8a1499ac834077df42b75edc85",
"version" : "2.2.4"
}
},
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift.git",
"state" : {
"revision" : "4d543d811ee644fa4cc4bfa0be996b4dd6ba0f54",
"version" : "0.13.3"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "ce9c0d897db8a840c39de64caaa9b60119cf4be8",
"version" : "0.8.1"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "48254824bb4248676bf7ce56014ff57b142b77eb",
"version" : "1.0.2"
}
},
{
"identity" : "swift-composable-architecture",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
"state" : {
"revision" : "599a2398adaaa7a4e3f5420cde7728c39e33677e",
"version" : "0.28.1"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "d9825fa541df64b1a7b182178d61b9a82730d01f",
"version" : "2.1.0"
}
},
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump",
"state" : {
"revision" : "c4f78db9b90ca57b7b6abc2223e235242739ea3c",
"version" : "0.4.0"
}
},
{
"identity" : "swift-identified-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-identified-collections",
"state" : {
"revision" : "680bf440178a78a627b1c2c64c0855f6523ad5b9",
"version" : "0.3.2"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
"version" : "1.4.2"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "124119f0bb12384cef35aa041d7c3a686108722d",
"version" : "2.40.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "8eea84ec6144167354387ef9244b0939f5852dc8",
"version" : "1.11.0"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "72bcaf607b40d7c51044f65b0f5ed8581a911832",
"version" : "1.21.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "1750873bce84b4129b5303655cce2c3d35b9ed3a",
"version" : "2.19.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "1a4692acb88156e3da1b0c6732a8a38b2a744166",
"version" : "1.12.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "e1499bc69b9040b29184f7f2996f7bab467c1639",
"version" : "1.19.0"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "50a70a9d3583fe228ce672e8923010c8df2deddd",
"version" : "0.2.1"
}
},
{
"identity" : "zcash-light-client-ffi",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi.git",
"state" : {
"branch" : "main",
"revision" : "1d236d07b9f8ea7d1380175cdef5c00bde70eed8"
}
},
{
"identity" : "zcashlightclientkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash/ZcashLightClientKit",
"state" : {
"revision" : "f3150072f5cafd53fa064b7cfc80ef3a84460fb2"
}
}
],
"version" : 2
} }

View File

@ -18,9 +18,9 @@ struct HomeState: Equatable {
var sendState: SendState var sendState: SendState
var scanState: ScanState var scanState: ScanState
var synchronizerStatus: String var synchronizerStatus: String
var totalBalance: Double var totalBalance: Int64
var transactionHistoryState: TransactionHistoryState var transactionHistoryState: TransactionHistoryState
var verifiedBalance: Double var verifiedBalance: Int64
} }
enum HomeAction: Equatable { enum HomeAction: Equatable {
@ -95,8 +95,8 @@ extension HomeReducer {
return Effect(value: .updateSynchronizerStatus) return Effect(value: .updateSynchronizerStatus)
case .updateBalance(let balance): case .updateBalance(let balance):
state.totalBalance = balance.total.asHumanReadableZecBalance() state.totalBalance = balance.total
state.verifiedBalance = balance.verified.asHumanReadableZecBalance() state.verifiedBalance = balance.verified
return .none return .none
case .updateDrawer(let drawerOverlay): case .updateDrawer(let drawerOverlay):
@ -244,9 +244,9 @@ extension HomeState {
sendState: .placeholder, sendState: .placeholder,
scanState: .placeholder, scanState: .placeholder,
synchronizerStatus: "", synchronizerStatus: "",
totalBalance: 0.0, totalBalance: 0,
transactionHistoryState: .emptyPlaceHolder, transactionHistoryState: .emptyPlaceHolder,
verifiedBalance: 0.0 verifiedBalance: 0
) )
} }
} }

View File

@ -18,7 +18,7 @@ struct HomeView: View {
Text("\(viewStore.synchronizerStatus)") Text("\(viewStore.synchronizerStatus)")
.padding(.top, 60) .padding(.top, 60)
Text("balance: \(viewStore.totalBalance)") Text("balance \(viewStore.totalBalance.asZecString()) ZEC")
.accessDebugMenuWithHiddenGesture { .accessDebugMenuWithHiddenGesture {
viewStore.send(.debugMenuStartup) viewStore.send(.debugMenuStartup)
} }

View File

@ -20,6 +20,7 @@ struct ImportSeedEditor: View {
var body: some View { var body: some View {
WithViewStore(store) { viewStore in WithViewStore(store) { viewStore in
TextEditor(text: viewStore.binding(\.$importedSeedPhrase)) TextEditor(text: viewStore.binding(\.$importedSeedPhrase))
.autocapitalization(.none)
.importSeedEditorModifier() .importSeedEditorModifier()
.padding(28) .padding(28)
} }
@ -27,11 +28,13 @@ struct ImportSeedEditor: View {
} }
struct ImportSeedEditorModifier: ViewModifier { struct ImportSeedEditorModifier: ViewModifier {
var backgroundColor = Color.white
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.foregroundColor(Asset.Colors.Text.importSeedEditor.color) .foregroundColor(Asset.Colors.Text.importSeedEditor.color)
.padding() .padding()
.background(Color.white) .background(backgroundColor)
.cornerRadius(4) .cornerRadius(4)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
@ -41,8 +44,8 @@ struct ImportSeedEditorModifier: ViewModifier {
} }
extension View { extension View {
func importSeedEditorModifier() -> some View { func importSeedEditorModifier(_ backgroundColor: Color = .white) -> some View {
modifier(ImportSeedEditorModifier()) modifier(ImportSeedEditorModifier(backgroundColor: backgroundColor))
} }
} }

View File

@ -6,11 +6,6 @@ struct Transaction: Equatable {
var amount: Int64 var amount: Int64
var memo: String var memo: String
var toAddress: String var toAddress: String
var amountString: String {
get { amount == 0 ? "" : String(format: "%.7f", amount.asHumanReadableZecBalance()) }
set { amount = Int64((newValue as NSString).doubleValue * 100_000_000) }
}
} }
extension Transaction { extension Transaction {
@ -25,8 +20,7 @@ extension Transaction {
struct SendState: Equatable { struct SendState: Equatable {
enum Route: Equatable { enum Route: Equatable {
case showConfirmation case confirmation
case showSent
case success case success
case failure case failure
case done case done
@ -35,9 +29,35 @@ struct SendState: Equatable {
var route: Route? var route: Route?
var isSendingTransaction = false var isSendingTransaction = false
var totalBalance = 0.0 var memo = ""
var totalBalance: Int64 = 0
var transaction: Transaction var transaction: Transaction
var transactionInputState: TransactionInputState var transactionAddressInputState: TransactionAddressInputState
var transactionAmountInputState: TransactionAmountInputState
var isInvalidAddressFormat: Bool {
!transactionAddressInputState.isValidAddress
&& !transactionAddressInputState.textFieldState.text.isEmpty
}
var isInvalidAmountFormat: Bool {
!transactionAmountInputState.textFieldState.valid
&& !transactionAmountInputState.textFieldState.text.isEmpty
}
var isValidForm: Bool {
transactionAmountInputState.amount > 0
&& transactionAddressInputState.isValidAddress
&& !isInsufficientFunds
}
var isInsufficientFunds: Bool {
transactionAmountInputState.amount > transactionAmountInputState.maxValue
}
var totalCurrencyBalance: Int64 {
(totalBalance.asHumanReadableZecBalance() * transactionAmountInputState.zecPrice).asZec()
}
} }
enum SendAction: Equatable { enum SendAction: Equatable {
@ -46,8 +66,10 @@ enum SendAction: Equatable {
case sendConfirmationPressed case sendConfirmationPressed
case sendTransactionResult(Result<TransactionState, NSError>) case sendTransactionResult(Result<TransactionState, NSError>)
case synchronizerStateChanged(WrappedSDKSynchronizerState) case synchronizerStateChanged(WrappedSDKSynchronizerState)
case transactionInput(TransactionInputAction) case transactionAddressInput(TransactionAddressInputAction)
case updateBalance(Double) case transactionAmountInput(TransactionAmountInputAction)
case updateBalance(Int64)
case updateMemo(String)
case updateTransaction(Transaction) case updateTransaction(Transaction)
case updateRoute(SendState.Route?) case updateRoute(SendState.Route?)
} }
@ -71,9 +93,9 @@ extension SendReducer {
static let `default` = SendReducer.combine( static let `default` = SendReducer.combine(
[ [
balanceReducer,
sendReducer, sendReducer,
transactionInputReducer transactionAddressInputReducer,
transactionAmountInputReducer
] ]
) )
.debug() .debug()
@ -89,6 +111,11 @@ extension SendReducer {
state.isSendingTransaction = false state.isSendingTransaction = false
return .none return .none
case .updateRoute(.confirmation):
state.transaction.amount = state.transactionAmountInputState.amount
state.transaction.toAddress = state.transactionAddressInputState.textFieldState.text
return .none
case let .updateRoute(route): case let .updateRoute(route):
state.route = route state.route = route
return .none return .none
@ -129,16 +156,13 @@ extension SendReducer {
} catch { } catch {
return Effect(value: .updateRoute(.failure)) return Effect(value: .updateRoute(.failure))
} }
case .transactionInput(let transactionInput):
return .none
default: case .transactionAmountInput(let transactionInput):
return .none return .none
}
} case .transactionAddressInput(let transactionInput):
return .none
private static let balanceReducer = SendReducer { state, action, environment in
switch action {
case .onAppear: case .onAppear:
return environment.wrappedSDKSynchronizer.stateChanged return environment.wrappedSDKSynchronizer.stateChanged
.map(SendAction.synchronizerStateChanged) .map(SendAction.synchronizerStateChanged)
@ -151,7 +175,7 @@ extension SendReducer {
case .synchronizerStateChanged(.synced): case .synchronizerStateChanged(.synced):
return environment.wrappedSDKSynchronizer.getShieldedBalance() return environment.wrappedSDKSynchronizer.getShieldedBalance()
.receive(on: environment.scheduler) .receive(on: environment.scheduler)
.map({ Double($0.total) / Double(100_000_000) }) .map({ $0.total })
.map(SendAction.updateBalance) .map(SendAction.updateBalance)
.eraseToEffect() .eraseToEffect()
@ -160,18 +184,29 @@ extension SendReducer {
case .updateBalance(let balance): case .updateBalance(let balance):
state.totalBalance = balance state.totalBalance = balance
state.transactionInputState.maxValue = Int64(balance * 100_000_000) state.transactionAmountInputState.maxValue = balance
return .none return .none
default: case .updateMemo(let memo):
state.memo = memo
return .none return .none
} }
} }
private static let transactionInputReducer: SendReducer = TransactionInputReducer.default.pullback( private static let transactionAddressInputReducer: SendReducer = TransactionAddressInputReducer.default.pullback(
state: \SendState.transactionInputState, state: \SendState.transactionAddressInputState,
action: /SendAction.transactionInput, action: /SendAction.transactionAddressInput,
environment: { _ in TransactionInputEnvironment() } environment: { environment in
TransactionAddressInputEnvironment(
wrappedDerivationTool: environment.wrappedDerivationTool
)
}
)
private static let transactionAmountInputReducer: SendReducer = TransactionAmountInputReducer.default.pullback(
state: \SendState.transactionAmountInputState,
action: /SendAction.transactionAmountInput,
environment: { _ in TransactionAmountInputEnvironment() }
) )
static func `default`(whenDone: @escaping () -> Void) -> SendReducer { static func `default`(whenDone: @escaping () -> Void) -> SendReducer {
@ -211,36 +246,36 @@ extension SendViewStore {
var bindingForConfirmation: Binding<Bool> { var bindingForConfirmation: Binding<Bool> {
self.routeBinding.map( self.routeBinding.map(
extract: { $0 == .showConfirmation || self.bindingForSuccess.wrappedValue || self.bindingForFailure.wrappedValue }, extract: { $0 == .confirmation || self.bindingForSuccess.wrappedValue || self.bindingForFailure.wrappedValue },
embed: { $0 ? SendState.Route.showConfirmation : nil } embed: { $0 ? SendState.Route.confirmation : nil }
) )
} }
var bindingForSuccess: Binding<Bool> { var bindingForSuccess: Binding<Bool> {
self.routeBinding.map( self.routeBinding.map(
extract: { $0 == .success || self.bindingForDone.wrappedValue }, extract: { $0 == .success || self.bindingForDone.wrappedValue },
embed: { $0 ? SendState.Route.success : SendState.Route.showConfirmation } embed: { $0 ? SendState.Route.success : SendState.Route.confirmation }
) )
} }
var bindingForFailure: Binding<Bool> { var bindingForFailure: Binding<Bool> {
self.routeBinding.map( self.routeBinding.map(
extract: { $0 == .failure || self.bindingForDone.wrappedValue }, extract: { $0 == .failure || self.bindingForDone.wrappedValue },
embed: { $0 ? SendState.Route.failure : SendState.Route.showConfirmation } embed: { $0 ? SendState.Route.failure : SendState.Route.confirmation }
) )
} }
var bindingForDone: Binding<Bool> { var bindingForDone: Binding<Bool> {
self.routeBinding.map( self.routeBinding.map(
extract: { $0 == .done }, extract: { $0 == .done },
embed: { $0 ? SendState.Route.done : SendState.Route.showConfirmation } embed: { $0 ? SendState.Route.done : SendState.Route.confirmation }
) )
} }
var bindingForBalance: Binding<Double> { var bindingForMemo: Binding<String> {
self.binding( self.binding(
get: \.totalBalance, get: \.memo,
send: SendAction.updateBalance send: SendAction.updateMemo
) )
} }
} }
@ -252,7 +287,8 @@ extension SendState {
.init( .init(
route: nil, route: nil,
transaction: .placeholder, transaction: .placeholder,
transactionInputState: .placeholer transactionAddressInputState: .placeholder,
transactionAmountInputState: .amount
) )
} }
@ -264,7 +300,8 @@ extension SendState {
memo: "", memo: "",
toAddress: "" toAddress: ""
), ),
transactionInputState: .placeholer transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
) )
} }
} }

View File

@ -2,73 +2,89 @@ import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct CreateTransaction: View { struct CreateTransaction: View {
let store: TransactionInputStore let store: SendStore
@Binding var transaction: Transaction
@Binding var isComplete: Bool
@Binding var totalBalance: Double
var body: some View { var body: some View {
UITextView.appearance().backgroundColor = .clear UITextView.appearance().backgroundColor = .clear
return WithViewStore(store) { viewStore in return WithViewStore(store) { viewStore in
VStack { VStack {
VStack { VStack(spacing: 0) {
Text("Balance \(totalBalance)") Text("Balance \(viewStore.totalBalance.asZecString()) ZEC")
Text("($\(viewStore.totalCurrencyBalance.asZecString()))")
SingleLineTextField( .font(.system(size: 13))
placeholderText: "0", .opacity(0.6)
title: "How much ZEC would you like to send?",
store: store.scope(
state: \.textFieldState,
action: TransactionInputAction.textField
),
titleAccessoryView: {
Button(
action: { viewStore.send(.setMax(viewStore.maxValue)) },
label: { Text("Max") }
)
.textFieldTitleAccessoryButtonStyle
},
inputAccessoryView: {
}
)
} }
.padding() .padding()
VStack { VStack {
Text("To Address") TransactionAmountTextField(
store: store.scope(
TextField( state: \.transactionAmountInputState,
"Address", action: SendAction.transactionAmountInput
text: $transaction.toAddress )
) )
.font(.system(size: 14))
.padding() if viewStore.isInvalidAmountFormat {
.background(Color.white) HStack {
.foregroundColor(Asset.Colors.Text.importSeedEditor.color) Text("invalid amount")
.foregroundColor(.red)
Spacer()
}
}
if viewStore.isInsufficientFunds {
HStack {
Text("insufficient funds")
.foregroundColor(.red)
Spacer()
}
}
}
.padding()
VStack {
TransactionAddressTextField(
store: store.scope(
state: \.transactionAddressInputState,
action: SendAction.transactionAddressInput
)
)
if viewStore.isInvalidAddressFormat {
HStack {
Text("invalid address")
.foregroundColor(.red)
Spacer()
}
}
} }
.padding() .padding()
VStack { VStack {
Text("Memo") Text("Memo")
TextEditor(text: $transaction.memo) TextEditor(text: viewStore.bindingForMemo)
.frame(maxWidth: .infinity, maxHeight: 150, alignment: .center) .frame(maxWidth: .infinity, maxHeight: 150, alignment: .center)
.importSeedEditorModifier() .importSeedEditorModifier(Asset.Colors.Text.activeButtonText.color)
} }
.padding() .padding()
Button( Button(
action: { isComplete = true }, action: { viewStore.send(.updateRoute(.confirmation)) },
label: { Text("Send") } label: { Text("Send") }
) )
.activeButtonStyle .activeButtonStyle
.frame(height: 50) .frame(height: 50)
.padding() .padding()
.disabled(!viewStore.isValidForm)
Spacer() Spacer()
} }
.navigationBarTitleDisplayMode(.inline)
.padding() .padding()
.applyScreenBackground() .applyScreenBackground()
} }
@ -81,18 +97,9 @@ struct Create_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NavigationView { NavigationView {
StateContainer( StateContainer(
initialState: ( initialState: ( false )
Transaction.placeholder, ) { _ in
false, CreateTransaction(store: .placeholder)
0.0
)
) {
CreateTransaction(
store: .placeholder,
transaction: $0.0,
isComplete: $0.1,
totalBalance: $0.2
)
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
@ -107,7 +114,8 @@ extension SendStore {
initialState: .init( initialState: .init(
route: nil, route: nil,
transaction: .placeholder, transaction: .placeholder,
transactionInputState: .placeholer transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
), ),
reducer: .default, reducer: .default,
environment: SendEnvironment( environment: SendEnvironment(

View File

@ -2,33 +2,17 @@ import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct SendView: View { struct SendView: View {
let store: Store<SendState, SendAction> let store: SendStore
var body: some View { var body: some View {
WithViewStore(store) { viewStore in WithViewStore(store) { viewStore in
CreateTransaction( CreateTransaction(store: store)
store: store.scope(
state: \.transactionInputState,
action: SendAction.transactionInput
),
transaction: viewStore.bindingForTransaction,
isComplete: viewStore.bindingForConfirmation,
totalBalance: viewStore.bindingForBalance
)
.onAppear { viewStore.send(.onAppear) } .onAppear { viewStore.send(.onAppear) }
.onDisappear { viewStore.send(.onDisappear) } .onDisappear { viewStore.send(.onDisappear) }
.navigationLinkEmpty( .navigationLinkEmpty(
isActive: viewStore.bindingForConfirmation, isActive: viewStore.bindingForConfirmation,
destination: { destination: {
TransactionConfirmation(viewStore: viewStore) TransactionConfirmation(viewStore: viewStore)
.navigationLinkEmpty(
isActive: viewStore.bindingForSuccess,
destination: { TransactionSent(viewStore: viewStore) }
)
.navigationLinkEmpty(
isActive: viewStore.bindingForFailure,
destination: { TransactionFailed(viewStore: viewStore) }
)
} }
) )
} }
@ -43,7 +27,8 @@ struct SendView_Previews: PreviewProvider {
initialState: .init( initialState: .init(
route: nil, route: nil,
transaction: .placeholder, transaction: .placeholder,
transactionInputState: .placeholer transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
), ),
reducer: .default, reducer: .default,
environment: SendEnvironment( environment: SendEnvironment(

View File

@ -6,7 +6,7 @@ struct TransactionConfirmation: View {
var body: some View { var body: some View {
VStack { VStack {
Text("Send \(String(format: "%.7f", Int64(viewStore.transactionInputState.amount).asHumanReadableZecBalance())) ZEC") Text("Send \(viewStore.transaction.amount.asZecString()) ZEC")
.padding() .padding()
Text("To \(viewStore.transaction.toAddress)") Text("To \(viewStore.transaction.toAddress)")
@ -25,6 +25,14 @@ struct TransactionConfirmation: View {
Spacer() Spacer()
} }
.applyScreenBackground() .applyScreenBackground()
.navigationLinkEmpty(
isActive: viewStore.bindingForSuccess,
destination: { TransactionSent(viewStore: viewStore) }
)
.navigationLinkEmpty(
isActive: viewStore.bindingForFailure,
destination: { TransactionFailed(viewStore: viewStore) }
)
} }
} }

View File

@ -52,7 +52,7 @@ extension TransactionHistoryView {
Spacer() Spacer()
Text(transaction.status == .received ? "+" : "") Text(transaction.status == .received ? "+" : "")
+ Text("\(String(format: "%.7f", transaction.zecAmount.asHumanReadableZecBalance())) ZEC") + Text("\(transaction.zecAmount.asZecString()) ZEC")
} }
} }
.navigationLink( .navigationLink(

View File

@ -63,3 +63,5 @@
"Skip" = "Skip"; "Skip" = "Skip";
"Next" = "Next"; "Next" = "Next";
"Send" = "Send"; "Send" = "Send";
"Clear" = "Clear";
"Max" = "Max";

View File

@ -8,13 +8,14 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct SingleLineTextField<TitleAccessoryContent, InputAccessoryContent>: View struct SingleLineTextField<TitleAccessoryContent, InputPrefixContent, InputAccessoryContent>: View
where TitleAccessoryContent: View, InputAccessoryContent: View { where TitleAccessoryContent: View, InputPrefixContent: View, InputAccessoryContent: View {
let placeholderText: String let placeholderText: String
let title: String let title: String
let store: TextFieldStore let store: TextFieldStore
@ViewBuilder let titleAccessoryView: TitleAccessoryContent @ViewBuilder let titleAccessoryView: TitleAccessoryContent
@ViewBuilder let inputPrefixView: InputPrefixContent
@ViewBuilder let inputAccessoryView: InputAccessoryContent @ViewBuilder let inputAccessoryView: InputAccessoryContent
var body: some View { var body: some View {
@ -23,6 +24,7 @@ struct SingleLineTextField<TitleAccessoryContent, InputAccessoryContent>: View
Text(title) Text(title)
.lineLimit(1) .lineLimit(1)
.truncationMode(.middle) .truncationMode(.middle)
.font(.system(size: 13))
Spacer() Spacer()
@ -30,6 +32,8 @@ struct SingleLineTextField<TitleAccessoryContent, InputAccessoryContent>: View
} }
HStack { HStack {
inputPrefixView
TextFieldInput( TextFieldInput(
placeholder: placeholderText, placeholder: placeholderText,
store: store store: store
@ -67,6 +71,7 @@ struct SingleLineTextField_Previews: PreviewProvider {
) )
.textFieldTitleAccessoryButtonStyle .textFieldTitleAccessoryButtonStyle
}, },
inputPrefixView: { EmptyView() },
inputAccessoryView: { EmptyView() } inputAccessoryView: { EmptyView() }
) )
} }
@ -84,6 +89,7 @@ struct SingleLineTextField_Previews: PreviewProvider {
title: "Who would you like to deal with really long text today?", title: "Who would you like to deal with really long text today?",
store: store, store: store,
titleAccessoryView: { EmptyView() }, titleAccessoryView: { EmptyView() },
inputPrefixView: { EmptyView() },
inputAccessoryView: { EmptyView() } inputAccessoryView: { EmptyView() }
) )
} }

View File

@ -21,6 +21,8 @@ struct TextFieldInput: View {
set: { viewStore.send(.set($0)) } set: { viewStore.send(.set($0)) }
) )
) )
.autocapitalization(.none)
.font(.system(size: 13))
.lineLimit(1) .lineLimit(1)
.truncationMode(.middle) .truncationMode(.middle)
.accentColor(Asset.Colors.Cursor.bar.color) .accentColor(Asset.Colors.Cursor.bar.color)

View File

@ -22,7 +22,6 @@ struct TextFieldState: Equatable {
} }
enum TextFieldAction: Equatable { enum TextFieldAction: Equatable {
// case apply((String) -> String)
case set(String) case set(String)
} }
@ -31,9 +30,6 @@ struct TextFieldEnvironment: Equatable { }
extension TextFieldReducer { extension TextFieldReducer {
static let `default` = TextFieldReducer { state, action, _ in static let `default` = TextFieldReducer { state, action, _ in
switch action { switch action {
// case .apply(let action):
// state.text = action(state.text)
// state.valid = state.text.isValid(for: state.validationType)
case .set(let text): case .set(let text):
state.text = text state.text = text
state.valid = state.text.isValid(for: state.validationType) state.valid = state.text.isValid(for: state.validationType)
@ -65,4 +61,9 @@ extension TextFieldState {
validationType: nil, validationType: nil,
text: "" text: ""
) )
static let amount = TextFieldState(
validationType: .floatingPoint,
text: ""
)
} }

View File

@ -0,0 +1,79 @@
//
// TransactionAddressInputStore.swift
// secant-testnet
//
// Created by Lukáš Korba on 05/05/22.
//
import ComposableArchitecture
import SwiftUI
typealias TransactionAddressInputReducer = Reducer<
TransactionAddressInputState,
TransactionAddressInputAction,
TransactionAddressInputEnvironment
>
typealias TransactionAddressInputStore = Store<TransactionAddressInputState, TransactionAddressInputAction>
struct TransactionAddressInputState: Equatable {
var textFieldState: TextFieldState
var isValidAddress = false
}
enum TransactionAddressInputAction: Equatable {
case clearAddress
case textField(TextFieldAction)
}
struct TransactionAddressInputEnvironment {
let wrappedDerivationTool: WrappedDerivationTool
}
extension TransactionAddressInputReducer {
static let `default` = TransactionAddressInputReducer.combine(
[
addressReducer,
textFieldReducer
]
)
private static let addressReducer = TransactionAddressInputReducer { state, action, environment in
switch action {
case .clearAddress:
state.textFieldState.text = ""
return .none
case .textField(.set(let address)):
do {
state.isValidAddress = try environment.wrappedDerivationTool.isValidZcashAddress(address)
} catch {
state.isValidAddress = false
}
return .none
}
}
private static let textFieldReducer: TransactionAddressInputReducer = TextFieldReducer.default.pullback(
state: \TransactionAddressInputState.textFieldState,
action: /TransactionAddressInputAction.textField,
environment: { _ in return .init() }
)
}
extension TransactionAddressInputState {
static let placeholder = TransactionAddressInputState(
textFieldState: .placeholder
)
}
extension TransactionAddressInputStore {
static let placeholder = TransactionAddressInputStore(
initialState: .placeholder,
reducer: .default,
environment: TransactionAddressInputEnvironment(
wrappedDerivationTool: .live()
)
)
}

View File

@ -0,0 +1,68 @@
//
// TransactionAddressTextField.swift
// secant-testnet
//
// Created by Lukáš Korba on 05/05/22.
//
import SwiftUI
import ComposableArchitecture
struct TransactionAddressTextField: View {
let store: TransactionAddressInputStore
var body: some View {
WithViewStore(store) { viewStore in
VStack {
SingleLineTextField(
placeholderText: "address",
title: "To",
store: store.scope(
state: \.textFieldState,
action: TransactionAddressInputAction.textField
),
titleAccessoryView: {
if !viewStore.textFieldState.text.isEmpty {
Button(
action: {
viewStore.send(.clearAddress)
},
label: {
Text("Clear")
}
)
.textFieldTitleAccessoryButtonStyle
}
},
inputPrefixView: { EmptyView() },
inputAccessoryView: {
Image(Asset.Assets.Icons.qrCode.name)
.resizable()
.frame(width: 30, height: 30)
}
)
}
}
}
}
struct TransactionAddressTextField_Previews: PreviewProvider {
static var previews: some View {
TransactionAddressTextField(
store: TransactionAddressInputStore(
initialState: .init(
textFieldState: .init(
validationType: .floatingPoint,
text: ""
)
),
reducer: .default,
environment: .init(wrappedDerivationTool: .live())
)
)
.preferredColorScheme(.dark)
.padding(.horizontal, 50)
.applyScreenBackground()
.previewLayout(.fixed(width: 500, height: 200))
}
}

View File

@ -7,6 +7,8 @@
import ComposableArchitecture import ComposableArchitecture
// TODO: Reimplement this into multicurrency supporter, issue #315 (https://github.com/zcash/secant-ios-wallet/issues/315)
typealias CurrencySelectionReducer = Reducer< typealias CurrencySelectionReducer = Reducer<
CurrencySelectionState, CurrencySelectionState,
CurrencySelectionAction, CurrencySelectionAction,
@ -35,7 +37,7 @@ enum CurrencySelectionAction: Equatable {
case swapCurrencyType case swapCurrencyType
} }
struct CurrencySelectionEnvironment: Equatable { } struct CurrencySelectionEnvironment { }
extension CurrencySelectionReducer { extension CurrencySelectionReducer {
static var `default`: Self { static var `default`: Self {

View File

@ -0,0 +1,132 @@
//
// TransactionAmountInputStore.swift
// secant-testnet
//
// Created by Adam Stener on 4/5/22.
//
import ComposableArchitecture
typealias TransactionAmountInputReducer = Reducer<
TransactionAmountInputState,
TransactionAmountInputAction,
TransactionAmountInputEnvironment
>
typealias TransactionAmountInputStore = Store<TransactionAmountInputState, TransactionAmountInputAction>
struct TransactionAmountInputState: Equatable {
var textFieldState: TextFieldState
var currencySelectionState: CurrencySelectionState
var maxValue: Int64 = 0
// TODO: - Get the ZEC price from the SDK, issue 311, https://github.com/zcash/secant-ios-wallet/issues/311
var zecPrice = 140.0
var amount: Int64 {
switch currencySelectionState.currencyType {
case .zec:
return (textFieldState.text.doubleValue ?? 0.0).asZec()
case .usd:
return ((textFieldState.text.doubleValue ?? 0.0) / zecPrice).asZec()
}
}
var maxCurrencyConvertedValue: Int64 {
switch currencySelectionState.currencyType {
case .zec:
return maxValue
case .usd:
return (maxValue.asHumanReadableZecBalance() * zecPrice).asZec()
}
}
var isMax: Bool {
return amount == maxValue
}
}
enum TransactionAmountInputAction: Equatable {
case clearValue
case setMax
case textField(TextFieldAction)
case currencySelection(CurrencySelectionAction)
}
struct TransactionAmountInputEnvironment: Equatable {}
extension TransactionAmountInputReducer {
static let `default` = TransactionAmountInputReducer.combine(
[
textFieldReducer,
currencySelectionReducer,
maxOverride,
currencyUpdate
]
)
static let maxOverride = TransactionAmountInputReducer { state, action, _ in
switch action {
case .setMax:
state.textFieldState.text = "\(state.maxCurrencyConvertedValue.asZecString())"
case .clearValue:
state.textFieldState.text = ""
default: break
}
return .none
}
static let currencyUpdate = TransactionAmountInputReducer { state, action, _ in
switch action {
case .currencySelection:
guard let currentDoubleValue = state.textFieldState.text.doubleValue else {
return .none
}
let currencyType = state.currencySelectionState.currencyType
let newValue = currencyType == .zec ?
currentDoubleValue / state.zecPrice :
currentDoubleValue * state.zecPrice
state.textFieldState.text = "\(newValue.asZecString())"
default: break
}
return .none
}
private static let textFieldReducer: TransactionAmountInputReducer = TextFieldReducer.default.pullback(
state: \TransactionAmountInputState.textFieldState,
action: /TransactionAmountInputAction.textField,
environment: { _ in return .init() }
)
private static let currencySelectionReducer: TransactionAmountInputReducer = CurrencySelectionReducer.default.pullback(
state: \TransactionAmountInputState.currencySelectionState,
action: /TransactionAmountInputAction.currencySelection,
environment: { _ in return .init() }
)
}
extension TransactionAmountInputState {
static let placeholder = TransactionAmountInputState(
textFieldState: .placeholder,
currencySelectionState: CurrencySelectionState()
)
static let amount = TransactionAmountInputState(
textFieldState: .amount,
currencySelectionState: CurrencySelectionState()
)
}
extension TransactionAmountInputStore {
static let placeholder = TransactionAmountInputStore(
initialState: .placeholder,
reducer: .default,
environment: TransactionAmountInputEnvironment()
)
}

View File

@ -1,5 +1,5 @@
// //
// TransactionTextField.swift // TransactionAmountTextField.swift
// secant-testnet // secant-testnet
// //
// Created by Adam Stener on 4/4/22. // Created by Adam Stener on 4/4/22.
@ -8,38 +8,43 @@
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct TransactionTextField: View { struct TransactionAmountTextField: View {
let store: TransactionInputStore let store: TransactionAmountInputStore
// Constant example used here, this could be injected by a dependency
// Access to this value could also be injected into the store as a dependency
// with a function to prouce this value.
let maxTransactionValue: Int64 = 500
var body: some View { var body: some View {
WithViewStore(store) { viewStore in WithViewStore(store) { viewStore in
VStack { VStack {
SingleLineTextField( SingleLineTextField(
placeholderText: "$0", placeholderText: "0",
title: "How much?", title: "How much ZEC would you like to send?",
store: store.scope( store: store.scope(
state: \.textFieldState, state: \.textFieldState,
action: TransactionInputAction.textField action: TransactionAmountInputAction.textField
), ),
titleAccessoryView: { titleAccessoryView: {
Button( Button(
action: { action: {
viewStore.send(.setMax(maxTransactionValue)) viewStore.send(viewStore.isMax ? .clearValue : .setMax)
}, },
label: { Text("Max") } label: {
Text(viewStore.isMax ? "Clear" : "Max")
}
) )
.textFieldTitleAccessoryButtonStyle .textFieldTitleAccessoryButtonStyle
}, },
inputPrefixView: {
if viewStore.currencySelectionState.currencyType == .zec {
ZcashSymbol()
.frame(width: 12, height: 12, alignment: .center)
} else {
Text("$")
}
},
inputAccessoryView: { inputAccessoryView: {
TransactionCurrencySelector( TransactionCurrencySelector(
store: store.scope( store: store.scope(
state: \.currencySelectionState, state: \.currencySelectionState,
action: TransactionInputAction.currencySelection action: TransactionAmountInputAction.currencySelection
) )
) )
} }
@ -49,10 +54,10 @@ struct TransactionTextField: View {
} }
} }
struct TransactionTextField_Previews: PreviewProvider { struct TransactionAmountTextField_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
TransactionTextField( TransactionAmountTextField(
store: TransactionInputStore( store: TransactionAmountInputStore(
initialState: .init( initialState: .init(
textFieldState: .init( textFieldState: .init(
validationType: .floatingPoint, validationType: .floatingPoint,
@ -80,8 +85,8 @@ struct TransactionTextField_Previews: PreviewProvider {
) )
.textFieldTitleAccessoryButtonStyle .textFieldTitleAccessoryButtonStyle
}, },
inputAccessoryView: { inputPrefixView: { EmptyView() },
} inputAccessoryView: { EmptyView() }
) )
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
.padding(.horizontal, 50) .padding(.horizontal, 50)
@ -92,8 +97,8 @@ struct TransactionTextField_Previews: PreviewProvider {
placeholderText: "", placeholderText: "",
title: "Address", title: "Address",
store: .address, store: .address,
titleAccessoryView: { titleAccessoryView: { EmptyView() },
}, inputPrefixView: { EmptyView() },
inputAccessoryView: { inputAccessoryView: {
Image(Asset.Assets.Icons.qrCode.name) Image(Asset.Assets.Icons.qrCode.name)
.resizable() .resizable()

View File

@ -1,107 +0,0 @@
//
// TransactionInputStore.swift
// secant-testnet
//
// Created by Adam Stener on 4/5/22.
//
import ComposableArchitecture
typealias TransactionInputReducer = Reducer<
TransactionInputState,
TransactionInputAction,
TransactionInputEnvironment
>
typealias TransactionReducerData = (inout TransactionInputState, TransactionInputAction) -> Void
typealias TransactionInputStore = Store<TransactionInputState, TransactionInputAction>
struct TransactionInputState: Equatable {
var textFieldState: TextFieldState
var currencySelectionState: CurrencySelectionState
var maxValue: Int64 = 0
var amount: Int64 {
Int64((Double(textFieldState.text) ?? 0.0) * 100_000_000)
}
}
enum TransactionInputAction: Equatable {
case setMax(Int64)
case textField(TextFieldAction)
case currencySelection(CurrencySelectionAction)
}
struct TransactionInputEnvironment: Equatable {}
extension TransactionInputReducer {
static let `default` = TransactionInputReducer.combine(
[
textFieldReducer,
currencySelectionReducer,
maxOverride,
currencyUpdate
]
)
static let maxOverride = TransactionInputReducer { state, action, _ in
switch action {
case .setMax(let value):
state.currencySelectionState.currencyType = .zec
state.textFieldState.text = "\(value.asHumanReadableZecBalance())"
default: break
}
return .none
}
static let currencyUpdate = TransactionInputReducer { state, action, _ in
switch action {
case .currencySelection:
guard let currentDoubleValue = Double(state.textFieldState.text) else {
return .none
}
let currencyType = state.currencySelectionState.currencyType
// The 2100 is another hard coded value (🚀🌒) but the store could
// have a dependency injected that would be responsible for
// providing the current exchange rate.
let newValue = currencyType == .zec ?
currentDoubleValue / 2100 :
currentDoubleValue * 2100
state.textFieldState.text = "\(newValue)"
default: break
}
return .none
}
private static let textFieldReducer: TransactionInputReducer = TextFieldReducer.default.pullback(
state: \TransactionInputState.textFieldState,
action: /TransactionInputAction.textField,
environment: { _ in return .init() }
)
private static let currencySelectionReducer: TransactionInputReducer = CurrencySelectionReducer.default.pullback(
state: \TransactionInputState.currencySelectionState,
action: /TransactionInputAction.currencySelection,
environment: { _ in return .init() }
)
}
extension TransactionInputState {
static let placeholer = TransactionInputState(
textFieldState: .placeholder,
currencySelectionState: CurrencySelectionState()
)
}
extension TransactionInputStore {
static let placeholder = TransactionInputStore(
initialState: .placeholer,
reducer: .default,
environment: TransactionInputEnvironment()
)
}

View File

@ -0,0 +1,19 @@
//
// Double+Zcash.swift
// secant-testnet
//
// Created by Lukáš Korba on 06.05.2022.
//
import Foundation
// TODO: Improve with decimals and zatoshi type, issue #272 (https://github.com/zcash/secant-ios-wallet/issues/272)
extension Double {
func asZec() -> Int64 {
return Int64((self * 100_000_000).rounded())
}
func asZecString() -> String {
NumberFormatter.zcashFormatter.string(from: NSNumber(value: self)) ?? ""
}
}

View File

@ -12,4 +12,8 @@ extension Int64 {
func asHumanReadableZecBalance() -> Double { func asHumanReadableZecBalance() -> Double {
Double(self) / Double(100_000_000) Double(self) / Double(100_000_000)
} }
func asZecString() -> String {
NumberFormatter.zcashFormatter.string(from: NSNumber(value: self.asHumanReadableZecBalance())) ?? ""
}
} }

View File

@ -9,9 +9,24 @@ extension String {
} }
} }
extension NumberFormatter {
static let zcashFormatter: NumberFormatter = {
var formatter = NumberFormatter()
formatter.maximumFractionDigits = 8
formatter.maximumIntegerDigits = 8
formatter.numberStyle = .decimal
formatter.usesGroupingSeparator = true
return formatter
}()
}
extension String {
var doubleValue: Double? {
return NumberFormatter.zcashFormatter.number(from: self)?.doubleValue
}
}
extension String { extension String {
// TODO: Issue #245 Add Validation Regex that support localization
private static let floatingPointRegex = "^[0-9]*.?[0-9]+"
private static let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}" private static let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"
private static let phoneRegex = "^^\\+(?:[0-9]?){6,14}[0-9]$" private static let phoneRegex = "^^\\+(?:[0-9]?){6,14}[0-9]$"
@ -32,7 +47,7 @@ extension String {
return text.validate(using: .emailRegex) return text.validate(using: .emailRegex)
case .floatingPoint: case .floatingPoint:
return text.validate(using: .floatingPointRegex) return text.doubleValue != nil
case .maxLength(let length): case .maxLength(let length):
return text.count <= length && !text.isEmpty return text.count <= length && !text.isEmpty

View File

@ -124,6 +124,11 @@ struct WrappedDerivationTool {
Checks validity of the shielded address. Checks validity of the shielded address.
*/ */
let isValidShieldedAddress: (String) throws -> Bool let isValidShieldedAddress: (String) throws -> Bool
/**
Checks if given address is a valid zcash address.
*/
let isValidZcashAddress: (String) throws -> Bool
} }
extension WrappedDerivationTool { extension WrappedDerivationTool {
@ -170,6 +175,10 @@ extension WrappedDerivationTool {
}, },
isValidShieldedAddress: { zAddress in isValidShieldedAddress: { zAddress in
try derivationTool.isValidShieldedAddress(zAddress) try derivationTool.isValidShieldedAddress(zAddress)
},
isValidZcashAddress: { address in
try derivationTool.isValidTransparentAddress(address) ? true :
try derivationTool.isValidShieldedAddress(address) ? true : false
} }
) )
} }

View File

@ -313,7 +313,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
mocked.map { mocked.map {
TransactionState.placeholder( TransactionState.placeholder(
date: Date.init(timeIntervalSince1970: $0.date), date: Date.init(timeIntervalSince1970: $0.date),
amount: $0.amount * 100000000, amount: $0.amount,
shielded: $0.shielded, shielded: $0.shielded,
status: $0.status, status: $0.status,
subtitle: $0.subtitle subtitle: $0.subtitle
@ -335,7 +335,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
mocked.map { mocked.map {
TransactionState.placeholder( TransactionState.placeholder(
date: Date.init(timeIntervalSince1970: $0.date), date: Date.init(timeIntervalSince1970: $0.date),
amount: $0.amount * 100000000, amount: $0.amount,
shielded: $0.shielded, shielded: $0.shielded,
status: $0.status, status: $0.status,
subtitle: $0.subtitle subtitle: $0.subtitle
@ -420,7 +420,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
mocked.map { mocked.map {
TransactionState.placeholder( TransactionState.placeholder(
date: Date.init(timeIntervalSince1970: $0.date), date: Date.init(timeIntervalSince1970: $0.date),
amount: $0.amount * 100000000, amount: $0.amount,
shielded: $0.shielded, shielded: $0.shielded,
status: $0.status, status: $0.status,
subtitle: $0.subtitle, subtitle: $0.subtitle,
@ -443,7 +443,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
mocked.map { mocked.map {
TransactionState.placeholder( TransactionState.placeholder(
date: Date.init(timeIntervalSince1970: $0.date), date: Date.init(timeIntervalSince1970: $0.date),
amount: $0.amount * 100000000, amount: $0.amount,
shielded: $0.shielded, shielded: $0.shielded,
status: $0.status, status: $0.status,
subtitle: $0.subtitle, subtitle: $0.subtitle,

View File

@ -0,0 +1,36 @@
//
// CurrencySelectionTests.swift
// secantTests
//
// Created by Lukáš Korba on 09.05.2022.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
class CurrencySelectionTests: XCTestCase {
func testCurrencySwapUsdToZec() throws {
let store = TestStore(
initialState: CurrencySelectionState(currencyType: .usd),
reducer: CurrencySelectionReducer.default,
environment: CurrencySelectionEnvironment()
)
store.send(.swapCurrencyType) { state in
state.currencyType = .zec
}
}
func testCurrencySwapZecToUsd() throws {
let store = TestStore(
initialState: CurrencySelectionState(currencyType: .zec),
reducer: CurrencySelectionReducer.default,
environment: CurrencySelectionEnvironment()
)
store.send(.swapCurrencyType) { state in
state.currencyType = .usd
}
}
}

View File

@ -10,6 +10,11 @@ import XCTest
import ComposableArchitecture import ComposableArchitecture
import ZcashLightClientKit import ZcashLightClientKit
// TODO: these tests will be updated with the Zatoshi/Balance representative once done, issue #272 https://github.com/zcash/secant-ios-wallet/issues/272
// TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312)
// swiftlint:disable type_body_length
class SendTests: XCTestCase { class SendTests: XCTestCase {
var storage = WalletStorage(secItem: .live) var storage = WalletStorage(secItem: .live)
@ -25,7 +30,7 @@ class SendTests: XCTestCase {
// setup the store and environment to be fully mocked // setup the store and environment to be fully mocked
let testScheduler = DispatchQueue.test let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment( let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock, mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(), scheduler: testScheduler.eraseToAnyScheduler(),
@ -81,7 +86,7 @@ class SendTests: XCTestCase {
// setup the store and environment to be fully mocked // setup the store and environment to be fully mocked
let testScheduler = DispatchQueue.test let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment( let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock, mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(), scheduler: testScheduler.eraseToAnyScheduler(),
@ -117,4 +122,389 @@ class SendTests: XCTestCase {
state.route = .failure state.route = .failure
} }
} }
func testAddressValidation() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: .placeholder,
reducer: SendReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr")))) { state in
state.transactionAddressInputState.textFieldState.text = "3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = false
XCTAssertTrue(
state.isInvalidAddressFormat,
"Send Tests: `testAddressValidation` is expected to be true but it's \(state.isInvalidAddressFormat)"
)
}
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertFalse(
state.isInvalidAddressFormat,
"Send Tests: `testAddressValidation` is expected to be false but it's \(state.isInvalidAddressFormat)"
)
}
}
func testInvalidAmountFormatEmptyInput() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: .placeholder,
reducer: SendReducer.default,
environment: testEnvironment
)
// Checks the computed property `isInvalidAmountFormat` which controls the error message to be shown on the screen
// With empty input it must be false
store.send(.transactionAmountInput(.textField(.set("")))) { state in
state.transactionAmountInputState.textFieldState.text = ""
state.transactionAmountInputState.textFieldState.valid = false
XCTAssertFalse(
state.isInvalidAmountFormat,
"Send Tests: `testInvalidAmountFormatEmptyInput` is expected to be false but it's \(state.isInvalidAmountFormat)"
)
}
}
func testInvalidAddressFormatEmptyInput() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: .placeholder,
reducer: SendReducer.default,
environment: testEnvironment
)
// Checks the computed property `isInvalidAddressFormat` which controls the error message to be shown on the screen
// With empty input it must be false
store.send(.transactionAddressInput(.textField(.set("")))) { state in
state.transactionAddressInputState.textFieldState.text = ""
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
XCTAssertFalse(
state.isInvalidAddressFormat,
"Send Tests: `testInvalidAddressFormatEmptyInput` is expected to be false but it's \(state.isInvalidAddressFormat)"
)
}
}
func testFundsSufficiency() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testFundsSufficiency is designed to test US locale only")
let sendState = SendState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountInputState(
textFieldState: .amount,
currencySelectionState: CurrencySelectionState(),
maxValue: 501_300
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: sendState,
reducer: SendReducer.default,
environment: testEnvironment
)
store.send(.transactionAmountInput(.textField(.set("0.00501299")))) { state in
state.transactionAmountInputState.textFieldState.text = "0.00501299"
state.transactionAmountInputState.textFieldState.valid = true
XCTAssertFalse(
state.isInsufficientFunds,
"Send Tests: `testFundsSufficiency` is expected to be false but it's \(state.isInsufficientFunds)"
)
}
store.send(.transactionAmountInput(.textField(.set("0.00501301")))) { state in
state.transactionAmountInputState.textFieldState.text = "0.00501301"
state.transactionAmountInputState.textFieldState.valid = true
XCTAssertTrue(
state.isInsufficientFunds,
"Send Tests: `testFundsSufficiency` is expected to be true but it's \(state.isInsufficientFunds)"
)
}
}
func testDifferentAmountFormats() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testDifferentAmountFormats is designed to test US locale only")
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: .placeholder,
reducer: SendReducer.default,
environment: testEnvironment
)
try amountFormatTest("1.234", true, 123_400_000, store)
try amountFormatTest("1,234", true, 123_400_000_000, store)
try amountFormatTest("1 234", true, 123_400_000_000, store)
try amountFormatTest("1,234.567", true, 123_456_700_000, store)
try amountFormatTest("1.", true, 100_000_000, store)
try amountFormatTest("1..", false, 0, store)
try amountFormatTest("1,.", false, 0, store)
try amountFormatTest("1.,", false, 0, store)
try amountFormatTest("1,,", false, 0, store)
try amountFormatTest("1,23", false, 0, store)
try amountFormatTest("1 23", false, 0, store)
try amountFormatTest("1.2.3", false, 0, store)
}
func testValidForm() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testValidForm is designed to test US locale only")
let sendState = SendState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "0.00501301"
),
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: sendState,
reducer: SendReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertTrue(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be true but it's \(state.isValidForm)"
)
}
}
func testInvalidForm_InsufficientFunds() throws {
let sendState = SendState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "0.00501301"
),
currencySelectionState: CurrencySelectionState(),
maxValue: 501_300
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: sendState,
reducer: SendReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
func testInvalidForm_AddressFormat() throws {
let sendState = SendState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "0.00501301"
),
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: sendState,
reducer: SendReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr")))) { state in
state.transactionAddressInputState.textFieldState.text = "3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = false
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
func testInvalidForm_AmountFormat() throws {
let sendState = SendState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "0.0.0501301"
),
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendEnvironment(
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live)),
wrappedDerivationTool: .live(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
let store = TestStore(
initialState: sendState,
reducer: SendReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
}
private extension SendTests {
func amountFormatTest(
_ amount: String,
_ expectedValidationResult: Bool,
_ expectedAmount: Int64,
_ store: TestStore<SendState, SendState, SendAction, SendAction, SendEnvironment>
) throws {
store.send(.transactionAmountInput(.textField(.set(amount)))) { state in
state.transactionAmountInputState.textFieldState.text = amount
state.transactionAmountInputState.textFieldState.valid = expectedValidationResult
XCTAssertEqual(
expectedAmount,
state.transactionAmountInputState.amount,
"Send Tests: `amountFormatTest` expected amount is \(expectedAmount) but result is \(state.isInvalidAddressFormat)"
)
}
}
} }

View File

@ -0,0 +1,34 @@
//
// TransactionAddressInputTests.swift
// secantTests
//
// Created by Lukáš Korba on 06.05.2022.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
class TransactionAddressInputTests: XCTestCase {
func testClearValue() throws {
let store = TestStore(
initialState:
TransactionAddressInputState(
textFieldState:
TextFieldState(
validationType: nil,
text: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
)
),
reducer: TransactionAddressInputReducer.default,
environment:
TransactionAddressInputEnvironment(
wrappedDerivationTool: .live()
)
)
store.send(.clearAddress) { state in
state.textFieldState.text = ""
}
}
}

View File

@ -0,0 +1,223 @@
//
// TransactionAmountInputTests.swift
// secantTests
//
// Created by Lukáš Korba on 06.05.2022.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
// TODO: these tests will be updated with the Zatoshi/Balance representative once done, issue #272 https://github.com/zcash/secant-ios-wallet/issues/272
// TODO: these test will be updated with the NumberFormater dependency to handle locale, issue #312 (https://github.com/zcash/secant-ios-wallet/issues/312)
class TransactionAmountInputTests: XCTestCase {
func testMaxValue() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testMaxValue is designed to test US locale only")
let store = TestStore(
initialState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "0.002"
),
currencySelectionState: CurrencySelectionState(),
maxValue: 501_301
),
reducer: TransactionAmountInputReducer.default,
environment: TransactionAmountInputEnvironment()
)
store.send(.setMax) { state in
state.textFieldState.text = "0.00501301"
XCTAssertEqual(501_301, state.amount, "AmountInput Tests: `testMaxValue` expected \(501_301) but received \(state.amount)")
}
}
func testClearValue() throws {
let store = TestStore(
initialState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "0.002"
),
currencySelectionState: CurrencySelectionState(),
maxValue: 501_301
),
reducer: TransactionAmountInputReducer.default,
environment: TransactionAmountInputEnvironment()
)
store.send(.clearValue) { state in
state.textFieldState.text = ""
XCTAssertEqual(0, state.amount, "AmountInput Tests: `testClearValue` expected \(0) but received \(state.amount)")
}
}
func testZecUsdConvertedAmount() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testZecUsdConvertedAmount is designed to test US locale only")
let store = TestStore(
initialState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "1.0"
),
currencySelectionState:
CurrencySelectionState(
currencyType: .zec
),
zecPrice: 1000.0
),
reducer: TransactionAmountInputReducer.default,
environment: TransactionAmountInputEnvironment()
)
store.send(.currencySelection(.swapCurrencyType)) { state in
state.textFieldState.text = "1,000"
state.currencySelectionState.currencyType = .usd
XCTAssertEqual(
100_000_000,
state.amount,
"AmountInput Tests: `testZecUsdConvertedAmount` expected \(100_000_000) but received \(state.amount)"
)
}
}
func testUsdZecConvertedAmount() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testUsdZecConvertedAmount is designed to test US locale only")
let store = TestStore(
initialState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "1 000"
),
currencySelectionState:
CurrencySelectionState(
currencyType: .usd
),
zecPrice: 1000.0
),
reducer: TransactionAmountInputReducer.default,
environment: TransactionAmountInputEnvironment()
)
store.send(.currencySelection(.swapCurrencyType)) { state in
state.textFieldState.text = "1"
state.currencySelectionState.currencyType = .zec
XCTAssertEqual(
100_000_000,
state.amount,
"AmountInput Tests: `testZecUsdConvertedAmount` expected \(100_000_000) but received \(state.amount)"
)
}
}
func testIfAmountIsMax() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testIfAmountIsMax is designed to test US locale only")
let store = TestStore(
initialState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "5"
),
currencySelectionState:
CurrencySelectionState(
currencyType: .usd
),
maxValue: 100_000_000,
zecPrice: 1000.0
),
reducer: TransactionAmountInputReducer.default,
environment: TransactionAmountInputEnvironment()
)
store.send(.textField(.set("1 000"))) { state in
state.textFieldState.text = "1 000"
state.textFieldState.valid = true
state.currencySelectionState.currencyType = .usd
XCTAssertTrue(
state.isMax,
"AmountInput Tests: `testIfAmountIsMax` is expected to be true but it's \(state.isMax)"
)
}
}
func testMaxZecValue() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testMaxZecValue is designed to test US locale only")
let store = TestStore(
initialState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "5"
),
currencySelectionState:
CurrencySelectionState(
currencyType: .zec
),
maxValue: 200_000_000,
zecPrice: 1000.0
),
reducer: TransactionAmountInputReducer.default,
environment: TransactionAmountInputEnvironment()
)
store.send(.setMax) { state in
state.textFieldState.text = "2"
XCTAssertEqual(
200_000_000,
state.maxCurrencyConvertedValue,
"AmountInput Tests: `testMaxZecValue` expected \(200_000_000) but received \(state.maxCurrencyConvertedValue)"
)
}
}
func testMaxUsdValue() throws {
try XCTSkipUnless(Locale.current.regionCode == "US", "testMaxUsdValue is designed to test US locale only")
let store = TestStore(
initialState:
TransactionAmountInputState(
textFieldState:
TextFieldState(
validationType: .floatingPoint,
text: "5"
),
currencySelectionState:
CurrencySelectionState(
currencyType: .usd
),
maxValue: 200_000_000,
zecPrice: 1000.0
),
reducer: TransactionAmountInputReducer.default,
environment: TransactionAmountInputEnvironment()
)
store.send(.setMax) { state in
state.textFieldState.text = "2,000"
XCTAssertEqual(
200_000_000_000,
state.maxCurrencyConvertedValue,
"AmountInput Tests: `testMaxUsdValue` expected \(200_000_000_000) but received \(state.maxCurrencyConvertedValue)"
)
}
}
}

View File

@ -52,7 +52,7 @@ class TransactionHistoryTests: XCTestCase {
let transactions = mocked.map { let transactions = mocked.map {
TransactionState.placeholder( TransactionState.placeholder(
date: Date.init(timeIntervalSince1970: $0.date), date: Date.init(timeIntervalSince1970: $0.date),
amount: $0.amount * 100000000, amount: $0.amount,
shielded: $0.shielded, shielded: $0.shielded,
status: $0.status, status: $0.status,
subtitle: $0.subtitle, subtitle: $0.subtitle,