Merge pull request #134 from zcash/feature/recovery-phrase-validation
Recovery Phrase Validation.
This commit is contained in:
commit
58175b5dcb
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1250"
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -14,13 +14,14 @@ struct RecoveryPhraseDisplayView: View {
|
|||
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()
|
||||
|
||||
|
@ -28,12 +29,17 @@ struct RecoveryPhraseDisplayView: View {
|
|||
.bodyText()
|
||||
}
|
||||
}
|
||||
.padding(.top, 0)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 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 {
|
||||
Button(
|
||||
|
@ -59,21 +65,21 @@ struct RecoveryPhraseDisplayView: View {
|
|||
Text("Oops no words")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
// TODO: NavigationBar Style
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.padding(.top, 0)
|
||||
.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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.fixedSize()
|
||||
.environment(\.sizeCategory, .accessibilityLarge)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -24,7 +24,8 @@ struct HomeView: View {
|
|||
initialState: .placeholder,
|
||||
reducer: SendReducer.default(
|
||||
whenDone: { HomeViewStore(store).send(.updateRoute(nil)) }
|
||||
).debug(),
|
||||
)
|
||||
.debug(),
|
||||
environment: ()
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
numberedText
|
||||
.layoutPriority(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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: []
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue