Merge pull request #134 from zcash/feature/recovery-phrase-validation

Recovery Phrase Validation.
This commit is contained in:
Francisco Gindre 2022-02-18 10:36:50 -03:00 committed by GitHub
commit 58175b5dcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1878 additions and 235 deletions

View File

@ -7,7 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0D185819272723FF0046B928 /* BlueChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D185818272723FF0046B928 /* BlueChip.swift */; }; 0D185819272723FF0046B928 /* ColoredChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D185818272723FF0046B928 /* ColoredChip.swift */; };
0D18581B272728D60046B928 /* PhraseChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18581A272728D60046B928 /* PhraseChip.swift */; }; 0D18581B272728D60046B928 /* PhraseChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18581A272728D60046B928 /* PhraseChip.swift */; };
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */; }; 0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */; };
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F726BDEB3500052649 /* MockServices.swift */; }; 0D1922F826BDEB3500052649 /* MockServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F726BDEB3500052649 /* MockServices.swift */; };
@ -16,6 +16,7 @@
0D354A0926D5A9D000315F45 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0626D5A9D000315F45 /* Services.swift */; }; 0D354A0926D5A9D000315F45 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0626D5A9D000315F45 /* Services.swift */; };
0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0726D5A9D000315F45 /* KeyStoring.swift */; }; 0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0726D5A9D000315F45 /* KeyStoring.swift */; };
0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */; }; 0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */; };
0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */; };
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */; }; 0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */; };
0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */; }; 0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */; };
0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4E7A0826B364170058B01E /* SecantApp.swift */; }; 0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4E7A0826B364170058B01E /* SecantApp.swift */; };
@ -28,6 +29,8 @@
0D535FDF271F4214009A9E3E /* Rubik-VariableFont_wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */; }; 0D535FDF271F4214009A9E3E /* Rubik-VariableFont_wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */; };
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */; }; 0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */; };
0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5D16F426E24CCF00AD33D1 /* AppError.swift */; }; 0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5D16F426E24CCF00AD33D1 /* AppError.swift */; };
0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6D628A276A528D002FB4CC /* DropDelegate.swift */; };
0D7CE63427349B5D0020E050 /* View+WhenDraggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */; };
0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */; }; 0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */; };
0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */; }; 0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */; };
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C5272B129C005A6414 /* WordChipGrid.swift */; }; 0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C5272B129C005A6414 /* WordChipGrid.swift */; };
@ -47,9 +50,14 @@
0DACFA9A27209FA70039EEA5 /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */; }; 0DACFA9A27209FA70039EEA5 /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */; };
0DACFA9C27209FA70039EEA5 /* Roboto-ThinItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */; }; 0DACFA9C27209FA70039EEA5 /* Roboto-ThinItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */; };
0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */; }; 0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */; };
0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC487C22772574C00BE6A63 /* ValidationSucceededView.swift */; };
0DDB6A5127737D4A0012A410 /* ValidationFailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DDB6A5027737D4A0012A410 /* ValidationFailedView.swift */; };
0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */; }; 0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */; };
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */; }; 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */; };
0DFE93DF272C6D4B000FCCA5 /* RecoveryFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93DE272C6D4B000FCCA5 /* RecoveryFlowTests.swift */; }; 0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93DE272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift */; };
0DFE93E1272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93E0272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift */; };
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93E2272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift */; };
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */; };
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */; }; 2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */; };
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; }; 2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; };
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; }; 2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; };
@ -118,7 +126,7 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0D185818272723FF0046B928 /* BlueChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueChip.swift; sourceTree = "<group>"; }; 0D185818272723FF0046B928 /* ColoredChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredChip.swift; sourceTree = "<group>"; };
0D18581A272728D60046B928 /* PhraseChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhraseChip.swift; sourceTree = "<group>"; }; 0D18581A272728D60046B928 /* PhraseChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhraseChip.swift; sourceTree = "<group>"; };
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = "<group>"; }; 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = "<group>"; };
0D1922F726BDEB3500052649 /* MockServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServices.swift; sourceTree = "<group>"; }; 0D1922F726BDEB3500052649 /* MockServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServices.swift; sourceTree = "<group>"; };
@ -127,6 +135,7 @@
0D354A0626D5A9D000315F45 /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; }; 0D354A0626D5A9D000315F45 /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
0D354A0726D5A9D000315F45 /* KeyStoring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyStoring.swift; sourceTree = "<group>"; }; 0D354A0726D5A9D000315F45 /* KeyStoring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyStoring.swift; sourceTree = "<group>"; };
0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseHandling.swift; sourceTree = "<group>"; }; 0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseHandling.swift; sourceTree = "<group>"; };
0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableWhenScaled.swift; sourceTree = "<group>"; };
0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayView.swift; sourceTree = "<group>"; }; 0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayView.swift; sourceTree = "<group>"; };
0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayStore.swift; sourceTree = "<group>"; }; 0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayStore.swift; sourceTree = "<group>"; };
0D4E7A0526B364170058B01E /* secant-testnet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "secant-testnet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 0D4E7A0526B364170058B01E /* secant-testnet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "secant-testnet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@ -145,6 +154,8 @@
0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Rubik-VariableFont_wght.ttf"; sourceTree = "<group>"; }; 0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Rubik-VariableFont_wght.ttf"; sourceTree = "<group>"; };
0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumeratedChip.swift; sourceTree = "<group>"; }; 0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumeratedChip.swift; sourceTree = "<group>"; };
0D5D16F426E24CCF00AD33D1 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; }; 0D5D16F426E24CCF00AD33D1 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
0D6D628A276A528D002FB4CC /* DropDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropDelegate.swift; sourceTree = "<group>"; };
0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+WhenDraggable.swift"; sourceTree = "<group>"; };
0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenBackground.swift; sourceTree = "<group>"; }; 0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenBackground.swift; sourceTree = "<group>"; };
0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantTextStyles.swift; sourceTree = "<group>"; }; 0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantTextStyles.swift; sourceTree = "<group>"; };
0D8A43C5272B129C005A6414 /* WordChipGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordChipGrid.swift; sourceTree = "<group>"; }; 0D8A43C5272B129C005A6414 /* WordChipGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordChipGrid.swift; sourceTree = "<group>"; };
@ -164,9 +175,14 @@
0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Light.ttf"; sourceTree = "<group>"; }; 0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Light.ttf"; sourceTree = "<group>"; };
0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-ThinItalic.ttf"; sourceTree = "<group>"; }; 0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-ThinItalic.ttf"; sourceTree = "<group>"; };
0DB8AA80271DC7520035BC9D /* DesignGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignGuide.swift; sourceTree = "<group>"; }; 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignGuide.swift; sourceTree = "<group>"; };
0DC487C22772574C00BE6A63 /* ValidationSucceededView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidationSucceededView.swift; sourceTree = "<group>"; };
0DDB6A5027737D4A0012A410 /* ValidationFailedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationFailedView.swift; sourceTree = "<group>"; };
0DF2DC50272344E400FA31E2 /* EmptyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyChip.swift; sourceTree = "<group>"; }; 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyChip.swift; sourceTree = "<group>"; };
0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+InnerShadow.swift"; sourceTree = "<group>"; }; 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+InnerShadow.swift"; sourceTree = "<group>"; };
0DFE93DE272C6D4B000FCCA5 /* RecoveryFlowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryFlowTests.swift; sourceTree = "<group>"; }; 0DFE93DE272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseBackupTests.swift; sourceTree = "<group>"; };
0DFE93E0272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseBackupValidationView.swift; sourceTree = "<group>"; };
0DFE93E2272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidation.swift; sourceTree = "<group>"; };
0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidationTests.swift; sourceTree = "<group>"; };
2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeaderView.swift; sourceTree = "<group>"; }; 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeaderView.swift; sourceTree = "<group>"; };
2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; }; 2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = "<group>"; }; 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = "<group>"; };
@ -182,7 +198,6 @@
6654C7402715A47300901167 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; }; 6654C7402715A47300901167 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStoreTests.swift; sourceTree = "<group>"; }; 6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStoreTests.swift; sourceTree = "<group>"; };
665C963E272C26E600BC04FB /* CircularFrameBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBackground.swift; sourceTree = "<group>"; }; 665C963E272C26E600BC04FB /* CircularFrameBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBackground.swift; sourceTree = "<group>"; };
66779071273AAC26003A1540 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
669FDAE8272C23B3007B9422 /* CircularFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrame.swift; sourceTree = "<group>"; }; 669FDAE8272C23B3007B9422 /* CircularFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrame.swift; sourceTree = "<group>"; };
669FDAEA272C23C2007B9422 /* CircularFrameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBadge.swift; sourceTree = "<group>"; }; 669FDAEA272C23C2007B9422 /* CircularFrameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBadge.swift; sourceTree = "<group>"; };
66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; }; 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; };
@ -283,8 +298,11 @@
0D3D04052728B2D70032ABC1 /* BackupFlow */ = { 0D3D04052728B2D70032ABC1 /* BackupFlow */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0D6D628A276A528D002FB4CC /* DropDelegate.swift */,
0D3D04062728B2EC0032ABC1 /* Views */, 0D3D04062728B2EC0032ABC1 /* Views */,
0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */, 0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */,
0DFE93E2272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift */,
0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */,
); );
path = BackupFlow; path = BackupFlow;
sourceTree = "<group>"; sourceTree = "<group>";
@ -292,8 +310,11 @@
0D3D04062728B2EC0032ABC1 /* Views */ = { 0D3D04062728B2EC0032ABC1 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0DC487C22772574C00BE6A63 /* ValidationSucceededView.swift */,
0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */, 0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */,
0D8A43C5272B129C005A6414 /* WordChipGrid.swift */, 0D8A43C5272B129C005A6414 /* WordChipGrid.swift */,
0DFE93E0272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift */,
0DDB6A5027737D4A0012A410 /* ValidationFailedView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -305,7 +326,6 @@
0D4E7A1926B364180058B01E /* secantTests */, 0D4E7A1926B364180058B01E /* secantTests */,
0D4E7A2426B364180058B01E /* secantUITests */, 0D4E7A2426B364180058B01E /* secantUITests */,
0D4E7A0626B364170058B01E /* Products */, 0D4E7A0626B364170058B01E /* Products */,
2EB660DF2747EA6000A06A07 /* Recovered References */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -353,6 +373,7 @@
0D4E7A1926B364180058B01E /* secantTests */ = { 0D4E7A1926B364180058B01E /* secantTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */,
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */, 0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */,
6654C7422715A48E00901167 /* OnboardingTests */, 6654C7422715A48E00901167 /* OnboardingTests */,
0D4E7A1A26B364180058B01E /* secantTests.swift */, 0D4E7A1A26B364180058B01E /* secantTests.swift */,
@ -384,7 +405,7 @@
children = ( children = (
0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */, 0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */,
0DF2DC50272344E400FA31E2 /* EmptyChip.swift */, 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */,
0D185818272723FF0046B928 /* BlueChip.swift */, 0D185818272723FF0046B928 /* ColoredChip.swift */,
0D18581A272728D60046B928 /* PhraseChip.swift */, 0D18581A272728D60046B928 /* PhraseChip.swift */,
); );
path = Chips; path = Chips;
@ -439,6 +460,7 @@
F9C165B3274031F600592F76 /* Bindings.swift */, F9C165B3274031F600592F76 /* Bindings.swift */,
F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */, F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */,
F93673D52742CB840099C6AF /* Previews.swift */, F93673D52742CB840099C6AF /* Previews.swift */,
0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@ -473,12 +495,20 @@
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */ = { 0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0DFE93DE272C6D4B000FCCA5 /* RecoveryFlowTests.swift */,
0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */, 0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */,
0DFE93DE272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift */,
); );
path = BackupFlowTests; path = BackupFlowTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */ = {
isa = PBXGroup;
children = (
0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */,
);
path = RecoveryPhraseValidationTests;
sourceTree = "<group>";
};
2E5C037F2738C55F008BFFD3 /* Onboarding */ = { 2E5C037F2738C55F008BFFD3 /* Onboarding */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -490,14 +520,6 @@
path = Onboarding; path = Onboarding;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
2EB660DF2747EA6000A06A07 /* Recovered References */ = {
isa = PBXGroup;
children = (
66779071273AAC26003A1540 /* OnboardingScreen.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
};
660558F4270C85F7009D6954 /* Generated */ = { 660558F4270C85F7009D6954 /* Generated */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -814,7 +836,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1250; LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1250; LastUpgradeCheck = 1320;
TargetAttributes = { TargetAttributes = {
0D4E7A0426B364170058B01E = { 0D4E7A0426B364170058B01E = {
CreatedOnToolsVersion = 12.5; CreatedOnToolsVersion = 12.5;
@ -945,16 +967,21 @@
files = ( files = (
2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */, 2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */,
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */, 660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */,
0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */,
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */, F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */, 669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */, F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */,
663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */, 663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */,
0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */,
0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */, 0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */,
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */, 0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */,
0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */, 0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */,
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift in Sources */,
0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */, 0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */,
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */, 0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,
6654C73E2715A41300901167 /* OnboardingStore.swift in Sources */, 6654C73E2715A41300901167 /* OnboardingStore.swift in Sources */,
0DDB6A5127737D4A0012A410 /* ValidationFailedView.swift in Sources */,
0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */,
F9971A5327680DD000A2DB75 /* Profile.swift in Sources */, F9971A5327680DD000A2DB75 /* Profile.swift in Sources */,
F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */, F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */,
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */, 669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
@ -968,6 +995,7 @@
F9971A4D27680DC400A2DB75 /* App.swift in Sources */, F9971A4D27680DC400A2DB75 /* App.swift in Sources */,
F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */, F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */,
F93874F1273C4DE200F0E875 /* HomeView.swift in Sources */, F93874F1273C4DE200F0E875 /* HomeView.swift in Sources */,
0D7CE63427349B5D0020E050 /* View+WhenDraggable.swift in Sources */,
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */, 0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */,
F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */, F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */,
F9971A4E27680DC400A2DB75 /* AppView.swift in Sources */, F9971A4E27680DC400A2DB75 /* AppView.swift in Sources */,
@ -991,7 +1019,7 @@
F9C165C02740403600592F76 /* ApproveView.swift in Sources */, F9C165C02740403600592F76 /* ApproveView.swift in Sources */,
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */, 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */, F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
0D185819272723FF0046B928 /* BlueChip.swift in Sources */, 0D185819272723FF0046B928 /* ColoredChip.swift in Sources */,
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */, 2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */, 2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */,
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */, 0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */,
@ -1005,6 +1033,7 @@
0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */, 0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */,
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */, F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
F9971A6027680DF600A2DB75 /* Scan.swift in Sources */, F9971A6027680DF600A2DB75 /* Scan.swift in Sources */,
0DFE93E1272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift in Sources */,
F9C165CB2741AB5D00592F76 /* SendView.swift in Sources */, F9C165CB2741AB5D00592F76 /* SendView.swift in Sources */,
F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */, F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */,
6654C7412715A47300901167 /* Onboarding.swift in Sources */, 6654C7412715A47300901167 /* Onboarding.swift in Sources */,
@ -1017,10 +1046,11 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0DFE93DF272C6D4B000FCCA5 /* RecoveryFlowTests.swift in Sources */, 0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */, 6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */, 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */, 0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -1170,7 +1200,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "\\"; CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG; DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1194,7 +1224,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "\\"; CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG; DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1250" LastUpgradeVersion = "1320"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xE9",
"green" : "0xE1",
"red" : "0xD8"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x3A",
"green" : "0x36",
"red" : "0x31"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.173",
"green" : "0.047",
"red" : "0.780"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.173",
"green" : "0.047",
"red" : "0.780"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xB1",
"green" : "0xB1",
"red" : "0xF5"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.694",
"green" : "0.694",
"red" : "0.961"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -6,8 +6,8 @@
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0xE3", "blue" : "0xE3",
"green" : "0xD4", "green" : "0xE7",
"red" : "0xC3" "red" : "0xF9"
} }
}, },
"idiom" : "universal" "idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "1.000", "blue" : "0.890",
"green" : "1.000", "green" : "0.906",
"red" : "1.000" "red" : "0.976"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0xE6", "blue" : "0x00",
"green" : "0xE5", "green" : "0x00",
"red" : "0xE0" "red" : "0x00"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -1,13 +1,18 @@
import ComposableArchitecture import ComposableArchitecture
struct AppState: Equatable { struct AppState: Equatable {
enum Route { enum Route: Equatable {
case startup case startup
case onboarding case onboarding
case home case home
case phraseValidation
case phraseDisplay
} }
var homeState: HomeState var homeState: HomeState
var onboardingState: OnboardingState var onboardingState: OnboardingState
var phraseValidationState: RecoveryPhraseValidationState
var phraseDisplayState: RecoveryPhraseDisplayState
var route: Route = .startup var route: Route = .startup
} }
@ -15,10 +20,11 @@ enum AppAction: Equatable {
case updateRoute(AppState.Route) case updateRoute(AppState.Route)
case home(HomeAction) case home(HomeAction)
case onboarding(OnboardingAction) case onboarding(OnboardingAction)
case phraseDisplay(RecoveryPhraseDisplayAction)
case phraseValidation(RecoveryPhraseValidationAction)
} }
struct AppEnvironment: Equatable { struct AppEnvironment: Equatable {}
}
// MARK: - AppReducer // MARK: - AppReducer
@ -29,7 +35,9 @@ extension AppReducer {
[ [
routeReducer, routeReducer,
homeReducer, homeReducer,
onboardingReducer onboardingReducer,
phraseValidationReducer.debug(),
phraseDisplayReducer.debug()
] ]
) )
@ -37,13 +45,25 @@ extension AppReducer {
switch action { switch action {
case let .updateRoute(route): case let .updateRoute(route):
state.route = route state.route = route
case .home(.reset): case .home(.reset):
state.route = .startup state.route = .startup
case .onboarding(.createNewWallet):
case .onboarding(.createNewWallet),
.phraseValidation(.proceedToHome):
state.route = .home state.route = .home
case .phraseValidation(.displayBackedUpPhrase),
.phraseDisplay(.createPhrase):
state.route = .phraseDisplay
case .phraseDisplay(.finishedPressed):
state.route = .phraseValidation
default: default:
break break
} }
return .none return .none
} }
@ -58,6 +78,18 @@ extension AppReducer {
action: /AppAction.onboarding, action: /AppAction.onboarding,
environment: { _ in } environment: { _ in }
) )
private static let phraseValidationReducer: AppReducer = RecoveryPhraseValidationReducer.default.pullback(
state: \AppState.phraseValidationState,
action: /AppAction.phraseValidation,
environment: { _ in BackupPhraseEnvironment.demo }
)
private static let phraseDisplayReducer: AppReducer = RecoveryPhraseDisplayReducer.default.pullback(
state: \AppState.phraseDisplayState,
action: /AppAction.phraseDisplay,
environment: { _ in BackupPhraseEnvironment.demo }
)
} }
// MARK: - AppStore // MARK: - AppStore
@ -80,7 +112,11 @@ extension AppState {
static var placeholder: Self { static var placeholder: Self {
.init( .init(
homeState: .placeholder, homeState: .placeholder,
onboardingState: .init() onboardingState: .init(),
phraseValidationState: RecoveryPhraseValidationState.placeholder,
phraseDisplayState: RecoveryPhraseDisplayState(
phrase: .placeholder
)
) )
} }
} }

