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:
parent
10070c3c02
commit
2bb5451047
|
@ -59,13 +59,13 @@
|
|||
2E35F99A27B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E35F99927B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift */; };
|
||||
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.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 */; };
|
||||
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; };
|
||||
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; };
|
||||
2EB1C5E827D77F6100BC43D7 /* TextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB1C5E727D77F6100BC43D7 /* TextFieldStore.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 */; };
|
||||
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.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 */; };
|
||||
9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.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 */; };
|
||||
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.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 */; };
|
||||
9EBEF87A27CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.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 */; };
|
||||
9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -276,6 +282,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -287,6 +295,10 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -577,6 +589,7 @@
|
|||
9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */,
|
||||
9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */,
|
||||
9E2F1C8128095AFE004E65FE /* Int64+Zcash.swift */,
|
||||
9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */,
|
||||
9E2F1C832809B606004E65FE /* DebugMenu.swift */,
|
||||
);
|
||||
path = Util;
|
||||
|
@ -630,10 +643,8 @@
|
|||
2E35F99027B28E6800EB79CD /* TextFields */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2E8719CA27FB09990082C926 /* TransactionTextField.swift */,
|
||||
2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */,
|
||||
2EB7758627FC67FD00269373 /* TransactionInputStore.swift */,
|
||||
2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */,
|
||||
9E5BF64C2823E84300BA3F17 /* TransactionAddress */,
|
||||
9E5BF64B2823C91200BA3F17 /* TransactionAmount */,
|
||||
2EDA07A527EDE31100D6F09B /* Components */,
|
||||
);
|
||||
path = TextFields;
|
||||
|
@ -653,11 +664,11 @@
|
|||
2EDA07A527EDE31100D6F09B /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */,
|
||||
2E35F99127B28E7600EB79CD /* SingleLineTextField.swift */,
|
||||
2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */,
|
||||
2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */,
|
||||
2E35F99927B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift */,
|
||||
2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
|
@ -806,11 +817,34 @@
|
|||
9E5BF642281FEC8700BA3F17 /* SendTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */,
|
||||
9E5BF643281FEC9900BA3F17 /* SendTests.swift */,
|
||||
9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */,
|
||||
9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */,
|
||||
);
|
||||
path = SendTests;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1224,7 +1258,7 @@
|
|||
0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */,
|
||||
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
|
||||
2EDA07A027EDE18C00D6F09B /* TextFieldInput.swift in Sources */,
|
||||
2EB7758727FC67FD00269373 /* TransactionInputStore.swift in Sources */,
|
||||
2EB7758727FC67FD00269373 /* TransactionAmountInputStore.swift in Sources */,
|
||||
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
|
||||
9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */,
|
||||
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */,
|
||||
|
@ -1254,6 +1288,7 @@
|
|||
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
|
||||
2E8719CD27FB0D3B0082C926 /* TransactionCurrencySelector.swift in Sources */,
|
||||
F9971A6C27680E1000A2DB75 /* WalletInfoView.swift in Sources */,
|
||||
9E5BF6502823E94900BA3F17 /* TransactionAddressInputStore.swift in Sources */,
|
||||
F9EEB8162742C2210032EEB8 /* WithStateBinding.swift in Sources */,
|
||||
F93673D62742CB840099C6AF /* Previews.swift in Sources */,
|
||||
0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */,
|
||||
|
@ -1294,12 +1329,14 @@
|
|||
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
|
||||
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */,
|
||||
9E2AC10327DA28200042AA47 /* WalletStorage.swift in Sources */,
|
||||
9EDDEA8C28250F9C00B4100C /* Double+Zcash.swift in Sources */,
|
||||
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */,
|
||||
9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */,
|
||||
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
|
||||
0D185819272723FF0046B928 /* ColoredChip.swift in Sources */,
|
||||
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
|
||||
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */,
|
||||
9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */,
|
||||
2E35F99227B28E7600EB79CD /* SingleLineTextField.swift in Sources */,
|
||||
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */,
|
||||
66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */,
|
||||
|
@ -1322,7 +1359,7 @@
|
|||
F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */,
|
||||
9EF8139C27F47AED0075AF48 /* InitializationState.swift in Sources */,
|
||||
0D0781C9278776D20083ACD7 /* ZcashSymbol.swift in Sources */,
|
||||
2E8719CB27FB09990082C926 /* TransactionTextField.swift in Sources */,
|
||||
2E8719CB27FB09990082C926 /* TransactionAmountTextField.swift in Sources */,
|
||||
6654C7412715A47300901167 /* Onboarding.swift in Sources */,
|
||||
F9C165C42740403600592F76 /* TransactionSentView.swift in Sources */,
|
||||
F9971A5927680DDE00A2DB75 /* RequestStore.swift in Sources */,
|
||||
|
@ -1334,7 +1371,10 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
|
||||
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */,
|
||||
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */,
|
||||
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
|
||||
9EDDEAA32829610D00B4100C /* TransactionAmountInputTests.swift in Sources */,
|
||||
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */,
|
||||
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
|
||||
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
|
||||
|
|
|
@ -1,184 +1,187 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||
"state" : {
|
||||
"revision" : "4cf088c29a20f52be0f2ca54992b492c54e0076b",
|
||||
"version" : "0.5.3"
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "combine-schedulers",
|
||||
"repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"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",
|
||||
"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
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ struct HomeState: Equatable {
|
|||
var sendState: SendState
|
||||
var scanState: ScanState
|
||||
var synchronizerStatus: String
|
||||
var totalBalance: Double
|
||||
var totalBalance: Int64
|
||||
var transactionHistoryState: TransactionHistoryState
|
||||
var verifiedBalance: Double
|
||||
var verifiedBalance: Int64
|
||||
}
|
||||
|
||||
enum HomeAction: Equatable {
|
||||
|
@ -95,8 +95,8 @@ extension HomeReducer {
|
|||
return Effect(value: .updateSynchronizerStatus)
|
||||
|
||||
case .updateBalance(let balance):
|
||||
state.totalBalance = balance.total.asHumanReadableZecBalance()
|
||||
state.verifiedBalance = balance.verified.asHumanReadableZecBalance()
|
||||
state.totalBalance = balance.total
|
||||
state.verifiedBalance = balance.verified
|
||||
return .none
|
||||
|
||||
case .updateDrawer(let drawerOverlay):
|
||||
|
@ -244,9 +244,9 @@ extension HomeState {
|
|||
sendState: .placeholder,
|
||||
scanState: .placeholder,
|
||||
synchronizerStatus: "",
|
||||
totalBalance: 0.0,
|
||||
totalBalance: 0,
|
||||
transactionHistoryState: .emptyPlaceHolder,
|
||||
verifiedBalance: 0.0
|
||||
verifiedBalance: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ struct HomeView: View {
|
|||
Text("\(viewStore.synchronizerStatus)")
|
||||
.padding(.top, 60)
|
||||
|
||||
Text("balance: \(viewStore.totalBalance)")
|
||||
Text("balance \(viewStore.totalBalance.asZecString()) ZEC")
|
||||
.accessDebugMenuWithHiddenGesture {
|
||||
viewStore.send(.debugMenuStartup)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ struct ImportSeedEditor: View {
|
|||
var body: some View {
|
||||
WithViewStore(store) { viewStore in
|
||||
TextEditor(text: viewStore.binding(\.$importedSeedPhrase))
|
||||
.autocapitalization(.none)
|
||||
.importSeedEditorModifier()
|
||||
.padding(28)
|
||||
}
|
||||
|
@ -27,11 +28,13 @@ struct ImportSeedEditor: View {
|
|||
}
|
||||
|
||||
struct ImportSeedEditorModifier: ViewModifier {
|
||||
var backgroundColor = Color.white
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.foregroundColor(Asset.Colors.Text.importSeedEditor.color)
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.background(backgroundColor)
|
||||
.cornerRadius(4)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
|
@ -41,8 +44,8 @@ struct ImportSeedEditorModifier: ViewModifier {
|
|||
}
|
||||
|
||||
extension View {
|
||||
func importSeedEditorModifier() -> some View {
|
||||
modifier(ImportSeedEditorModifier())
|
||||
func importSeedEditorModifier(_ backgroundColor: Color = .white) -> some View {
|
||||
modifier(ImportSeedEditorModifier(backgroundColor: backgroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,6 @@ struct Transaction: Equatable {
|
|||
var amount: Int64
|
||||
var memo: 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 {
|
||||
|
@ -25,8 +20,7 @@ extension Transaction {
|
|||
|
||||
struct SendState: Equatable {
|
||||
enum Route: Equatable {
|
||||
case showConfirmation
|
||||
case showSent
|
||||
case confirmation
|
||||
case success
|
||||
case failure
|
||||
case done
|
||||
|
@ -35,9 +29,35 @@ struct SendState: Equatable {
|
|||
var route: Route?
|
||||
|
||||
var isSendingTransaction = false
|
||||
var totalBalance = 0.0
|
||||
var memo = ""
|
||||
var totalBalance: Int64 = 0
|
||||
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 {
|
||||
|
@ -46,8 +66,10 @@ enum SendAction: Equatable {
|
|||
case sendConfirmationPressed
|
||||
case sendTransactionResult(Result<TransactionState, NSError>)
|
||||
case synchronizerStateChanged(WrappedSDKSynchronizerState)
|
||||
case transactionInput(TransactionInputAction)
|
||||
case updateBalance(Double)
|
||||
case transactionAddressInput(TransactionAddressInputAction)
|
||||
case transactionAmountInput(TransactionAmountInputAction)
|
||||
case updateBalance(Int64)
|
||||
case updateMemo(String)
|
||||
case updateTransaction(Transaction)
|
||||
case updateRoute(SendState.Route?)
|
||||
}
|
||||
|
@ -71,9 +93,9 @@ extension SendReducer {
|
|||
|
||||
static let `default` = SendReducer.combine(
|
||||
[
|
||||
balanceReducer,
|
||||
sendReducer,
|
||||
transactionInputReducer
|
||||
transactionAddressInputReducer,
|
||||
transactionAmountInputReducer
|
||||
]
|
||||
)
|
||||
.debug()
|
||||
|
@ -89,6 +111,11 @@ extension SendReducer {
|
|||
state.isSendingTransaction = false
|
||||
return .none
|
||||
|
||||
case .updateRoute(.confirmation):
|
||||
state.transaction.amount = state.transactionAmountInputState.amount
|
||||
state.transaction.toAddress = state.transactionAddressInputState.textFieldState.text
|
||||
return .none
|
||||
|
||||
case let .updateRoute(route):
|
||||
state.route = route
|
||||
return .none
|
||||
|
@ -129,16 +156,13 @@ extension SendReducer {
|
|||
} catch {
|
||||
return Effect(value: .updateRoute(.failure))
|
||||
}
|
||||
case .transactionInput(let transactionInput):
|
||||
return .none
|
||||
|
||||
default:
|
||||
case .transactionAmountInput(let transactionInput):
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
private static let balanceReducer = SendReducer { state, action, environment in
|
||||
switch action {
|
||||
|
||||
case .transactionAddressInput(let transactionInput):
|
||||
return .none
|
||||
|
||||
case .onAppear:
|
||||
return environment.wrappedSDKSynchronizer.stateChanged
|
||||
.map(SendAction.synchronizerStateChanged)
|
||||
|
@ -151,7 +175,7 @@ extension SendReducer {
|
|||
case .synchronizerStateChanged(.synced):
|
||||
return environment.wrappedSDKSynchronizer.getShieldedBalance()
|
||||
.receive(on: environment.scheduler)
|
||||
.map({ Double($0.total) / Double(100_000_000) })
|
||||
.map({ $0.total })
|
||||
.map(SendAction.updateBalance)
|
||||
.eraseToEffect()
|
||||
|
||||
|
@ -160,18 +184,29 @@ extension SendReducer {
|
|||
|
||||
case .updateBalance(let balance):
|
||||
state.totalBalance = balance
|
||||
state.transactionInputState.maxValue = Int64(balance * 100_000_000)
|
||||
state.transactionAmountInputState.maxValue = balance
|
||||
return .none
|
||||
|
||||
default:
|
||||
|
||||
case .updateMemo(let memo):
|
||||
state.memo = memo
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
private static let transactionInputReducer: SendReducer = TransactionInputReducer.default.pullback(
|
||||
state: \SendState.transactionInputState,
|
||||
action: /SendAction.transactionInput,
|
||||
environment: { _ in TransactionInputEnvironment() }
|
||||
private static let transactionAddressInputReducer: SendReducer = TransactionAddressInputReducer.default.pullback(
|
||||
state: \SendState.transactionAddressInputState,
|
||||
action: /SendAction.transactionAddressInput,
|
||||
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 {
|
||||
|
@ -211,36 +246,36 @@ extension SendViewStore {
|
|||
|
||||
var bindingForConfirmation: Binding<Bool> {
|
||||
self.routeBinding.map(
|
||||
extract: { $0 == .showConfirmation || self.bindingForSuccess.wrappedValue || self.bindingForFailure.wrappedValue },
|
||||
embed: { $0 ? SendState.Route.showConfirmation : nil }
|
||||
extract: { $0 == .confirmation || self.bindingForSuccess.wrappedValue || self.bindingForFailure.wrappedValue },
|
||||
embed: { $0 ? SendState.Route.confirmation : nil }
|
||||
)
|
||||
}
|
||||
|
||||
var bindingForSuccess: Binding<Bool> {
|
||||
self.routeBinding.map(
|
||||
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> {
|
||||
self.routeBinding.map(
|
||||
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> {
|
||||
self.routeBinding.map(
|
||||
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(
|
||||
get: \.totalBalance,
|
||||
send: SendAction.updateBalance
|
||||
get: \.memo,
|
||||
send: SendAction.updateMemo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -252,7 +287,8 @@ extension SendState {
|
|||
.init(
|
||||
route: nil,
|
||||
transaction: .placeholder,
|
||||
transactionInputState: .placeholer
|
||||
transactionAddressInputState: .placeholder,
|
||||
transactionAmountInputState: .amount
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -264,7 +300,8 @@ extension SendState {
|
|||
memo: "",
|
||||
toAddress: ""
|
||||
),
|
||||
transactionInputState: .placeholer
|
||||
transactionAddressInputState: .placeholder,
|
||||
transactionAmountInputState: .placeholder
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,73 +2,89 @@ import SwiftUI
|
|||
import ComposableArchitecture
|
||||
|
||||
struct CreateTransaction: View {
|
||||
let store: TransactionInputStore
|
||||
|
||||
@Binding var transaction: Transaction
|
||||
@Binding var isComplete: Bool
|
||||
@Binding var totalBalance: Double
|
||||
let store: SendStore
|
||||
|
||||
var body: some View {
|
||||
UITextView.appearance().backgroundColor = .clear
|
||||
|
||||
return WithViewStore(store) { viewStore in
|
||||
VStack {
|
||||
VStack {
|
||||
Text("Balance \(totalBalance)")
|
||||
|
||||
SingleLineTextField(
|
||||
placeholderText: "0",
|
||||
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: {
|
||||
}
|
||||
)
|
||||
VStack(spacing: 0) {
|
||||
Text("Balance \(viewStore.totalBalance.asZecString()) ZEC")
|
||||
Text("($\(viewStore.totalCurrencyBalance.asZecString()))")
|
||||
.font(.system(size: 13))
|
||||
.opacity(0.6)
|
||||
}
|
||||
.padding()
|
||||
|
||||
|
||||
VStack {
|
||||
Text("To Address")
|
||||
|
||||
TextField(
|
||||
"Address",
|
||||
text: $transaction.toAddress
|
||||
TransactionAmountTextField(
|
||||
store: store.scope(
|
||||
state: \.transactionAmountInputState,
|
||||
action: SendAction.transactionAmountInput
|
||||
)
|
||||
)
|
||||
.font(.system(size: 14))
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.foregroundColor(Asset.Colors.Text.importSeedEditor.color)
|
||||
|
||||
if viewStore.isInvalidAmountFormat {
|
||||
HStack {
|
||||
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()
|
||||
|
||||
VStack {
|
||||
Text("Memo")
|
||||
|
||||
TextEditor(text: $transaction.memo)
|
||||
TextEditor(text: viewStore.bindingForMemo)
|
||||
.frame(maxWidth: .infinity, maxHeight: 150, alignment: .center)
|
||||
.importSeedEditorModifier()
|
||||
.importSeedEditorModifier(Asset.Colors.Text.activeButtonText.color)
|
||||
}
|
||||
.padding()
|
||||
|
||||
Button(
|
||||
action: { isComplete = true },
|
||||
action: { viewStore.send(.updateRoute(.confirmation)) },
|
||||
label: { Text("Send") }
|
||||
)
|
||||
.activeButtonStyle
|
||||
.frame(height: 50)
|
||||
.padding()
|
||||
|
||||
.activeButtonStyle
|
||||
.frame(height: 50)
|
||||
.padding()
|
||||
.disabled(!viewStore.isValidForm)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.padding()
|
||||
.applyScreenBackground()
|
||||
}
|
||||
|
@ -81,18 +97,9 @@ struct Create_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
NavigationView {
|
||||
StateContainer(
|
||||
initialState: (
|
||||
Transaction.placeholder,
|
||||
false,
|
||||
0.0
|
||||
)
|
||||
) {
|
||||
CreateTransaction(
|
||||
store: .placeholder,
|
||||
transaction: $0.0,
|
||||
isComplete: $0.1,
|
||||
totalBalance: $0.2
|
||||
)
|
||||
initialState: ( false )
|
||||
) { _ in
|
||||
CreateTransaction(store: .placeholder)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.preferredColorScheme(.dark)
|
||||
|
@ -107,7 +114,8 @@ extension SendStore {
|
|||
initialState: .init(
|
||||
route: nil,
|
||||
transaction: .placeholder,
|
||||
transactionInputState: .placeholer
|
||||
transactionAddressInputState: .placeholder,
|
||||
transactionAmountInputState: .placeholder
|
||||
),
|
||||
reducer: .default,
|
||||
environment: SendEnvironment(
|
||||
|
|
|
@ -2,33 +2,17 @@ import SwiftUI
|
|||
import ComposableArchitecture
|
||||
|
||||
struct SendView: View {
|
||||
let store: Store<SendState, SendAction>
|
||||
let store: SendStore
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(store) { viewStore in
|
||||
CreateTransaction(
|
||||
store: store.scope(
|
||||
state: \.transactionInputState,
|
||||
action: SendAction.transactionInput
|
||||
),
|
||||
transaction: viewStore.bindingForTransaction,
|
||||
isComplete: viewStore.bindingForConfirmation,
|
||||
totalBalance: viewStore.bindingForBalance
|
||||
)
|
||||
CreateTransaction(store: store)
|
||||
.onAppear { viewStore.send(.onAppear) }
|
||||
.onDisappear { viewStore.send(.onDisappear) }
|
||||
.navigationLinkEmpty(
|
||||
isActive: viewStore.bindingForConfirmation,
|
||||
destination: {
|
||||
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(
|
||||
route: nil,
|
||||
transaction: .placeholder,
|
||||
transactionInputState: .placeholer
|
||||
transactionAddressInputState: .placeholder,
|
||||
transactionAmountInputState: .placeholder
|
||||
),
|
||||
reducer: .default,
|
||||
environment: SendEnvironment(
|
||||
|
|
|
@ -6,7 +6,7 @@ struct TransactionConfirmation: View {
|
|||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Send \(String(format: "%.7f", Int64(viewStore.transactionInputState.amount).asHumanReadableZecBalance())) ZEC")
|
||||
Text("Send \(viewStore.transaction.amount.asZecString()) ZEC")
|
||||
.padding()
|
||||
|
||||
Text("To \(viewStore.transaction.toAddress)")
|
||||
|
@ -25,6 +25,14 @@ struct TransactionConfirmation: View {
|
|||
Spacer()
|
||||
}
|
||||
.applyScreenBackground()
|
||||
.navigationLinkEmpty(
|
||||
isActive: viewStore.bindingForSuccess,
|
||||
destination: { TransactionSent(viewStore: viewStore) }
|
||||
)
|
||||
.navigationLinkEmpty(
|
||||
isActive: viewStore.bindingForFailure,
|
||||
destination: { TransactionFailed(viewStore: viewStore) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ extension TransactionHistoryView {
|
|||
Spacer()
|
||||
|
||||
Text(transaction.status == .received ? "+" : "")
|
||||
+ Text("\(String(format: "%.7f", transaction.zecAmount.asHumanReadableZecBalance())) ZEC")
|
||||
+ Text("\(transaction.zecAmount.asZecString()) ZEC")
|
||||
}
|
||||
}
|
||||
.navigationLink(
|
||||
|
|
|
@ -63,3 +63,5 @@
|
|||
"Skip" = "Skip";
|
||||
"Next" = "Next";
|
||||
"Send" = "Send";
|
||||
"Clear" = "Clear";
|
||||
"Max" = "Max";
|
||||
|
|
|
@ -8,13 +8,14 @@
|
|||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct SingleLineTextField<TitleAccessoryContent, InputAccessoryContent>: View
|
||||
where TitleAccessoryContent: View, InputAccessoryContent: View {
|
||||
struct SingleLineTextField<TitleAccessoryContent, InputPrefixContent, InputAccessoryContent>: View
|
||||
where TitleAccessoryContent: View, InputPrefixContent: View, InputAccessoryContent: View {
|
||||
let placeholderText: String
|
||||
let title: String
|
||||
let store: TextFieldStore
|
||||
|
||||
@ViewBuilder let titleAccessoryView: TitleAccessoryContent
|
||||
@ViewBuilder let inputPrefixView: InputPrefixContent
|
||||
@ViewBuilder let inputAccessoryView: InputAccessoryContent
|
||||
|
||||
var body: some View {
|
||||
|
@ -23,6 +24,7 @@ struct SingleLineTextField<TitleAccessoryContent, InputAccessoryContent>: View
|
|||
Text(title)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.font(.system(size: 13))
|
||||
|
||||
Spacer()
|
||||
|
||||
|
@ -30,6 +32,8 @@ struct SingleLineTextField<TitleAccessoryContent, InputAccessoryContent>: View
|
|||
}
|
||||
|
||||
HStack {
|
||||
inputPrefixView
|
||||
|
||||
TextFieldInput(
|
||||
placeholder: placeholderText,
|
||||
store: store
|
||||
|
@ -67,6 +71,7 @@ struct SingleLineTextField_Previews: PreviewProvider {
|
|||
)
|
||||
.textFieldTitleAccessoryButtonStyle
|
||||
},
|
||||
inputPrefixView: { EmptyView() },
|
||||
inputAccessoryView: { EmptyView() }
|
||||
)
|
||||
}
|
||||
|
@ -84,6 +89,7 @@ struct SingleLineTextField_Previews: PreviewProvider {
|
|||
title: "Who would you like to deal with really long text today?",
|
||||
store: store,
|
||||
titleAccessoryView: { EmptyView() },
|
||||
inputPrefixView: { EmptyView() },
|
||||
inputAccessoryView: { EmptyView() }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ struct TextFieldInput: View {
|
|||
set: { viewStore.send(.set($0)) }
|
||||
)
|
||||
)
|
||||
.autocapitalization(.none)
|
||||
.font(.system(size: 13))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.accentColor(Asset.Colors.Cursor.bar.color)
|
||||
|
|
|
@ -22,7 +22,6 @@ struct TextFieldState: Equatable {
|
|||
}
|
||||
|
||||
enum TextFieldAction: Equatable {
|
||||
// case apply((String) -> String)
|
||||
case set(String)
|
||||
}
|
||||
|
||||
|
@ -31,9 +30,6 @@ struct TextFieldEnvironment: Equatable { }
|
|||
extension TextFieldReducer {
|
||||
static let `default` = TextFieldReducer { state, action, _ in
|
||||
switch action {
|
||||
// case .apply(let action):
|
||||
// state.text = action(state.text)
|
||||
// state.valid = state.text.isValid(for: state.validationType)
|
||||
case .set(let text):
|
||||
state.text = text
|
||||
state.valid = state.text.isValid(for: state.validationType)
|
||||
|
@ -65,4 +61,9 @@ extension TextFieldState {
|
|||
validationType: nil,
|
||||
text: ""
|
||||
)
|
||||
|
||||
static let amount = TextFieldState(
|
||||
validationType: .floatingPoint,
|
||||
text: ""
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import ComposableArchitecture
|
||||
|
||||
// TODO: Reimplement this into multicurrency supporter, issue #315 (https://github.com/zcash/secant-ios-wallet/issues/315)
|
||||
|
||||
typealias CurrencySelectionReducer = Reducer<
|
||||
CurrencySelectionState,
|
||||
CurrencySelectionAction,
|
||||
|
@ -35,7 +37,7 @@ enum CurrencySelectionAction: Equatable {
|
|||
case swapCurrencyType
|
||||
}
|
||||
|
||||
struct CurrencySelectionEnvironment: Equatable { }
|
||||
struct CurrencySelectionEnvironment { }
|
||||
|
||||
extension CurrencySelectionReducer {
|
||||
static var `default`: Self {
|
|
@ -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()
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// TransactionTextField.swift
|
||||
// TransactionAmountTextField.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Adam Stener on 4/4/22.
|
||||
|
@ -8,38 +8,43 @@
|
|||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct TransactionTextField: View {
|
||||
let store: TransactionInputStore
|
||||
|
||||
// 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
|
||||
|
||||
struct TransactionAmountTextField: View {
|
||||
let store: TransactionAmountInputStore
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(store) { viewStore in
|
||||
VStack {
|
||||
SingleLineTextField(
|
||||
placeholderText: "$0",
|
||||
title: "How much?",
|
||||
placeholderText: "0",
|
||||
title: "How much ZEC would you like to send?",
|
||||
store: store.scope(
|
||||
state: \.textFieldState,
|
||||
action: TransactionInputAction.textField
|
||||
action: TransactionAmountInputAction.textField
|
||||
),
|
||||
titleAccessoryView: {
|
||||
Button(
|
||||
action: {
|
||||
viewStore.send(.setMax(maxTransactionValue))
|
||||
viewStore.send(viewStore.isMax ? .clearValue : .setMax)
|
||||
},
|
||||
label: { Text("Max") }
|
||||
label: {
|
||||
Text(viewStore.isMax ? "Clear" : "Max")
|
||||
}
|
||||
)
|
||||
.textFieldTitleAccessoryButtonStyle
|
||||
},
|
||||
inputPrefixView: {
|
||||
if viewStore.currencySelectionState.currencyType == .zec {
|
||||
ZcashSymbol()
|
||||
.frame(width: 12, height: 12, alignment: .center)
|
||||
} else {
|
||||
Text("$")
|
||||
}
|
||||
},
|
||||
inputAccessoryView: {
|
||||
TransactionCurrencySelector(
|
||||
store: store.scope(
|
||||
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 {
|
||||
TransactionTextField(
|
||||
store: TransactionInputStore(
|
||||
TransactionAmountTextField(
|
||||
store: TransactionAmountInputStore(
|
||||
initialState: .init(
|
||||
textFieldState: .init(
|
||||
validationType: .floatingPoint,
|
||||
|
@ -80,8 +85,8 @@ struct TransactionTextField_Previews: PreviewProvider {
|
|||
)
|
||||
.textFieldTitleAccessoryButtonStyle
|
||||
},
|
||||
inputAccessoryView: {
|
||||
}
|
||||
inputPrefixView: { EmptyView() },
|
||||
inputAccessoryView: { EmptyView() }
|
||||
)
|
||||
.preferredColorScheme(.dark)
|
||||
.padding(.horizontal, 50)
|
||||
|
@ -92,8 +97,8 @@ struct TransactionTextField_Previews: PreviewProvider {
|
|||
placeholderText: "",
|
||||
title: "Address",
|
||||
store: .address,
|
||||
titleAccessoryView: {
|
||||
},
|
||||
titleAccessoryView: { EmptyView() },
|
||||
inputPrefixView: { EmptyView() },
|
||||
inputAccessoryView: {
|
||||
Image(Asset.Assets.Icons.qrCode.name)
|
||||
.resizable()
|
|
@ -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()
|
||||
)
|
||||
}
|
|
@ -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)) ?? ""
|
||||
}
|
||||
}
|
|
@ -12,4 +12,8 @@ extension Int64 {
|
|||
func asHumanReadableZecBalance() -> Double {
|
||||
Double(self) / Double(100_000_000)
|
||||
}
|
||||
|
||||
func asZecString() -> String {
|
||||
NumberFormatter.zcashFormatter.string(from: NSNumber(value: self.asHumanReadableZecBalance())) ?? ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
// 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 phoneRegex = "^^\\+(?:[0-9]?){6,14}[0-9]$"
|
||||
|
||||
|
@ -32,7 +47,7 @@ extension String {
|
|||
return text.validate(using: .emailRegex)
|
||||
|
||||
case .floatingPoint:
|
||||
return text.validate(using: .floatingPointRegex)
|
||||
return text.doubleValue != nil
|
||||
|
||||
case .maxLength(let length):
|
||||
return text.count <= length && !text.isEmpty
|
||||
|
|
|
@ -124,6 +124,11 @@ struct WrappedDerivationTool {
|
|||
Checks validity of the shielded address.
|
||||
*/
|
||||
let isValidShieldedAddress: (String) throws -> Bool
|
||||
|
||||
/**
|
||||
Checks if given address is a valid zcash address.
|
||||
*/
|
||||
let isValidZcashAddress: (String) throws -> Bool
|
||||
}
|
||||
|
||||
extension WrappedDerivationTool {
|
||||
|
@ -170,6 +175,10 @@ extension WrappedDerivationTool {
|
|||
},
|
||||
isValidShieldedAddress: { zAddress in
|
||||
try derivationTool.isValidShieldedAddress(zAddress)
|
||||
},
|
||||
isValidZcashAddress: { address in
|
||||
try derivationTool.isValidTransparentAddress(address) ? true :
|
||||
try derivationTool.isValidShieldedAddress(address) ? true : false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -313,7 +313,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
|
|||
mocked.map {
|
||||
TransactionState.placeholder(
|
||||
date: Date.init(timeIntervalSince1970: $0.date),
|
||||
amount: $0.amount * 100000000,
|
||||
amount: $0.amount,
|
||||
shielded: $0.shielded,
|
||||
status: $0.status,
|
||||
subtitle: $0.subtitle
|
||||
|
@ -335,7 +335,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
|
|||
mocked.map {
|
||||
TransactionState.placeholder(
|
||||
date: Date.init(timeIntervalSince1970: $0.date),
|
||||
amount: $0.amount * 100000000,
|
||||
amount: $0.amount,
|
||||
shielded: $0.shielded,
|
||||
status: $0.status,
|
||||
subtitle: $0.subtitle
|
||||
|
@ -420,7 +420,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
|
|||
mocked.map {
|
||||
TransactionState.placeholder(
|
||||
date: Date.init(timeIntervalSince1970: $0.date),
|
||||
amount: $0.amount * 100000000,
|
||||
amount: $0.amount,
|
||||
shielded: $0.shielded,
|
||||
status: $0.status,
|
||||
subtitle: $0.subtitle,
|
||||
|
@ -443,7 +443,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
|
|||
mocked.map {
|
||||
TransactionState.placeholder(
|
||||
date: Date.init(timeIntervalSince1970: $0.date),
|
||||
amount: $0.amount * 100000000,
|
||||
amount: $0.amount,
|
||||
shielded: $0.shielded,
|
||||
status: $0.status,
|
||||
subtitle: $0.subtitle,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,11 @@ import XCTest
|
|||
import ComposableArchitecture
|
||||
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 {
|
||||
var storage = WalletStorage(secItem: .live)
|
||||
|
||||
|
@ -25,7 +30,7 @@ class SendTests: XCTestCase {
|
|||
|
||||
// setup the store and environment to be fully mocked
|
||||
let testScheduler = DispatchQueue.test
|
||||
|
||||
|
||||
let testEnvironment = SendEnvironment(
|
||||
mnemonicSeedPhraseProvider: .mock,
|
||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||
|
@ -81,7 +86,7 @@ class SendTests: XCTestCase {
|
|||
|
||||
// setup the store and environment to be fully mocked
|
||||
let testScheduler = DispatchQueue.test
|
||||
|
||||
|
||||
let testEnvironment = SendEnvironment(
|
||||
mnemonicSeedPhraseProvider: .mock,
|
||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||
|
@ -117,4 +122,389 @@ class SendTests: XCTestCase {
|
|||
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)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ class TransactionHistoryTests: XCTestCase {
|
|||
let transactions = mocked.map {
|
||||
TransactionState.placeholder(
|
||||
date: Date.init(timeIntervalSince1970: $0.date),
|
||||
amount: $0.amount * 100000000,
|
||||
amount: $0.amount,
|
||||
shielded: $0.shielded,
|
||||
status: $0.status,
|
||||
subtitle: $0.subtitle,
|
||||
|
|
Loading…
Reference in New Issue