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

@ -10,31 +10,37 @@ import ComposableArchitecture
struct RecoveryPhraseDisplayView: View { struct RecoveryPhraseDisplayView: View {
let store: RecoveryPhraseDisplayStore let store: RecoveryPhraseDisplayStore
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()
Text("Back them up now! There will be a test.") Text("Back them up now! There will be a test.")
.bodyText() .bodyText()
} }
} }
.padding(.top, 0)
VStack(alignment: .leading, spacing: 20) { .padding(.bottom, 20)
ForEach(chunks, id: \.startIndex) { chunk in
WordChipGrid(words: chunk.words, startingAt: chunk.startIndex) VStack(alignment: .leading, spacing: 35) {
ForEach(groups, id: \.startIndex) { group in
VStack {
WordChipGrid(words: group.words, startingAt: group.startIndex)
}
} }
} }
.padding(.horizontal, 5)
VStack { VStack {
Button( Button(
action: { viewStore.send(.finishedPressed) }, action: { viewStore.send(.finishedPressed) },
@ -42,7 +48,7 @@ struct RecoveryPhraseDisplayView: View {
) )
.activeButtonStyle .activeButtonStyle
.frame(height: 60) .frame(height: 60)
Button( Button(
action: { action: {
viewStore.send(.copyToBufferPressed) viewStore.send(.copyToBufferPressed)
@ -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: []
)
}
}