View File

@ -18,6 +18,7 @@ struct AppView: View {
) )
} }
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())
case .onboarding: case .onboarding:
OnboardingScreen( OnboardingScreen(
store: store.scope( store: store.scope(
@ -25,10 +26,45 @@ struct AppView: View {
action: AppAction.onboarding action: AppAction.onboarding
) )
) )
case .startup: case .startup:
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
StartupView(sendAction: viewStore.send) StartupView(sendAction: viewStore.send)
} }
case .phraseValidation:
NavigationView {
RecoveryPhraseBackupValidationView(
store: store.scope(
state: \.phraseValidationState,
action: AppAction.phraseValidation
)
)
.toolbar(
content: {
ToolbarItem(
placement: .navigationBarLeading,
content: {
Button(
action: { viewStore.send(.updateRoute(.startup)) },
label: { Text("Back") }
)
}
)
}
)
.navigationViewStyle(StackNavigationViewStyle())
}
case .phraseDisplay:
NavigationView {
RecoveryPhraseDisplayView(
store: store.scope(
state: \.phraseDisplayState,
action: AppAction.phraseDisplay
)
)
}
} }
} }
} }
@ -43,9 +79,18 @@ private struct StartupView: View {
Button("Go To Home") { Button("Go To Home") {
sendAction(.updateRoute(.home)) sendAction(.updateRoute(.home))
} }
Button("Go To Onboarding") { Button("Go To Onboarding") {
sendAction(.updateRoute(.onboarding)) sendAction(.updateRoute(.onboarding))
} }
Button("Go To Phrase Validation Demo") {
sendAction(.updateRoute(.phraseValidation))
}
Button("Go To Phrase Display Demo") {
sendAction(.updateRoute(.phraseDisplay))
}
} }
} }
.navigationBarTitle("Startup") .navigationBarTitle("Startup")

