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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
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" : {
"alpha" : "1.000",
"blue" : "0xE3",
"green" : "0xD4",
"red" : "0xC3"
"green" : "0xE7",
"red" : "0xF9"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
"blue" : "0.890",
"green" : "0.906",
"red" : "0.976"
}
},
"idiom" : "universal"

View File

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

View File

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

View File

@ -18,6 +18,7 @@ struct AppView: View {
)
}
.navigationViewStyle(StackNavigationViewStyle())
case .onboarding:
OnboardingScreen(
store: store.scope(
@ -25,10 +26,45 @@ struct AppView: View {
action: AppAction.onboarding
)
)
case .startup:
ZStack(alignment: .topTrailing) {
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") {
sendAction(.updateRoute(.home))
}
Button("Go To Onboarding") {
sendAction(.updateRoute(.onboarding))
}
Button("Go To Phrase Validation Demo") {
sendAction(.updateRoute(.phraseValidation))
}
Button("Go To Phrase Display Demo") {
sendAction(.updateRoute(.phraseDisplay))
}
}
}
.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(
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
newPhrase: { Effect(value: .init(words: RecoveryPhrase.demo.words)) },
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
pasteboard: .test
)
static let live = Self(
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
newPhrase: { Effect(value: .init(words: RecoveryPhrase.demo.words)) },
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
pasteboard: .live
)
}
@ -64,25 +64,33 @@ extension BackupPhraseEnvironment {
typealias RecoveryPhraseDisplayStore = Store<RecoveryPhraseDisplayState, RecoveryPhraseDisplayAction>
struct RecoveryPhrase: Equatable {
struct Chunk: Hashable {
struct Group: Hashable {
var startIndex: Int
var words: [String]
}
let words: [String]
private let chunkSize = 6
private let groupSize = 6
func toChunks() -> [Chunk] {
let chunks = words.count / chunkSize
return zip(0 ..< chunks, words.chunked(into: chunkSize)).map {
Chunk(startIndex: $0 * chunkSize + 1, words: $1)
func toGroups() -> [Group] {
let chunks = words.count / groupSize
return zip(0 ..< chunks, words.chunked(into: groupSize)).map {
Group(startIndex: $0 * groupSize + 1, words: $1)
}
}
func toString() -> String {
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 {

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 {
let store: RecoveryPhraseDisplayStore
var body: some View {
WithViewStore(self.store) { viewStore in
ScrollView {
VStack {
if let chunks = viewStore.phrase?.toChunks() {
VStack(alignment: .center, spacing: 0) {
if let groups = viewStore.phrase?.toGroups() {
VStack(spacing: 20) {
Text("Your Secret Recovery Phrase")
.titleText()
.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.")
.bodyText()
Text("Back them up now! There will be a test.")
.bodyText()
}
}
VStack(alignment: .leading, spacing: 20) {
ForEach(chunks, id: \.startIndex) { chunk in
WordChipGrid(words: chunk.words, startingAt: chunk.startIndex)
.padding(.top, 0)
.padding(.bottom, 20)
VStack(alignment: .leading, spacing: 35) {
ForEach(groups, id: \.startIndex) { group in
VStack {
WordChipGrid(words: group.words, startingAt: group.startIndex)
}
}
}
.padding(.horizontal, 5)
VStack {
Button(
action: { viewStore.send(.finishedPressed) },
@ -42,7 +48,7 @@ struct RecoveryPhraseDisplayView: View {
)
.activeButtonStyle
.frame(height: 60)
Button(
action: {
viewStore.send(.copyToBufferPressed)
@ -59,21 +65,21 @@ struct RecoveryPhraseDisplayView: View {
Text("Oops no words")
}
}
.padding()
}
.padding(.bottom, 20)
.padding(.horizontal)
.padding(.top, 0)
.applyScreenBackground()
}
// TODO: NavigationBar Style
.navigationBarTitleDisplayMode(.inline)
.applyScreenBackground()
.navigationBarTitleDisplayMode(.inline) // TODO: NavigationBar Style
.navigationBarHidden(true)
}
}
// 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 {
static var demo: RecoveryPhraseDisplayStore {
RecoveryPhraseDisplayStore(
initialState: .init(phrase: .demo),
initialState: .init(phrase: .placeholder),
reducer: .default,
environment: .demo
)
@ -97,19 +103,19 @@ extension RecoveryPhrase {
"pizza", "just", "garlic"
]
static let demo = RecoveryPhrase(words: testPhrase)
static let placeholder = RecoveryPhrase(words: testPhrase)
static let empty = RecoveryPhrase(words: [])
}
struct RecoveryPhraseDisplayView_Previews: PreviewProvider {
static let scheduler = DispatchQueue.main
static let store = RecoveryPhraseDisplayStore.demo
static var previews: some View {
NavigationView {
RecoveryPhraseDisplayView(store: store)
}
.environment(\.sizeCategory, .accessibilityLarge)
NavigationView {
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
/**
A 3x2 grid of numbered or empty chips.
*/
/// A 3x(N/3) grid of numbered or empty chips.
struct WordChipGrid: View {
static let spacing: CGFloat = 10
var chips: [PhraseChip.Kind]
var coloredChipColor: Color
var threeColumnGrid = Array(
repeating: GridItem(
.flexible(minimum: 60, maximum: 120),
.flexible(minimum: 100, maximum: 120),
spacing: Self.spacing,
alignment: .topLeading
),
@ -24,52 +22,37 @@ struct WordChipGrid: View {
)
var body: some View {
LazyVGrid(
columns: threeColumnGrid,
alignment: .leading,
spacing: Self.spacing
) {
ForEach(chips, id: \.self) { wordChip in
chipView(for: wordChip)
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 30
)
LazyVGrid(columns: threeColumnGrid, alignment: .center, spacing: 10) {
ForEach(chips, id: \.self) { item in
PhraseChip(kind: item)
}
}
}
init(words: [String], startingAt index: Int) {
self.chips = zip(words, index..<index + words.count).map({ word, index in
word.isEmpty ? .empty : .ordered(position: index, word: word)
})
init(chips: [PhraseChip.Kind], coloredChipColor: Color) {
self.chips = chips
self.coloredChipColor = coloredChipColor
}
@ViewBuilder func chipView(for chipKind: PhraseChip.Kind) -> some View {
switch chipKind {
case .empty:
EmptyChip()
case let .ordered(position, word):
EnumeratedChip(index: position, text: word)
case .unassigned(let word):
BlueChip(word: word)
init(words: [String], startingAt index: Int, coloredChipColor: Color = .clear) {
let chips = zip(words, index..<index + words.count).map { word, index in
word.isEmpty ? PhraseChip.Kind.empty : .ordered(position: index, word: word)
}
self.init(chips: chips, coloredChipColor: coloredChipColor)
}
}
struct WordChipGrid_Previews: PreviewProvider {
private static var words = [
"pyramid", "negative", "page",
"crown", "", "zebra"
"morning", "", "zebra"
]
static var previews: some View {
VStack {
WordChipGrid(words: words, startingAt: 1)
}
.padding()
WordChipGrid(words: words, startingAt: 1)
.frame(maxHeight: .infinity)
.fixedSize()
.environment(\.sizeCategory, .accessibilityLarge)
.padding()
}
}

View File

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

View File

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

View File

@ -40,6 +40,7 @@ internal enum Asset {
internal enum BackgroundColors {
internal static let numberedChip = ColorAsset(name: "numberedChip")
internal static let phraseGridDarkGray = ColorAsset(name: "phraseGridDarkGray")
internal static let red = ColorAsset(name: "red")
}
internal enum Buttons {
internal static let activeButton = ColorAsset(name: "ActiveButton")
@ -67,6 +68,8 @@ internal enum Asset {
internal enum ScreenBackground {
internal static let gradientEnd = ColorAsset(name: "gradientEnd")
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 static let emptyChipInnerShadow = ColorAsset(name: "emptyChipInnerShadow")

View File

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

View File

@ -6,14 +6,12 @@
//
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 {
var colors = [
Asset.Colors.ScreenBackground.gradientStart.color,
Asset.Colors.ScreenBackground.gradientEnd.color
]
var colors: [Color]
var body: some View {
LinearGradient(
colors: colors,
@ -24,22 +22,40 @@ struct ScreenBackground: View {
}
struct ScreenBackgroundModifier: ViewModifier {
var colors: [Color]
func body(content: Content) -> some View {
ZStack {
ScreenBackground()
ScreenBackground(colors: colors)
.edgesIgnoringSafeArea(.all)
content
}
}
}
extension View {
/**
Adds a Vertical Linear Gradient with the default Colors of VLinearGradient. Supports both Light and Dark Mode
*/
/// Adds a Vertical Linear Gradient with the default Colors of VLinearGradient.
/// Supports both Light and Dark Mode
func applyScreenBackground() -> some View {
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
struct BlueChip: View {
struct ColoredChip: View {
var word: String
var color = Asset.Colors.Buttons.activeButton.color
var body: some View {
Text(word)
.font(FontFamily.Rubik.regular.textStyle(.body))
.font(.custom(FontFamily.Rubik.regular.name, size: 15))
.frame(
minWidth: 0,
maxWidth: 120,
maxWidth: .infinity,
minHeight: 30,
idealHeight: 40
maxHeight: .infinity
)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(Asset.Colors.Text.activeButtonText.color)
.padding(.horizontal, 4)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Asset.Colors.Buttons.activeButton.color)
.background(color)
.cornerRadius(6)
}
}
struct BlueChip_Previews: PreviewProvider {
struct ColoredChip_Previews: PreviewProvider {
static var previews: some View {
BlueChip(word: "negative")
ColoredChip(word: "negative")
.frame(width: 115)
.applyScreenBackground()
}
}

View File

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

View File

@ -8,102 +8,57 @@
import SwiftUI
struct EnumeratedChip: View {
let basePadding: CGFloat = 14
@Clamped(1...24)
var index: Int = 1
var text: String
var overlayPadding: CGFloat = 20
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(
minWidth: 0,
maxWidth: .infinity,
minHeight: 30,
maxHeight: .infinity,
alignment: .leading
)
.padding(.leading, 14)
.padding(.vertical, 4)
.background(Asset.Colors.BackgroundColors.numberedChip.color)
.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))
)
.padding(.leading, basePadding + overlayPadding)
.padding([.trailing, .vertical], 4)
.fixedSize(horizontal: false, vertical: true)
.shadow(
color: Asset.Colors.Shadow.numberedTextShadow.color,
radius: 1,
x: 0,
y: 1
)
.fixedSize(horizontal: false, vertical: true)
.frame(height: geometry.size.height, alignment: .center)
}
}
var body: some View {
numberedText
.layoutPriority(1)
.background(Asset.Colors.BackgroundColors.numberedChip.color)
.cornerRadius(6)
.shadow(color: Asset.Colors.Shadow.numberedTextShadow.color, radius: 3, x: 0, y: 1)
.overlay(
GeometryReader { geometry in
Text("\(index)")
.foregroundColor(Asset.Colors.Text.highlightedSuperscriptText.color)
.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 {
private static var threeColumnGrid = Array(
repeating: GridItem(
.flexible(minimum: 60, maximum: 120),
spacing: 15,
alignment: .topLeading
),
count: 3
)
private static var words = [
"pyramid", "negative", "page",
"crown", "", "zebra"
]
@ViewBuilder static var grid: some View {
LazyVGrid(
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()
WordChipGrid(words: words, startingAt: 1)
}
static var previews: some View {

View File

@ -17,26 +17,13 @@ struct PhraseChip: View {
var kind: Kind
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 {
case .empty:
EmptyChip()
case let .ordered(position, word):
EnumeratedChip(index: position, text: word)
case .unassigned(let word):
BlueChip(word: word)
ColoredChip(word: word)
}
}
}
@ -45,13 +32,13 @@ struct PhraseChip_Previews: PreviewProvider {
static var previews: some View {
VStack {
PhraseChip(kind: .unassigned(word: "negative"))
.frame(height: 40)
.frame(width: 120, height: 40)
PhraseChip(kind: .empty)
.frame(height: 40)
.frame(width: 120, height: 40)
PhraseChip(kind: .ordered(position: 23, word: "mutual"))
.frame(height: 40)
.frame(width: 120, height: 40)
}
.applyScreenBackground()
}

View File

@ -12,16 +12,39 @@ enum Badge: Equatable {
case shield
case list
case person
case error
var image: Image {
@ViewBuilder var image: some View {
switch self {
case .shield: return Asset.Assets.Icons.shield.image
case .list: return Asset.Assets.Icons.list.image
case .person: return Asset.Assets.Icons.profile.image
case .shield:
Asset.Assets.Icons.shield.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 ViewState: Equatable {
let index: Int
@ -44,8 +67,6 @@ struct BadgesOverlay: Animatable, ViewModifier {
ZStack {
ForEach(0..<viewStore.badges.count) { badgeIndex in
viewStore.badges[viewStore.index].image
.resizable()
.renderingMode(.none)
.frame(
width: proxy.size.width * 0.35,
height: proxy.size.height * 0.35,
@ -82,7 +103,6 @@ struct BadgeOverlay: Animatable, ViewModifier {
Spacer()
badge.image
.resizable()
.frame(
width: proxy.size.width * 0.35,
height: proxy.size.height * 0.35,
@ -128,6 +148,10 @@ struct Badge_Previews: PreviewProvider {
CircularFrame()
.frame(width: size, height: size)
.badgeIcon(.person)
CircularFrame()
.frame(width: size, height: size)
.badgeIcon(.error)
}
.preferredColorScheme(.light)
.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
@testable import secant_testnet
class RecoveryFlowTests: XCTestCase {
class RecoveryPhraseBackupTests: XCTestCase {
func testGiven24WordsBIP39ChunkItIntoQuarters() throws {
let words = [
"bring", "salute", "thank",
@ -25,7 +25,7 @@ class RecoveryFlowTests: XCTestCase {
]
let phrase = RecoveryPhrase(words: words)
let chunks = phrase.toChunks()
let chunks = phrase.toGroups()
XCTAssertEqual(chunks.count, 4)
XCTAssertEqual(chunks[0].startIndex, 1)

View File

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