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

cleanup

validation

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

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

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

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

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

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

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

- send routing simplified and cleaned up

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

- unit tests

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

- cleanup

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

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

View File

@ -59,13 +59,13 @@
2E35F99A27B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E35F99927B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift */; };
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 */,

View File

@ -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
}

View File

@ -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
)
}
}

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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
)
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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) }
)
}
}

View File

@ -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(

View File

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

View File

@ -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() }
)
}

View File

@ -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)

View File

@ -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: ""
)
}

View File

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

View File

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

View File

@ -7,6 +7,8 @@
import ComposableArchitecture
// 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 {

View File

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

View File

@ -1,5 +1,5 @@
//
// TransactionTextField.swift
// TransactionAmountTextField.swift
// secant-testnet
//
// 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()

View File

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

View File

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

View File

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

View File

@ -9,9 +9,24 @@ extension String {
}
}
extension NumberFormatter {
static let zcashFormatter: NumberFormatter = {
var formatter = NumberFormatter()
formatter.maximumFractionDigits = 8
formatter.maximumIntegerDigits = 8
formatter.numberStyle = .decimal
formatter.usesGroupingSeparator = true
return formatter
}()
}
extension String {
var doubleValue: Double? {
return NumberFormatter.zcashFormatter.number(from: self)?.doubleValue
}
}
extension String {
// 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

View File

@ -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
}
)
}

View File

@ -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,

View File

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

View File

@ -10,6 +10,11 @@ import XCTest
import ComposableArchitecture
import 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)"
)
}
}
}

View File

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

View File

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

View File

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