View File

@ -0,0 +1,43 @@
//
// DropDelegate.swift
// secant-testnet
//
// Created by Francisco Gindre on 11/16/21.
//
import Foundation
import SwiftUI
import OrderedCollections
import ComposableArchitecture
/// Drop delegate that accepts items conforming to `PhraseChip.validationWordTypeIdentifier`
struct WordChipDropDelegate: DropDelegate {
var dropAction: ((PhraseChip.Kind) -> Void)?
func validateDrop(info: DropInfo) -> Bool {
return info.hasItemsConforming(to: [PhraseChip.validationWordTypeIdentifier])
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: [PhraseChip.validationWordTypeIdentifier]).first {
item.loadItem(forTypeIdentifier: PhraseChip.validationWordTypeIdentifier, options: nil) { text, _ in
DispatchQueue.main.async {
if let data = text as? Data {
// Extract string from data
let word = String(decoding: data, as: UTF8.self)
dropAction?(.unassigned(word: word as String))
}
}
}
return true
}
return false
}
}
extension RecoveryPhraseValidationState {
func groupCompleted(index: Int) -> Bool {
validationWords.first(where: { $0.groupIndex == index }) != nil
}
}

View File

@ -50,13 +50,13 @@ extension BackupPhraseEnvironment {
static let demo = Self( static let demo = Self(
mainQueue: DispatchQueue.main.eraseToAnyScheduler(), mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
newPhrase: { Effect(value: .init(words: RecoveryPhrase.demo.words)) }, newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
pasteboard: .test pasteboard: .test
) )
static let live = Self( static let live = Self(
mainQueue: DispatchQueue.main.eraseToAnyScheduler(), mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
newPhrase: { Effect(value: .init(words: RecoveryPhrase.demo.words)) }, newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
pasteboard: .live pasteboard: .live
) )
} }
@ -64,25 +64,33 @@ extension BackupPhraseEnvironment {
typealias RecoveryPhraseDisplayStore = Store<RecoveryPhraseDisplayState, RecoveryPhraseDisplayAction> typealias RecoveryPhraseDisplayStore = Store<RecoveryPhraseDisplayState, RecoveryPhraseDisplayAction>
struct RecoveryPhrase: Equatable { struct RecoveryPhrase: Equatable {
struct Chunk: Hashable { struct Group: Hashable {
var startIndex: Int var startIndex: Int
var words: [String] var words: [String]
} }
let words: [String] let words: [String]
private let chunkSize = 6 private let groupSize = 6
func toChunks() -> [Chunk] { func toGroups() -> [Group] {
let chunks = words.count / chunkSize let chunks = words.count / groupSize
return zip(0 ..< chunks, words.chunked(into: chunkSize)).map { return zip(0 ..< chunks, words.chunked(into: groupSize)).map {
Chunk(startIndex: $0 * chunkSize + 1, words: $1) Group(startIndex: $0 * groupSize + 1, words: $1)
} }
} }
func toString() -> String { func toString() -> String {
words.joined(separator: " ") words.joined(separator: " ")
} }
func words(fromMissingIndices indices: [Int]) -> [PhraseChip.Kind] {
assert((indices.count - 1) * groupSize <= self.words.count)
return indices.enumerated().map { index, position in
.unassigned(word: self.words[(index * groupSize) + position])
}
}
} }
struct RecoveryPhraseDisplayState: Equatable { struct RecoveryPhraseDisplayState: Equatable {

View File

@ -0,0 +1,182 @@
//
// RecoveryPhraseValidation.swift
// secant-testnet
//
// Created by Francisco Gindre on 10/29/21.
//
import Foundation
import ComposableArchitecture
import SwiftUI
typealias RecoveryPhraseValidationStore = Store<RecoveryPhraseValidationState, RecoveryPhraseValidationAction>
typealias RecoveryPhraseValidationViewStore = ViewStore<RecoveryPhraseValidationState, RecoveryPhraseValidationAction>
/// Represents the data of a word that has been placed into an empty position, that will be used
/// to validate the completed phrase when all ValidationWords have been placed.
struct ValidationWord: Equatable {
var groupIndex: Int
var word: String
}
struct RecoveryPhraseValidationState: Equatable {
enum Route: Equatable, CaseIterable {
case success
case failure
}
static let wordGroupSize = 6
static let phraseChunks = 4
var phrase: RecoveryPhrase
var missingIndices: [Int]
var missingWordChips: [PhraseChip.Kind]
var validationWords: [ValidationWord]
var route: Route?
var isComplete: Bool {
!validationWords.isEmpty && validationWords.count == missingIndices.count
}
var isValid: Bool {
guard let resultingPhrase = self.resultingPhrase else { return false }
return resultingPhrase == phrase.words
}
}
extension RecoveryPhraseValidationViewStore {
func bindingForRoute(_ route: RecoveryPhraseValidationState.Route) -> Binding<Bool> {
self.binding(
get: { $0.route == route },
send: { isActive in
return .updateRoute(isActive ? route : nil)
}
)
}
}
extension RecoveryPhraseValidationState {
/// creates an initial `RecoveryPhraseValidationState` with no completions and random missing indices.
/// - Note: Use this function to create a random validation puzzle for a given phrase.
static func random(phrase: RecoveryPhrase) -> Self {
let missingIndices = Self.randomIndices()
let missingWordChipKind = phrase.words(fromMissingIndices: missingIndices)
return RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: missingWordChipKind,
validationWords: []
)
}
}
extension RecoveryPhraseValidationState {
/// Given an array of RecoveryPhraseStepCompletion, missing indices, original phrase and the number of groups it was split into,
/// assembly the resulting phrase. This comes up with the "proposed solution" for the recovery phrase validation challenge.
/// - returns:an array of String containing the recovery phrase words ordered by the original phrase order, or `nil`
/// if a resulting phrase can't be formed becasue the validation state is not complete.
var resultingPhrase: [String]? {
guard missingIndices.count == validationWords.count else { return nil }
guard validationWords.count == Self.phraseChunks else { return nil }
var words = phrase.words
let groupLength = words.count / Self.phraseChunks
// iterate based on the completions the user did on the UI
for validationWord in validationWords {
// figure out which phrase group (chunk) this completion belongs to
let groupIndex = validationWord.groupIndex
// validate that's the right number
assert(groupIndex < Self.phraseChunks)
// get the missing index that the user did this completion for on the given group
let missingIndex = missingIndices[groupIndex]
// figure out what this means in terms of the whole recovery phrase
let concreteIndex = groupIndex * groupLength + missingIndex
assert(concreteIndex < words.count)
// replace the word on the copy of the original phrase with the completion the user did
words[concreteIndex] = validationWord.word
}
return words
}
static func randomIndices() -> [Int] {
return (0..<phraseChunks).map { _ in
Int.random(in: 0 ..< wordGroupSize)
}
}
}
extension RecoveryPhrase.Group {
/// Returns an array of words where the word at the missing index will be an empty string
func words(with missingIndex: Int) -> [String] {
assert(missingIndex >= 0)
assert(missingIndex < self.words.count)
var wordsApplyingMissing = self.words
wordsApplyingMissing[missingIndex] = ""
return wordsApplyingMissing
}
}
enum RecoveryPhraseValidationAction: Equatable {
case updateRoute(RecoveryPhraseValidationState.Route?)
case reset
case move(wordChip: PhraseChip.Kind, intoGroup: Int)
case succeed
case fail
case proceedToHome
case displayBackedUpPhrase
}
typealias RecoveryPhraseValidationReducer = Reducer<RecoveryPhraseValidationState, RecoveryPhraseValidationAction, BackupPhraseEnvironment>
extension RecoveryPhraseValidationReducer {
static let `default` = RecoveryPhraseValidationReducer { state, action, environment in
switch action {
case .reset:
state = RecoveryPhraseValidationState.random(phrase: state.phrase)
case let .move(wordChip, group):
guard
case let PhraseChip.Kind.unassigned(word) = wordChip,
let missingChipIndex = state.missingWordChips.firstIndex(of: wordChip)
else { return .none }
state.missingWordChips[missingChipIndex] = .empty
state.validationWords.append(ValidationWord(groupIndex: group, word: word))
if state.isComplete {
let value: RecoveryPhraseValidationAction = state.isValid ? .succeed : .fail
return Effect(value: value)
.delay(for: 1, scheduler: environment.mainQueue)
.eraseToEffect()
}
return .none
case .succeed:
state.route = .success
case .fail:
state.route = .failure
case .updateRoute(let route):
state.route = route
case .proceedToHome:
break
case .displayBackedUpPhrase:
break
}
return .none
}
}

View File

@ -0,0 +1,46 @@
//
// PhraseChip+WhenDraggable.swift
// secant-testnet
//
// Created by Francisco Gindre on 11/4/21.
//
import Foundation
import SwiftUI
import ComposableArchitecture
extension PhraseChip {
static let validationWordTypeIdentifier = "public.text"
/// Makes a PhraseChip draggable when it is of kind .unassigned
@ViewBuilder func makeDraggable() -> some View {
switch self.kind {
case .unassigned(let word):
self.onDrag {
NSItemProvider(object: word as NSString)
}
default:
self
}
}
}
extension View {
/// Makes a View accept drop types Self.validationWordTypeIdentifier when it is of kind .empty
func whenIsDroppable(_ isDroppable: Bool, dropDelegate: DropDelegate) -> some View {
self.modifier(MakeDroppableModifier(isDroppable: isDroppable, drop: dropDelegate))
}
}
struct MakeDroppableModifier: ViewModifier {
var isDroppable: Bool
var drop: DropDelegate
func body(content: Content) -> some View {
if isDroppable {
content.onDrop(of: [PhraseChip.validationWordTypeIdentifier], delegate: drop)
} else {
content
}
}
}

View File

@ -0,0 +1,283 @@
//
// RecoveryPhraseBackupView.swift
// secant-testnet
//
// Created by Francisco Gindre on 10/29/21.
//
import SwiftUI
import ComposableArchitecture
struct RecoveryPhraseBackupValidationView: View {
let store: RecoveryPhraseValidationStore
var body: some View {
WithViewStore(self.store) { viewStore in
VStack(alignment: .center) {
header(for: viewStore)
.padding(.horizontal)
.padding(.bottom, 10)
ZStack {
Asset.Colors.BackgroundColors.phraseGridDarkGray.color
.edgesIgnoringSafeArea(.bottom)
VStack(alignment: .center, spacing: 35) {
let state = viewStore.state
let groups = state.phrase.toGroups()
ForEach(Array(zip(groups.indices, groups)), id: \.0) { index, group in
WordChipGrid(
state: state,
groupIndex: index,
wordGroup: group,
misingIndex: index
)
.frame(alignment: .center)
.background(Asset.Colors.BackgroundColors.phraseGridDarkGray.color)
.whenIsDroppable(
!state.groupCompleted(index: index),
dropDelegate: WordChipDropDelegate { chipKind in
viewStore.send(.move(wordChip: chipKind, intoGroup: index))
}
)
}
Spacer()
}
.padding()
.padding(.top, 0)
.navigationLinkEmpty(
isActive: viewStore.bindingForRoute(.success),
destination: { ValidationSucceededView(store: store) }
)
.navigationLinkEmpty(
isActive: viewStore.bindingForRoute(.failure),
destination: { ValidationFailedView(store: store) }
)
}
.frame(alignment: .top)
}
.applyScreenBackground()
.scrollableWhenScaledUp()
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(Text("Verify Your Backup"))
}
}
@ViewBuilder func header(for viewStore: RecoveryPhraseValidationViewStore) -> some View {
VStack {
if viewStore.isComplete {
completeHeader(for: viewStore.state)
} else {
Text("Drag the words below to match your backed-up copy.")
.bodyText()
}
viewStore.state.missingWordGrid()
}
.padding(.horizontal, 30)
}
@ViewBuilder func completeHeader(for state: RecoveryPhraseValidationState) -> some View {
if state.isValid {
Text("Congratulations! You validated your secret recovery phrase.")
.bodyText()
} else {
Text("Your placed words did not match your secret recovery phrase")
.bodyText()
}
}
}
private extension RecoveryPhraseValidationState {
@ViewBuilder func missingWordGrid() -> some View {
let columns = Array(
repeating: GridItem(.flexible(minimum: 100, maximum: 120), spacing: 20),
count: 2
)
LazyVGrid(columns: columns, alignment: .center, spacing: 20) {
ForEach(0..<missingWordChips.count) { chipIndex in
PhraseChip(kind: missingWordChips[chipIndex])
.makeDraggable()
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 30
)
}
}
.padding(0)
}
}
extension RecoveryPhraseValidationState {
func wordsChips(
for groupIndex: Int,
groupSize: Int,
from wordGroup: RecoveryPhrase.Group
) -> [PhraseChip.Kind] {
let validationWord = validationWords.first(where: { $0.groupIndex == groupIndex })
return wordGroup.words.enumerated().map { index, word in
guard index == missingIndices[groupIndex] else {
return .ordered(position: (groupSize * groupIndex) + index + 1, word: word)
}
if let completedWord = validationWord?.word {
return .unassigned(word: completedWord)
}
return .empty
}
}
}
extension RecoveryPhraseValidationState {
static let placeholder = RecoveryPhraseValidationState.random(phrase: .placeholder)
static let placeholderStep1 = RecoveryPhraseValidationState(
phrase: .placeholder,
missingIndices: [2, 0, 3, 5],
missingWordChips: [
.unassigned(word: "thank"),
.empty,
.unassigned(word: "boil"),
.unassigned(word: "garlic")
],
validationWords: [
.init(groupIndex: 2, word: "morning")
],
route: nil
)
static let placeholderStep2 = RecoveryPhraseValidationState(
phrase: .placeholder,
missingIndices: [2, 0, 3, 5],
missingWordChips: [
.empty,
.empty,
.unassigned(word: "boil"),
.unassigned(word: "garlic")
],
validationWords: [
.init(groupIndex: 2, word: "morning"),
.init(groupIndex: 0, word: "thank")
],
route: nil
)
static let placeholderStep3 = RecoveryPhraseValidationState(
phrase: .placeholder,
missingIndices: [2, 0, 3, 5],
missingWordChips: [
.empty,
.empty,
.unassigned(word: "boil"),
.empty
],
validationWords: [
.init(groupIndex: 2, word: "morning"),
.init(groupIndex: 0, word: "thank"),
.init(groupIndex: 3, word: "garlic")
],
route: nil
)
static let placeholderStep4 = RecoveryPhraseValidationState(
phrase: .placeholder,
missingIndices: [2, 0, 3, 5],
missingWordChips: [
.empty,
.empty,
.empty,
.empty
],
validationWords: [
.init(groupIndex: 2, word: "morning"),
.init(groupIndex: 0, word: "thank"),
.init(groupIndex: 3, word: "garlic"),
.init(groupIndex: 1, word: "boil")
],
route: nil
)
}
extension RecoveryPhraseValidationStore {
private static let scheduler = DispatchQueue.main
static let demo = Store(
initialState: .placeholder,
reducer: .default,
environment: .demo
)
static let demoStep1 = Store(
initialState: .placeholderStep1,
reducer: .default,
environment: .demo
)
static let demoStep2 = Store(
initialState: .placeholderStep1,
reducer: .default,
environment: .demo
)
static let demoStep3 = Store(
initialState: .placeholderStep3,
reducer: .default,
environment: .demo
)
static let demoStep4 = Store(
initialState: .placeholderStep4,
reducer: .default,
environment: .demo
)
}
private extension WordChipGrid {
init(
state: RecoveryPhraseValidationState,
groupIndex: Int,
wordGroup: RecoveryPhrase.Group,
misingIndex: Int
) {
let chips = state.wordsChips(
for: groupIndex,
groupSize: RecoveryPhraseValidationState.wordGroupSize,
from: wordGroup
)
self.init(chips: chips, coloredChipColor: state.coloredChipColor)
}
}
private extension RecoveryPhraseValidationState {
var coloredChipColor: Color {
if self.isComplete {
return isValid ? Asset.Colors.Buttons.activeButton.color : Asset.Colors.BackgroundColors.red.color
} else {
return Asset.Colors.Buttons.activeButton.color
}
}
}
struct RecoveryPhraseBackupView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
RecoveryPhraseBackupValidationView(store: .demoStep4)
}
NavigationView {
RecoveryPhraseBackupValidationView(store: .demoStep1)
}
NavigationView {
RecoveryPhraseBackupValidationView(store: .demoStep1)
}
.preferredColorScheme(.dark)
}
}

