diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 00b35cd..b9c18ea 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -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 = ""; }; 2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = ""; }; 2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySelectionStore.swift; sourceTree = ""; }; - 2E8719CA27FB09990082C926 /* TransactionTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionTextField.swift; sourceTree = ""; }; + 2E8719CA27FB09990082C926 /* TransactionAmountTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionAmountTextField.swift; sourceTree = ""; }; 2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionCurrencySelector.swift; sourceTree = ""; }; 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = ""; }; 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingContentView.swift; sourceTree = ""; }; 2EB1C5E727D77F6100BC43D7 /* TextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldStore.swift; sourceTree = ""; }; - 2EB7758627FC67FD00269373 /* TransactionInputStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInputStore.swift; sourceTree = ""; }; + 2EB7758627FC67FD00269373 /* TransactionAmountInputStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputStore.swift; sourceTree = ""; }; 2EDA079F27EDE18C00D6F09B /* TextFieldInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldInput.swift; sourceTree = ""; }; 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldFooter.swift; sourceTree = ""; }; 2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugFrame.swift; sourceTree = ""; }; @@ -276,6 +282,8 @@ 9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = ""; }; 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedUserDefaults.swift; sourceTree = ""; }; 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedNotificationCenter.swift; sourceTree = ""; }; + 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextField.swift; sourceTree = ""; }; + 9E5BF64E2823E94900BA3F17 /* TransactionAddressInputStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputStore.swift; sourceTree = ""; }; 9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = ""; }; 9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = ""; }; @@ -287,6 +295,10 @@ 9EAFEB8E2808183D00199FC9 /* SandboxStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxStore.swift; sourceTree = ""; }; 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = ""; }; 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFiles.swift; sourceTree = ""; }; + 9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Zcash.swift"; sourceTree = ""; }; + 9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencySelectionTests.swift; sourceTree = ""; }; + 9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputTests.swift; sourceTree = ""; }; + 9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputTests.swift; sourceTree = ""; }; 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = ""; }; 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = ""; }; 9EF8135F27F043CC0075AF48 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -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 = ""; @@ -806,11 +817,34 @@ 9E5BF642281FEC8700BA3F17 /* SendTests */ = { isa = PBXGroup; children = ( + 9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */, 9E5BF643281FEC9900BA3F17 /* SendTests.swift */, + 9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */, + 9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */, ); path = SendTests; sourceTree = ""; }; + 9E5BF64B2823C91200BA3F17 /* TransactionAmount */ = { + isa = PBXGroup; + children = ( + 2EB7758627FC67FD00269373 /* TransactionAmountInputStore.swift */, + 2E8719CA27FB09990082C926 /* TransactionAmountTextField.swift */, + 2E6CF8DC27D78319004DCD7A /* CurrencySelectionStore.swift */, + 2E8719CC27FB0D3B0082C926 /* TransactionCurrencySelector.swift */, + ); + path = TransactionAmount; + sourceTree = ""; + }; + 9E5BF64C2823E84300BA3F17 /* TransactionAddress */ = { + isa = PBXGroup; + children = ( + 9E5BF64E2823E94900BA3F17 /* TransactionAddressInputStore.swift */, + 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */, + ); + path = TransactionAddress; + sourceTree = ""; + }; 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 */, diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 71b3b9f..4e8bcfd 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 } diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index f4b487a..7b1d636 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -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 ) } } diff --git a/secant/Features/Home/Views/HomeView.swift b/secant/Features/Home/Views/HomeView.swift index 6b14546..b5bb27a 100644 --- a/secant/Features/Home/Views/HomeView.swift +++ b/secant/Features/Home/Views/HomeView.swift @@ -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) } diff --git a/secant/Features/ImportWallet/Views/ImportSeedEditor.swift b/secant/Features/ImportWallet/Views/ImportSeedEditor.swift index 9f0630b..98a4a91 100644 --- a/secant/Features/ImportWallet/Views/ImportSeedEditor.swift +++ b/secant/Features/ImportWallet/Views/ImportSeedEditor.swift @@ -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)) } } diff --git a/secant/Features/Send/SendStore.swift b/secant/Features/Send/SendStore.swift index 1b5a81c..54e7eb5 100644 --- a/secant/Features/Send/SendStore.swift +++ b/secant/Features/Send/SendStore.swift @@ -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) 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 { 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 { 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 { 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 { 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 { + var bindingForMemo: Binding { 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 ) } } diff --git a/secant/Features/Send/Views/CreateTransactionView.swift b/secant/Features/Send/Views/CreateTransactionView.swift index 50f954d..dd9d1b4 100644 --- a/secant/Features/Send/Views/CreateTransactionView.swift +++ b/secant/Features/Send/Views/CreateTransactionView.swift @@ -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( diff --git a/secant/Features/Send/Views/SendView.swift b/secant/Features/Send/Views/SendView.swift index 16bc459..3ab38d0 100644 --- a/secant/Features/Send/Views/SendView.swift +++ b/secant/Features/Send/Views/SendView.swift @@ -2,33 +2,17 @@ import SwiftUI import ComposableArchitecture struct SendView: View { - let store: Store + 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( diff --git a/secant/Features/Send/Views/TransactionConfirmationView.swift b/secant/Features/Send/Views/TransactionConfirmationView.swift index 6de8142..e64f21b 100644 --- a/secant/Features/Send/Views/TransactionConfirmationView.swift +++ b/secant/Features/Send/Views/TransactionConfirmationView.swift @@ -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) } + ) } } diff --git a/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift b/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift index cd12448..0869e07 100644 --- a/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift +++ b/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift @@ -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( diff --git a/secant/Localizable.strings b/secant/Localizable.strings index f8404cc..c9e0f68 100644 --- a/secant/Localizable.strings +++ b/secant/Localizable.strings @@ -63,3 +63,5 @@ "Skip" = "Skip"; "Next" = "Next"; "Send" = "Send"; +"Clear" = "Clear"; +"Max" = "Max"; diff --git a/secant/UIComponents/TextFields/Components/SingleLineTextField.swift b/secant/UIComponents/TextFields/Components/SingleLineTextField.swift index af25276..acd3802 100644 --- a/secant/UIComponents/TextFields/Components/SingleLineTextField.swift +++ b/secant/UIComponents/TextFields/Components/SingleLineTextField.swift @@ -8,13 +8,14 @@ import SwiftUI import ComposableArchitecture -struct SingleLineTextField: View - where TitleAccessoryContent: View, InputAccessoryContent: View { +struct SingleLineTextField: 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: View Text(title) .lineLimit(1) .truncationMode(.middle) + .font(.system(size: 13)) Spacer() @@ -30,6 +32,8 @@ struct SingleLineTextField: 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() } ) } diff --git a/secant/UIComponents/TextFields/Components/TextFieldInput.swift b/secant/UIComponents/TextFields/Components/TextFieldInput.swift index e6934f2..0c44fd6 100644 --- a/secant/UIComponents/TextFields/Components/TextFieldInput.swift +++ b/secant/UIComponents/TextFields/Components/TextFieldInput.swift @@ -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) diff --git a/secant/UIComponents/TextFields/Components/TextFieldStore.swift b/secant/UIComponents/TextFields/Components/TextFieldStore.swift index 5db480c..833d33f 100644 --- a/secant/UIComponents/TextFields/Components/TextFieldStore.swift +++ b/secant/UIComponents/TextFields/Components/TextFieldStore.swift @@ -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: "" + ) } diff --git a/secant/UIComponents/TextFields/TransactionAddress/TransactionAddressInputStore.swift b/secant/UIComponents/TextFields/TransactionAddress/TransactionAddressInputStore.swift new file mode 100644 index 0000000..5b05cd9 --- /dev/null +++ b/secant/UIComponents/TextFields/TransactionAddress/TransactionAddressInputStore.swift @@ -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 + +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() + ) + ) +} diff --git a/secant/UIComponents/TextFields/TransactionAddress/TransactionAddressTextField.swift b/secant/UIComponents/TextFields/TransactionAddress/TransactionAddressTextField.swift new file mode 100644 index 0000000..c3af618 --- /dev/null +++ b/secant/UIComponents/TextFields/TransactionAddress/TransactionAddressTextField.swift @@ -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)) + } +} diff --git a/secant/UIComponents/TextFields/CurrencySelectionStore.swift b/secant/UIComponents/TextFields/TransactionAmount/CurrencySelectionStore.swift similarity index 87% rename from secant/UIComponents/TextFields/CurrencySelectionStore.swift rename to secant/UIComponents/TextFields/TransactionAmount/CurrencySelectionStore.swift index f1b3c76..448fee0 100644 --- a/secant/UIComponents/TextFields/CurrencySelectionStore.swift +++ b/secant/UIComponents/TextFields/TransactionAmount/CurrencySelectionStore.swift @@ -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 { diff --git a/secant/UIComponents/TextFields/TransactionAmount/TransactionAmountInputStore.swift b/secant/UIComponents/TextFields/TransactionAmount/TransactionAmountInputStore.swift new file mode 100644 index 0000000..7c2d80c --- /dev/null +++ b/secant/UIComponents/TextFields/TransactionAmount/TransactionAmountInputStore.swift @@ -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 + +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() + ) +} diff --git a/secant/UIComponents/TextFields/TransactionTextField.swift b/secant/UIComponents/TextFields/TransactionAmount/TransactionAmountTextField.swift similarity index 65% rename from secant/UIComponents/TextFields/TransactionTextField.swift rename to secant/UIComponents/TextFields/TransactionAmount/TransactionAmountTextField.swift index 4b200c4..4780046 100644 --- a/secant/UIComponents/TextFields/TransactionTextField.swift +++ b/secant/UIComponents/TextFields/TransactionAmount/TransactionAmountTextField.swift @@ -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() diff --git a/secant/UIComponents/TextFields/TransactionCurrencySelector.swift b/secant/UIComponents/TextFields/TransactionAmount/TransactionCurrencySelector.swift similarity index 100% rename from secant/UIComponents/TextFields/TransactionCurrencySelector.swift rename to secant/UIComponents/TextFields/TransactionAmount/TransactionCurrencySelector.swift diff --git a/secant/UIComponents/TextFields/TransactionInputStore.swift b/secant/UIComponents/TextFields/TransactionInputStore.swift deleted file mode 100644 index 148ff4e..0000000 --- a/secant/UIComponents/TextFields/TransactionInputStore.swift +++ /dev/null @@ -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 - -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() - ) -} diff --git a/secant/Util/Double+Zcash.swift b/secant/Util/Double+Zcash.swift new file mode 100644 index 0000000..b17beb4 --- /dev/null +++ b/secant/Util/Double+Zcash.swift @@ -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)) ?? "" + } +} diff --git a/secant/Util/Int64+Zcash.swift b/secant/Util/Int64+Zcash.swift index 3d4b3c9..37276ac 100644 --- a/secant/Util/Int64+Zcash.swift +++ b/secant/Util/Int64+Zcash.swift @@ -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())) ?? "" + } } diff --git a/secant/Util/Strings.swift b/secant/Util/Strings.swift index 1ff9e03..c65e344 100644 --- a/secant/Util/Strings.swift +++ b/secant/Util/Strings.swift @@ -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 diff --git a/secant/Wrappers/WrappedDerivationTool.swift b/secant/Wrappers/WrappedDerivationTool.swift index ec54fe2..33d22ed 100644 --- a/secant/Wrappers/WrappedDerivationTool.swift +++ b/secant/Wrappers/WrappedDerivationTool.swift @@ -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 } ) } diff --git a/secant/Wrappers/WrappedSDKSynchronizer.swift b/secant/Wrappers/WrappedSDKSynchronizer.swift index 14ccae4..62dd595 100644 --- a/secant/Wrappers/WrappedSDKSynchronizer.swift +++ b/secant/Wrappers/WrappedSDKSynchronizer.swift @@ -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, diff --git a/secantTests/SendTests/CurrencySelectionTests.swift b/secantTests/SendTests/CurrencySelectionTests.swift new file mode 100644 index 0000000..e00851e --- /dev/null +++ b/secantTests/SendTests/CurrencySelectionTests.swift @@ -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 + } + } +} diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index 8396ce2..d683299 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -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 + ) 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)" + ) + } + } } diff --git a/secantTests/SendTests/TransactionAddressInputTests.swift b/secantTests/SendTests/TransactionAddressInputTests.swift new file mode 100644 index 0000000..5b17d2f --- /dev/null +++ b/secantTests/SendTests/TransactionAddressInputTests.swift @@ -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 = "" + } + } +} diff --git a/secantTests/SendTests/TransactionAmountInputTests.swift b/secantTests/SendTests/TransactionAmountInputTests.swift new file mode 100644 index 0000000..e3e068b --- /dev/null +++ b/secantTests/SendTests/TransactionAmountInputTests.swift @@ -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)" + ) + } + } +} diff --git a/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift b/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift index b04d021..599c027 100644 --- a/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift +++ b/secantTests/TransactionHistoryTests/TransactionHistoryTests.swift @@ -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,