View File

@ -14,13 +14,14 @@ struct RecoveryPhraseDisplayView: View {
var body: some View { var body: some View {
WithViewStore(self.store) { viewStore in WithViewStore(self.store) { viewStore in
ScrollView { ScrollView {
VStack { VStack(alignment: .center, spacing: 0) {
if let chunks = viewStore.phrase?.toChunks() { if let groups = viewStore.phrase?.toGroups() {
VStack(spacing: 20) { VStack(spacing: 20) {
Text("Your Secret Recovery Phrase") Text("Your Secret Recovery Phrase")
.titleText() .titleText()
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .center, spacing: 4) {
Text("The following 24 words represent your funds and the security used to protect them.") Text("The following 24 words represent your funds and the security used to protect them.")
.bodyText() .bodyText()
@ -28,12 +29,17 @@ struct RecoveryPhraseDisplayView: View {
.bodyText() .bodyText()
} }
} }
.padding(.top, 0)
.padding(.bottom, 20)
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 35) {
ForEach(chunks, id: \.startIndex) { chunk in ForEach(groups, id: \.startIndex) { group in
WordChipGrid(words: chunk.words, startingAt: chunk.startIndex) VStack {
WordChipGrid(words: group.words, startingAt: group.startIndex)
}
} }
} }
.padding(.horizontal, 5)
VStack { VStack {
Button( Button(
@ -59,21 +65,21 @@ struct RecoveryPhraseDisplayView: View {
Text("Oops no words") Text("Oops no words")
} }
} }
.padding()
} }
.padding(.bottom, 20)
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 0)
.applyScreenBackground()
} }
.navigationBarTitleDisplayMode(.inline) // TODO: NavigationBar Style
// TODO: NavigationBar Style .navigationBarHidden(true)
.navigationBarTitleDisplayMode(.inline)
.applyScreenBackground()
} }
} }
// TODO: This should have a #DEBUG tag, but if so, it's not possible to compile this on release mode and submit it to testflight // TODO: This should have a #DEBUG tag, but if so, it's not possible to compile this on release mode and submit it to testflight
extension RecoveryPhraseDisplayStore { extension RecoveryPhraseDisplayStore {
static var demo: RecoveryPhraseDisplayStore { static var demo: RecoveryPhraseDisplayStore {
RecoveryPhraseDisplayStore( RecoveryPhraseDisplayStore(
initialState: .init(phrase: .demo), initialState: .init(phrase: .placeholder),
reducer: .default, reducer: .default,
environment: .demo environment: .demo
) )
@ -97,19 +103,19 @@ extension RecoveryPhrase {
"pizza", "just", "garlic" "pizza", "just", "garlic"
] ]
static let demo = RecoveryPhrase(words: testPhrase) static let placeholder = RecoveryPhrase(words: testPhrase)
static let empty = RecoveryPhrase(words: []) static let empty = RecoveryPhrase(words: [])
} }
struct RecoveryPhraseDisplayView_Previews: PreviewProvider { struct RecoveryPhraseDisplayView_Previews: PreviewProvider {
static let scheduler = DispatchQueue.main static let scheduler = DispatchQueue.main
static let store = RecoveryPhraseDisplayStore.demo static let store = RecoveryPhraseDisplayStore.demo
static var previews: some View { static var previews: some View {
NavigationView { NavigationView {
RecoveryPhraseDisplayView(store: store) RecoveryPhraseDisplayView(store: store)
} }
.environment(\.sizeCategory, .accessibilityLarge)
NavigationView { NavigationView {
RecoveryPhraseDisplayView(store: store) RecoveryPhraseDisplayView(store: store)

View File

@ -0,0 +1,78 @@
//
// ValidationFailed.swift
// secant-testnet
//
// Created by Francisco Gindre on 12/22/21.
//
import SwiftUI
import ComposableArchitecture
struct ValidationFailedView: View {
var store: RecoveryPhraseValidationStore
var body: some View {
WithViewStore(store) { viewStore in
GeometryReader { proxy in
VStack {
VStack(alignment: .center, spacing: 20) {
Text("Ouch, sorry, no.")
.font(.custom(FontFamily.Rubik.regular.name, size: 30))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.bottom, 20)
CircularFrame()
.backgroundImage(
Asset.Assets.Backgrounds.callout1.image
)
.frame(
width: proxy.size.width * 0.84,
height: proxy.size.width * 0.84
)
.badgeIcon(.error)
Spacer()
VStack(alignment: .center, spacing: 40) {
VStack(alignment: .center, spacing: 20) {
Text("Your placed words did not match your secret recovery phrase.")
.bodyText()
.fixedSize(horizontal: false, vertical: true)
Text("Remember, you can't recover your funds if you lose (or incorrectly save) these 24 words.")
.bodyText()
.fixedSize(horizontal: false, vertical: true)
}
Button(
action: { viewStore.send(.reset) },
label: { Text("I'm ready to try again") }
)
.activeButtonStyle
.frame(height: 60)
}
.padding()
Spacer()
}
.frame(width: proxy.size.width)
.scrollableWhenScaledUp()
}
.padding()
.navigationBarBackButtonHidden(true)
.applyErredScreenBackground()
}
}
}
struct ValidationFailed_Previews: PreviewProvider {
static var previews: some View {
Group {
ValidationFailedView(store: .demo)
ValidationFailedView(store: .demo)
.environment(\.sizeCategory, .accessibilityLarge)
}
}
}

View File

@ -0,0 +1,105 @@
//
// SuccessView.swift
// secant-testnet
//
// Created by Adam Stener on 12/8/21.
//
import SwiftUI
import ComposableArchitecture
struct ValidationSucceededView: View {
var store: RecoveryPhraseValidationStore
@ScaledMetric var scaledPadding: CGFloat = 10
@ScaledMetric var scaledButtonHeight: CGFloat = 130
var body: some View {
WithViewStore(store) { viewStore in
GeometryReader { proxy in
VStack {
VStack(spacing: 20) {
Text("Success!")
.font(.custom(FontFamily.Rubik.regular.name, size: 36))
Text("Place that backup somewhere safe and venture forth in security.")
.bodyText()
.multilineTextAlignment(.center)
.lineSpacing(2)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
VStack {
CircularFrame()
.backgroundImage(
Asset.Assets.Backgrounds.callout1.image
)
.frame(
width: proxy.size.width * 0.84,
height: proxy.size.width * 0.84
)
.badgeIcon(.shield)
}
.padding(.vertical, 20)
Spacer()
VStack(spacing: 15) {
Button(
action: {
viewStore.send(.proceedToHome, animation: .easeIn(duration: 1))
},
label: {
Text("Take me to my wallet!")
.fixedSize(horizontal: false, vertical: true)
}
)
.activeButtonStyle
.frame(
minHeight: 60,
idealHeight: 60,
maxHeight: .infinity
)
Button(
action: {
viewStore.send(
.displayBackedUpPhrase,
animation: .easeIn(duration: 1)
)
},
label: {
Text("Show me my phrase again")
.fixedSize(horizontal: false, vertical: true)
}
)
.secondaryButtonStyle
.frame(
minHeight: 60,
idealHeight: 60,
maxHeight: .infinity
)
}
.frame(height: scaledButtonHeight)
.padding(.vertical, scaledPadding)
}
.padding(.horizontal)
.scrollableWhenScaledUp()
}
}
.navigationBarBackButtonHidden(true)
.applyScreenBackground()
}
}
struct ValidationSuccededView_Previews: PreviewProvider {
static var previews: some View {
Group {
ValidationSucceededView(store: .demo)
ValidationSucceededView(store: .demo)
.environment(\.sizeCategory, .accessibilityExtraLarge)
}
}
}

View File

@ -7,16 +7,14 @@
import SwiftUI import SwiftUI
/** /// A 3x(N/3) grid of numbered or empty chips.
A 3x2 grid of numbered or empty chips.
*/
struct WordChipGrid: View { struct WordChipGrid: View {
static let spacing: CGFloat = 10 static let spacing: CGFloat = 10
var chips: [PhraseChip.Kind] var chips: [PhraseChip.Kind]
var coloredChipColor: Color
var threeColumnGrid = Array( var threeColumnGrid = Array(
repeating: GridItem( repeating: GridItem(
.flexible(minimum: 60, maximum: 120), .flexible(minimum: 100, maximum: 120),
spacing: Self.spacing, spacing: Self.spacing,
alignment: .topLeading alignment: .topLeading
), ),
@ -24,52 +22,37 @@ struct WordChipGrid: View {
) )
var body: some View { var body: some View {
LazyVGrid( LazyVGrid(columns: threeColumnGrid, alignment: .center, spacing: 10) {
columns: threeColumnGrid, ForEach(chips, id: \.self) { item in
alignment: .leading, PhraseChip(kind: item)
spacing: Self.spacing
) {
ForEach(chips, id: \.self) { wordChip in
chipView(for: wordChip)
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 30
)
} }
} }
} }
init(words: [String], startingAt index: Int) { init(chips: [PhraseChip.Kind], coloredChipColor: Color) {
self.chips = zip(words, index..<index + words.count).map({ word, index in self.chips = chips
word.isEmpty ? .empty : .ordered(position: index, word: word) self.coloredChipColor = coloredChipColor
})
} }
@ViewBuilder func chipView(for chipKind: PhraseChip.Kind) -> some View { init(words: [String], startingAt index: Int, coloredChipColor: Color = .clear) {
switch chipKind { let chips = zip(words, index..<index + words.count).map { word, index in
case .empty: word.isEmpty ? PhraseChip.Kind.empty : .ordered(position: index, word: word)
EmptyChip()
case let .ordered(position, word):
EnumeratedChip(index: position, text: word)
case .unassigned(let word):
BlueChip(word: word)
} }
self.init(chips: chips, coloredChipColor: coloredChipColor)
} }
} }
struct WordChipGrid_Previews: PreviewProvider { struct WordChipGrid_Previews: PreviewProvider {
private static var words = [ private static var words = [
"pyramid", "negative", "page", "pyramid", "negative", "page",
"crown", "", "zebra" "morning", "", "zebra"
] ]
static var previews: some View { static var previews: some View {
VStack { WordChipGrid(words: words, startingAt: 1)
WordChipGrid(words: words, startingAt: 1) .frame(maxHeight: .infinity)
} .fixedSize()
.padding() .environment(\.sizeCategory, .accessibilityLarge)
.padding()
} }
} }

View File

@ -45,7 +45,8 @@ extension HomeReducer {
action: /HomeAction.profile, action: /HomeAction.profile,
environment: { _ in environment: { _ in
return ProfileEnvironment() return ProfileEnvironment()
}) }
)
.run(&state, action, ()) .run(&state, action, ())
case .reset: case .reset:
return .none return .none
@ -103,7 +104,6 @@ extension HomeViewStore {
} }
// MARK: PlaceHolders // MARK: PlaceHolders
extension HomeState { extension HomeState {
static var placeholder: Self { static var placeholder: Self {
.init( .init(

View File

@ -24,7 +24,8 @@ struct HomeView: View {
initialState: .placeholder, initialState: .placeholder,
reducer: SendReducer.default( reducer: SendReducer.default(
whenDone: { HomeViewStore(store).send(.updateRoute(nil)) } whenDone: { HomeViewStore(store).send(.updateRoute(nil)) }
).debug(), )
.debug(),
environment: () environment: ()
) )
) )
@ -45,13 +46,13 @@ struct HomeView: View {
List { List {
Section(header: Text("Navigation Stack Routes")) { Section(header: Text("Navigation Stack Routes")) {
ForEach(navigationRouteValues) { routeValue in ForEach(navigationRouteValues) { routeValue in
Text("\(String(describing: routeValue.route))") Text("\(String(describing: routeValue.route))")
.navigationLink( .navigationLink(
isActive: viewStore.bindingForRoute(routeValue.route), isActive: viewStore.bindingForRoute(routeValue.route),
destination: { destination: {
view(for: routeValue.route) view(for: routeValue.route)
} }
) )
} }
} }

View File

@ -40,6 +40,7 @@ internal enum Asset {
internal enum BackgroundColors { internal enum BackgroundColors {
internal static let numberedChip = ColorAsset(name: "numberedChip") internal static let numberedChip = ColorAsset(name: "numberedChip")
internal static let phraseGridDarkGray = ColorAsset(name: "phraseGridDarkGray") internal static let phraseGridDarkGray = ColorAsset(name: "phraseGridDarkGray")
internal static let red = ColorAsset(name: "red")
} }
internal enum Buttons { internal enum Buttons {
internal static let activeButton = ColorAsset(name: "ActiveButton") internal static let activeButton = ColorAsset(name: "ActiveButton")
@ -67,6 +68,8 @@ internal enum Asset {
internal enum ScreenBackground { internal enum ScreenBackground {
internal static let gradientEnd = ColorAsset(name: "gradientEnd") internal static let gradientEnd = ColorAsset(name: "gradientEnd")
internal static let gradientStart = ColorAsset(name: "gradientStart") internal static let gradientStart = ColorAsset(name: "gradientStart")
internal static let redGradientEnd = ColorAsset(name: "redGradientEnd")
internal static let redGradientStart = ColorAsset(name: "redGradientStart")
} }
internal enum Shadow { internal enum Shadow {
internal static let emptyChipInnerShadow = ColorAsset(name: "emptyChipInnerShadow") internal static let emptyChipInnerShadow = ColorAsset(name: "emptyChipInnerShadow")

View File

@ -15,9 +15,6 @@ struct OnboardingScreen: View {
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
ZStack { ZStack {
ScreenBackground()
.edgesIgnoringSafeArea(.all)
OnboardingHeaderView( OnboardingHeaderView(
store: store.scope( store: store.scope(
state: { state in state: { state in
@ -45,6 +42,7 @@ struct OnboardingScreen: View {
OnboardingFooterView(store: store) OnboardingFooterView(store: store)
} }
} }
.applyScreenBackground()
} }
} }

View File

@ -6,14 +6,12 @@
// //
import SwiftUI import SwiftUI
/**
A Vertical LinearGradient that takes an array of Colors and renders them vertically in a centered fashion mostly used as a background for Screen views.. /// A Vertical LinearGradient that takes an array of Colors and renders them vertically
*/ /// in a centered fashion mostly used as a background for Screen views..
struct ScreenBackground: View { struct ScreenBackground: View {
var colors = [ var colors: [Color]
Asset.Colors.ScreenBackground.gradientStart.color,
Asset.Colors.ScreenBackground.gradientEnd.color
]
var body: some View { var body: some View {
LinearGradient( LinearGradient(
colors: colors, colors: colors,
@ -24,22 +22,40 @@ struct ScreenBackground: View {
} }
struct ScreenBackgroundModifier: ViewModifier { struct ScreenBackgroundModifier: ViewModifier {
var colors: [Color]
func body(content: Content) -> some View { func body(content: Content) -> some View {
ZStack { ZStack {
ScreenBackground() ScreenBackground(colors: colors)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
content content
} }
} }
} }
extension View { extension View {
/** /// Adds a Vertical Linear Gradient with the default Colors of VLinearGradient.
Adds a Vertical Linear Gradient with the default Colors of VLinearGradient. Supports both Light and Dark Mode /// Supports both Light and Dark Mode
*/
func applyScreenBackground() -> some View { func applyScreenBackground() -> some View {
self.modifier( self.modifier(
ScreenBackgroundModifier() ScreenBackgroundModifier(
colors: [
Asset.Colors.ScreenBackground.gradientStart.color,
Asset.Colors.ScreenBackground.gradientEnd.color
]
)
)
}
func applyErredScreenBackground() -> some View {
self.modifier(
ScreenBackgroundModifier(
colors: [
Asset.Colors.ScreenBackground.redGradientStart.color,
Asset.Colors.ScreenBackground.redGradientEnd.color
]
)
) )
} }
} }

View File

@ -7,28 +7,31 @@
import SwiftUI import SwiftUI
struct BlueChip: View { struct ColoredChip: View {
var word: String var word: String
var color = Asset.Colors.Buttons.activeButton.color
var body: some View { var body: some View {
Text(word) Text(word)
.font(FontFamily.Rubik.regular.textStyle(.body)) .font(.custom(FontFamily.Rubik.regular.name, size: 15))
.frame( .frame(
minWidth: 0, minWidth: 0,
maxWidth: 120, maxWidth: .infinity,
minHeight: 30, minHeight: 30,
idealHeight: 40 maxHeight: .infinity
) )
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(Asset.Colors.Text.activeButtonText.color) .foregroundColor(Asset.Colors.Text.activeButtonText.color)
.padding(.horizontal, 4) .padding(.horizontal, 8)
.padding(.vertical, 4) .padding(.vertical, 4)
.background(Asset.Colors.Buttons.activeButton.color) .background(color)
.cornerRadius(6) .cornerRadius(6)
} }
} }
struct BlueChip_Previews: PreviewProvider { struct ColoredChip_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
BlueChip(word: "negative") ColoredChip(word: "negative")
.frame(width: 115)
.applyScreenBackground() .applyScreenBackground()
} }
} }

View File

@ -8,6 +8,8 @@
import SwiftUI import SwiftUI
struct EmptyChip: View { struct EmptyChip: View {
@Environment(\.colorScheme) var colorScheme
var body: some View { var body: some View {
RoundedRectangle(cornerRadius: 6, style: RoundedCornerStyle.continuous) RoundedRectangle(cornerRadius: 6, style: RoundedCornerStyle.continuous)
.stroke(Asset.Colors.Text.activeButtonText.color, lineWidth: 0.5) .stroke(Asset.Colors.Text.activeButtonText.color, lineWidth: 0.5)
@ -24,15 +26,24 @@ struct EmptyChip: View {
width: 4, width: 4,
blur: 2 blur: 2
) )
.background(chipBackground)
.frame( .frame(
minWidth: 0, minWidth: 0,
maxWidth: .infinity, maxWidth: .infinity,
minHeight: 40, minHeight: 40,
idealHeight: 40,
maxHeight: .infinity, maxHeight: .infinity,
alignment: .leading alignment: .leading
) )
} }
@ViewBuilder var chipBackground: some View {
if colorScheme == .dark {
RoundedRectangle(cornerRadius: 6, style: RoundedCornerStyle.continuous)
.fill(Asset.Colors.ScreenBackground.gradientEnd.color)
} else {
Color.clear
}
}
} }
struct EmptyChip_Previews: PreviewProvider { struct EmptyChip_Previews: PreviewProvider {
@ -49,7 +60,7 @@ struct EmptyChip_Previews: PreviewProvider {
Group { Group {
ZStack { ZStack {
ScreenBackground() Color.gray
EmptyChip() EmptyChip()
.frame(width: 100, height: 40, alignment: .leading) .frame(width: 100, height: 40, alignment: .leading)
} }

View File

@ -8,102 +8,57 @@
import SwiftUI import SwiftUI
struct EnumeratedChip: View { struct EnumeratedChip: View {
let basePadding: CGFloat = 14
@Clamped(1...24) @Clamped(1...24)
var index: Int = 1 var index: Int = 1
var text: String var text: String
var overlayPadding: CGFloat = 20
var body: some View { var body: some View {
NumberedText(number: index, text: text) Text(text)
.foregroundColor(Asset.Colors.Text.button.color)
.font(.custom(FontFamily.Rubik.regular.name, size: 14))
.frame( .frame(
minWidth: 0,
maxWidth: .infinity, maxWidth: .infinity,
minHeight: 30, minHeight: 30,
maxHeight: .infinity, maxHeight: .infinity,
alignment: .leading alignment: .leading
) )
.padding(.leading, 14) .padding(.leading, basePadding + overlayPadding)
.padding(.vertical, 4) .padding([.trailing, .vertical], 4)
.background(Asset.Colors.BackgroundColors.numberedChip.color) .fixedSize(horizontal: false, vertical: true)
.cornerRadius(6)
.shadow(color: Asset.Colors.Shadow.numberedTextShadow.color, radius: 3, x: 0, y: 1)
}
}
struct NumberedText: View {
var number: Int = 1
var text: String
@ViewBuilder var numberedText: some View {
GeometryReader { geometry in
(Text("\(number)")
.baselineOffset(geometry.size.height / 4)
.foregroundColor(Asset.Colors.Text.highlightedSuperscriptText.color)
.font(.custom(FontFamily.Roboto.bold.name, size: 12)) +
Text(" \(text)")
.foregroundColor(Asset.Colors.Text.button.color)
.font(.custom(FontFamily.Rubik.regular.name, size: 14))
)
.shadow( .shadow(
color: Asset.Colors.Shadow.numberedTextShadow.color, color: Asset.Colors.Shadow.numberedTextShadow.color,
radius: 1, radius: 1,
x: 0, x: 0,
y: 1 y: 1
) )
.fixedSize(horizontal: false, vertical: true) .background(Asset.Colors.BackgroundColors.numberedChip.color)
.frame(height: geometry.size.height, alignment: .center) .cornerRadius(6)
} .shadow(color: Asset.Colors.Shadow.numberedTextShadow.color, radius: 3, x: 0, y: 1)
} .overlay(
GeometryReader { geometry in
var body: some View { Text("\(index)")
numberedText .foregroundColor(Asset.Colors.Text.highlightedSuperscriptText.color)
.layoutPriority(1) .font(.custom(FontFamily.Roboto.bold.name, size: 10))
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
.padding(.leading, basePadding)
.padding(.top, 4)
}
)
} }
} }
struct EnumeratedChip_Previews: PreviewProvider { struct EnumeratedChip_Previews: PreviewProvider {
private static var threeColumnGrid = Array(
repeating: GridItem(
.flexible(minimum: 60, maximum: 120),
spacing: 15,
alignment: .topLeading
),
count: 3
)
private static var words = [ private static var words = [
"pyramid", "negative", "page", "pyramid", "negative", "page",
"crown", "", "zebra" "crown", "", "zebra"
] ]
@ViewBuilder static var grid: some View { @ViewBuilder static var grid: some View {
LazyVGrid( WordChipGrid(words: words, startingAt: 1)
columns: threeColumnGrid,
alignment: .leading,
spacing: 15
) {
ForEach(Array(zip(words.indices, words)), id: \.1) { i, word in
if word.isEmpty {
EmptyChip()
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 40,
maxHeight: .infinity
)
} else {
EnumeratedChip(index: (i + 1), text: word)
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 30,
maxHeight: .infinity
)
}
}
}
.padding()
} }
static var previews: some View { static var previews: some View {

View File

@ -17,26 +17,13 @@ struct PhraseChip: View {
var kind: Kind var kind: Kind
var body: some View { var body: some View {
chipFor(for: kind)
.frame(
minWidth: 0,
maxWidth: 120,
minHeight: 30,
idealHeight: 40
)
.animation(.easeIn)
}
@ViewBuilder func chipFor(for kind: Kind) -> some View {
switch kind { switch kind {
case .empty: case .empty:
EmptyChip() EmptyChip()
case let .ordered(position, word): case let .ordered(position, word):
EnumeratedChip(index: position, text: word) EnumeratedChip(index: position, text: word)
case .unassigned(let word): case .unassigned(let word):
BlueChip(word: word) ColoredChip(word: word)
} }
} }
} }
@ -45,13 +32,13 @@ struct PhraseChip_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
VStack { VStack {
PhraseChip(kind: .unassigned(word: "negative")) PhraseChip(kind: .unassigned(word: "negative"))
.frame(height: 40) .frame(width: 120, height: 40)
PhraseChip(kind: .empty) PhraseChip(kind: .empty)
.frame(height: 40) .frame(width: 120, height: 40)
PhraseChip(kind: .ordered(position: 23, word: "mutual")) PhraseChip(kind: .ordered(position: 23, word: "mutual"))
.frame(height: 40) .frame(width: 120, height: 40)
} }
.applyScreenBackground() .applyScreenBackground()
} }

View File

@ -12,16 +12,39 @@ enum Badge: Equatable {
case shield case shield
case list case list
case person case person
case error
var image: Image { @ViewBuilder var image: some View {
switch self { switch self {
case .shield: return Asset.Assets.Icons.shield.image case .shield:
case .list: return Asset.Assets.Icons.list.image Asset.Assets.Icons.shield.image
case .person: return Asset.Assets.Icons.profile.image .resizable()
.renderingMode(.none)
case .list:
Asset.Assets.Icons.list.image
.resizable()
.renderingMode(.none)
case .person:
Asset.Assets.Icons.profile.image
.resizable()
.renderingMode(.none)
case .error:
ErrorBadge()
} }
} }
} }
struct ErrorBadge: View {
var body: some View {
Text("X")
.font(.custom(FontFamily.Rubik.bold.name, size: 36))
.foregroundColor(Asset.Colors.BackgroundColors.red.color)
.frame(width: 60, height: 55, alignment: .center)
.background(Asset.Colors.BackgroundColors.numberedChip.color)
.cornerRadius(10)
}
}
struct BadgesOverlay: Animatable, ViewModifier { struct BadgesOverlay: Animatable, ViewModifier {
struct ViewState: Equatable { struct ViewState: Equatable {
let index: Int let index: Int
@ -44,8 +67,6 @@ struct BadgesOverlay: Animatable, ViewModifier {
ZStack { ZStack {
ForEach(0..<viewStore.badges.count) { badgeIndex in ForEach(0..<viewStore.badges.count) { badgeIndex in
viewStore.badges[viewStore.index].image viewStore.badges[viewStore.index].image
.resizable()
.renderingMode(.none)
.frame( .frame(
width: proxy.size.width * 0.35, width: proxy.size.width * 0.35,
height: proxy.size.height * 0.35, height: proxy.size.height * 0.35,
@ -82,7 +103,6 @@ struct BadgeOverlay: Animatable, ViewModifier {
Spacer() Spacer()
badge.image badge.image
.resizable()
.frame( .frame(
width: proxy.size.width * 0.35, width: proxy.size.width * 0.35,
height: proxy.size.height * 0.35, height: proxy.size.height * 0.35,
@ -128,6 +148,10 @@ struct Badge_Previews: PreviewProvider {
CircularFrame() CircularFrame()
.frame(width: size, height: size) .frame(width: size, height: size)
.badgeIcon(.person) .badgeIcon(.person)
CircularFrame()
.frame(width: size, height: size)
.badgeIcon(.error)
} }
.preferredColorScheme(.light) .preferredColorScheme(.light)
.previewLayout(.fixed(width: size + 50, height: size + 50)) .previewLayout(.fixed(width: size + 50, height: size + 50))

View File

@ -0,0 +1,29 @@
//
// ScrollableWhenScaled.swift
// secant-testnet
//
// Created by Francisco Gindre on 12/27/21.
//
import SwiftUI
// swiftlint:disable:next private_over_fileprivate strict_fileprivate
fileprivate struct ScrollableWhenScaledUpModifier: ViewModifier {
@ScaledMetric var scale: CGFloat = 1
func body(content: Content) -> some View {
if scale > 1 {
ScrollView {
content
}
} else {
content
}
}
}
extension View {
func scrollableWhenScaledUp() -> some View {
self.modifier(ScrollableWhenScaledUpModifier())
}
}

View File

@ -8,7 +8,7 @@
import XCTest import XCTest
@testable import secant_testnet @testable import secant_testnet
class RecoveryFlowTests: XCTestCase { class RecoveryPhraseBackupTests: XCTestCase {
func testGiven24WordsBIP39ChunkItIntoQuarters() throws { func testGiven24WordsBIP39ChunkItIntoQuarters() throws {
let words = [ let words = [
"bring", "salute", "thank", "bring", "salute", "thank",
@ -25,7 +25,7 @@ class RecoveryFlowTests: XCTestCase {
] ]
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let chunks = phrase.toChunks() let chunks = phrase.toGroups()
XCTAssertEqual(chunks.count, 4) XCTAssertEqual(chunks.count, 4)
XCTAssertEqual(chunks[0].startIndex, 1) XCTAssertEqual(chunks[0].startIndex, 1)

View File

@ -18,13 +18,13 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase {
) )
store.send(.copyToBufferPressed) { store.send(.copyToBufferPressed) {
$0.phrase = .demo $0.phrase = .placeholder
$0.showCopyToBufferAlert = true $0.showCopyToBufferAlert = true
} }
XCTAssertEqual( XCTAssertEqual(
store.environment.pasteboard.getString(), store.environment.pasteboard.getString(),
RecoveryPhrase.demo.toString() RecoveryPhrase.placeholder.toString()
) )
} }
@ -35,8 +35,8 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase {
environment: .demo environment: .demo
) )
store.send(.phraseResponse(.success(.demo))) { store.send(.phraseResponse(.success(.placeholder))) {
$0.phrase = .demo $0.phrase = .placeholder
$0.showCopyToBufferAlert = false $0.showCopyToBufferAlert = false
} }
} }
@ -44,7 +44,7 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase {
private extension RecoveryPhraseDisplayState { private extension RecoveryPhraseDisplayState {
static let test = RecoveryPhraseDisplayState( static let test = RecoveryPhraseDisplayState(
phrase: .demo, phrase: .placeholder,
showCopyToBufferAlert: false showCopyToBufferAlert: false
) )

View File

@ -0,0 +1,657 @@
//
// RecoveryPhraseValidationTests.swift
// secantTests
//
// Created by Francisco Gindre on 10/29/21.
//
// swiftlint:disable type_body_length
import XCTest
import ComposableArchitecture
@testable import secant_testnet
class RecoveryPhraseValidationTests: XCTestCase {
static let testScheduler = DispatchQueue.test
let testEnvironment = BackupPhraseEnvironment(
mainQueue: testScheduler.eraseToAnyScheduler(),
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
pasteboard: .test
)
func testPickWordsFromMissingIndices() throws {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let phrase = RecoveryPhrase(words: words)
let indices = [1, 0, 5, 3]
let expected = ["salute", "boil", "cancel", "pizza"].map({ PhraseChip.Kind.unassigned(word: $0) })
let result = phrase.words(fromMissingIndices: indices)
XCTAssertEqual(expected, result)
}
func testWhenInInitialStepChipIsDraggedIntoGroup1FollowingStepIsIncomplete() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let missingWordChips: [PhraseChip.Kind] = ["salute", "boil", "cancel", "pizza"].map({ PhraseChip.Kind.unassigned(word: $0) })
let initialStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: missingWordChips,
validationWords: []
)
let store = TestStore(initialState: initialStep, reducer: RecoveryPhraseValidationReducer.default, environment: testEnvironment)
let expectedMissingChips = [
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "boil"),
PhraseChip.Kind.unassigned(word: "cancel"),
PhraseChip.Kind.unassigned(word: "pizza")
]
let expectedValidationWords = [ValidationWord(groupIndex: 1, word: "salute")]
store.send(.move(wordChip: .unassigned(word: "salute"), intoGroup: 1)) {
$0.validationWords = expectedValidationWords
$0.missingWordChips = expectedMissingChips
XCTAssertFalse($0.isComplete)
}
}
func testWhenInInitialStepChipIsDraggedIntoGroup0FollowingStepIsIncompleteNextStateIsIncomplete() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let missingWordChips = ["salute", "boil", "cancel", "pizza"].map({ PhraseChip.Kind.unassigned(word: $0) })
let initialStep = RecoveryPhraseValidationState.initial(
phrase: phrase,
missingIndices: missingIndices,
missingWordsChips: missingWordChips
)
let store = TestStore(initialState: initialStep, reducer: RecoveryPhraseValidationReducer.default, environment: testEnvironment)
let expectedMissingChips = [
PhraseChip.Kind.unassigned(word: "salute"),
PhraseChip.Kind.unassigned(word: "boil"),
PhraseChip.Kind.unassigned(word: "cancel"),
PhraseChip.Kind.empty
]
let expectedValidationWords = [ValidationWord(groupIndex: 0, word: "pizza")]
store.send(.move(wordChip: missingWordChips[3], intoGroup: 0)) {
$0.missingWordChips = expectedMissingChips
$0.validationWords = expectedValidationWords
XCTAssertFalse($0.isComplete)
}
}
func testWhenInIncompleteWith2CompletionsAndAChipIsDroppedInGroup3NextStateIsIncomplete() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "boil"),
PhraseChip.Kind.unassigned(word: "cancel"),
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [ValidationWord(groupIndex: 0, word: "salute")]
)
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationReducer.default, environment: testEnvironment)
let expectedMissingWordChips = [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "cancel"),
PhraseChip.Kind.unassigned(word: "pizza")
]
let expectedValidationWords = [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil")
]
store.send(.move(wordChip: PhraseChip.Kind.unassigned(word: "boil"), intoGroup: 1)) {
$0.missingWordChips = expectedMissingWordChips
$0.validationWords = expectedValidationWords
XCTAssertFalse($0.isComplete)
}
}
func testWhenInIncompleteWith2CompletionsAndAChipIsDroppedInGroup2NextStateIsIncomplete() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "cancel"),
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil")
]
)
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationReducer.default, environment: testEnvironment)
let expectedMissingWordChips = [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "pizza")
]
let expectedValidationWords = [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel")
]
store.send(.move(wordChip: PhraseChip.Kind.unassigned(word: "cancel"), intoGroup: 2)) {
$0.missingWordChips = expectedMissingWordChips
$0.validationWords = expectedValidationWords
XCTAssertFalse($0.isComplete)
}
}
func testWhenInIncompleteWith3CompletionsAndAChipIsDroppedInGroup3NextStateIsComplete() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel")
]
)
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationReducer.default, environment: testEnvironment)
let expectedMissingWordChips = [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty
]
let expectedValidationWords = [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel"),
ValidationWord(groupIndex: 3, word: "pizza")
]
store.send(.move(wordChip: PhraseChip.Kind.unassigned(word: "pizza"), intoGroup: 3)) {
$0.missingWordChips = expectedMissingWordChips
$0.validationWords = expectedValidationWords
XCTAssertTrue($0.isComplete)
XCTAssertTrue($0.isValid)
}
Self.testScheduler.advance(by: 2)
store.receive(.succeed) {
XCTAssertTrue($0.isComplete)
$0.route = .success
}
}
func testWhenInIncompleteWith3CompletionsAndAChipIsDroppedInGroup3NextStateIsFailure() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 2, word: "boil"),
ValidationWord(groupIndex: 1, word: "cancel")
]
)
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationReducer.default, environment: testEnvironment)
let expectedMissingWordChips = [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty
]
let expectedValidationWords = [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 2, word: "boil"),
ValidationWord(groupIndex: 1, word: "cancel"),
ValidationWord(groupIndex: 3, word: "pizza")
]
store.send(.move(wordChip: PhraseChip.Kind.unassigned(word: "pizza"), intoGroup: 3)) {
$0.missingWordChips = expectedMissingWordChips
$0.validationWords = expectedValidationWords
XCTAssertTrue($0.isComplete)
}
Self.testScheduler.advance(by: 2)
store.receive(.fail) {
$0.route = .failure
XCTAssertFalse($0.isValid)
}
}
func testWhenAWordGroupDoesNotHaveACompletionItHasAnEmptyChipInTheGivenMissingIndex() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel")
]
)
let result = currentStep.wordsChips(
for: 0,
groupSize: 6,
from: phrase.toGroups()[0]
)
let expected = [
PhraseChip.Kind.ordered(position: 1, word: "bring"),
.empty,
.ordered(position: 3, word: "thank"),
.ordered(position: 4, word: "require"),
.ordered(position: 5, word: "spirit"),
.ordered(position: 6, word: "toe")
]
XCTAssertEqual(expected, result)
}
func testWhenAWordGroupHasACompletionItHasABlueChipWithTheCompletedWordInTheGivenMissingIndex() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel")
]
)
let result = currentStep.wordsChips(
for: 0,
groupSize: 6,
from: phrase.toGroups()[0]
)
let expected = [
PhraseChip.Kind.ordered(position: 1, word: "bring"),
.unassigned(word: "salute"),
.ordered(position: 3, word: "thank"),
.ordered(position: 4, word: "require"),
.ordered(position: 5, word: "spirit"),
.ordered(position: 6, word: "toe")
]
XCTAssertEqual(expected, result)
}
func testWhenRecoveryPhraseValidationStateIsNotCompleteResultingPhraseIsNil() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel")
]
)
XCTAssertNil(currentStep.resultingPhrase)
}
func testRecoveryPhraseValidationStateIsNotCompleteAndNotValidWhenNotCompleted() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: [
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(word: "pizza")
],
validationWords: [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel")
]
)
XCTAssertFalse(currentStep.isComplete)
XCTAssertFalse(currentStep.isValid)
}
func testCreateResultPhraseFromCompletion() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let completion = [
ValidationWord(groupIndex: 0, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 2, word: "cancel"),
ValidationWord(groupIndex: 3, word: "pizza")
]
let result = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: phrase.words(fromMissingIndices: missingIndices),
validationWords: completion,
route: nil
)
XCTAssertTrue(result.isValid)
XCTAssertTrue(result.isComplete)
XCTAssertEqual(words, result.resultingPhrase)
}
func testCreateResultPhraseInvalidPhraseFromCompletion() {
let words = [
"bring", "salute", "thank",
"require", "spirit", "toe",
// second chunk
"boil", "hill", "casino",
"trophy", "drink", "frown",
// third chunk
"bird", "grit", "close",
"morning", "bind", "cancel",
// Fourth chunk
"daughter", "salon", "quit",
"pizza", "just", "garlic"
]
let missingIndices = [1, 0, 5, 3]
let phrase = RecoveryPhrase(words: words)
let completion = [
ValidationWord(groupIndex: 3, word: "salute"),
ValidationWord(groupIndex: 1, word: "boil"),
ValidationWord(groupIndex: 0, word: "cancel"),
ValidationWord(groupIndex: 2, word: "pizza")
]
let result = RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: phrase.words(fromMissingIndices: missingIndices),
validationWords: completion,
route: nil
)
XCTAssertFalse(result.isValid)
XCTAssertTrue(result.isComplete)
XCTAssertNotEqual(words, result.resultingPhrase)
}
}
extension RecoveryPhraseValidationState {
static func initial(
phrase: RecoveryPhrase,
missingIndices: [Int],
missingWordsChips: [PhraseChip.Kind]
) -> Self {
RecoveryPhraseValidationState(
phrase: phrase,
missingIndices: missingIndices,
missingWordChips: missingWordsChips,
validationWords: []
)
}
}