Issue #44: Recovery Phrase Validation flow + tests
Rename struct to RecoveryPhraseValidationState. Add docs WordGrid + tests Make Word Groups Droppable and Blue word chips draggable cleanup Rename Stores and adopt aliases and default pattern for reducers Fix drop not working FIX: apply background to header. Spacing Fix compilation errors. Add validation demo to AppView Fix: the empty chips are rendered once, because they are not uniquely identifiable Add the Validation screen to the App Home make mutating functions static fix project warnings Fix Tests Fixed .complete test refactoring the Enum. first step Move given() to RecoveryPhraseValidationState and fix tests Add canary test for computed property of State Move RecoveryPhraseValidationStep to a nested type of RecoveryPhraseValidationState rename RecoveryPhraseValidationState.RecoveryPhraseValidationStep to RecoveryPhraseValidationState.Step Move static functions from RecoveryPhraseValidationStep to RecoveryPhraseValidationState Move creational factory methods together to the same extension Remove unused functions remove associated values from Step enum Fix: Avoid Drop being disable between chips 0.0.1-10 add navigation bar to phrase validation demo Reduce spacing between groups. Code cleanup Remove RecoveryPhraseValidationStep.swift Move remaining code to proper places PR Fixes. Move .initial factory method to test target. Rename given() to apply(chip:group:) and make it a member function Cleanup. Remove .validate, .invalid and .valid states. Figure word chips out of the state. Fix randomIndices() Tie view's title to state Connect Header to State BlueChip is now ColoredChip. Success Screen fix project Connect Success Screen to successful validation Add Red color to backgrounds Validation Failed Screen Connect SuccessValidation Screen to home hide back button on Success view Connect Phrase Display to validation 0.0.1-11 Fix word grid background colors View Modifier to add scrollview when the content is being scaled up by DynamicType Adjust UI spacing and padding to designs Add Placeholder states for SwiftUI Previews Flatten EnumeratedChip Hierarchy Flatten EnumeratedChip view hierarchy Fix: LazyVGrid can't take GeometryReader on its items' bodies Fix: Colored Chip does not adjust Fix: Vertical separation between wordgroups is too tall. Fix: Accesibility fixes for Validation Failed screen Rename ValidationFailedView Accessibility Pass on Validation success screen FIX: Colored chips too big when scaled up Fix: ValidationFailedScreen does not scroll well when scaled up Fix Empty chip shadow color for dark color scheme Fix: chip grid background does not bleed out to bottom of the screen build 12 Fix: pre success/failure screen step shrinks the screen because word grid is missing Resolved PR comments Fixes to resolve PR conversations Fixes to resolve PR conversations Fix PhraseChip preview Make ScrollableWhenScaledUp modifier fileprivate Remove comments and clean up code Fix Swiftlint issues Renamed RecoveryPhraseStepFulfillment to ValidationWord Rename pickWordsFromMissingIndices PR fixes PR suggestions PR suggestions Move words(fromMissingIndices:size) to RecoveryPhrase Make ScrollableWhenScaled struct fileprivate PR Suggestions Part two PR Suggestion changes remove unused PR suggestions suggested rename PR suggestions remove apply(chip:into) move that to Reducer Formatting changes more formatting changes Fix: iPhone 13 Pro Max displays 4 columns instead of three Fix: Recovery Phrase puzzle shows incorrect number of columns and margin alignment on bigger devices Add test to catch state not changing as intended Phrase validation reducer refactor + tests make step computed property a bool make isComplete a single line PR Suggestions Rename ValidationSuccededView.swift Fix Bug: valid phrase should contemplate that the phrase is complete first PR Suggestion refactor and add Unit Tests for resultingPhrase, isComplete, isValid
This commit is contained in:
parent
80bbdc8cda
commit
b75def9cc1
|
@ -7,7 +7,7 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0D185819272723FF0046B928 /* BlueChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D185818272723FF0046B928 /* BlueChip.swift */; };
|
0D185819272723FF0046B928 /* ColoredChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D185818272723FF0046B928 /* ColoredChip.swift */; };
|
||||||
0D18581B272728D60046B928 /* PhraseChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18581A272728D60046B928 /* PhraseChip.swift */; };
|
0D18581B272728D60046B928 /* PhraseChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18581A272728D60046B928 /* PhraseChip.swift */; };
|
||||||
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */; };
|
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */; };
|
||||||
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F726BDEB3500052649 /* MockServices.swift */; };
|
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F726BDEB3500052649 /* MockServices.swift */; };
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
0D354A0926D5A9D000315F45 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0626D5A9D000315F45 /* Services.swift */; };
|
0D354A0926D5A9D000315F45 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0626D5A9D000315F45 /* Services.swift */; };
|
||||||
0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0726D5A9D000315F45 /* KeyStoring.swift */; };
|
0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0726D5A9D000315F45 /* KeyStoring.swift */; };
|
||||||
0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */; };
|
0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */; };
|
||||||
|
0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */; };
|
||||||
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */; };
|
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */; };
|
||||||
0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */; };
|
0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */; };
|
||||||
0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4E7A0826B364170058B01E /* SecantApp.swift */; };
|
0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4E7A0826B364170058B01E /* SecantApp.swift */; };
|
||||||
|
@ -28,6 +29,8 @@
|
||||||
0D535FDF271F4214009A9E3E /* Rubik-VariableFont_wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */; };
|
0D535FDF271F4214009A9E3E /* Rubik-VariableFont_wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */; };
|
||||||
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */; };
|
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */; };
|
||||||
0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5D16F426E24CCF00AD33D1 /* AppError.swift */; };
|
0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5D16F426E24CCF00AD33D1 /* AppError.swift */; };
|
||||||
|
0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6D628A276A528D002FB4CC /* DropDelegate.swift */; };
|
||||||
|
0D7CE63427349B5D0020E050 /* View+WhenDraggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */; };
|
||||||
0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */; };
|
0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */; };
|
||||||
0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */; };
|
0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */; };
|
||||||
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C5272B129C005A6414 /* WordChipGrid.swift */; };
|
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D8A43C5272B129C005A6414 /* WordChipGrid.swift */; };
|
||||||
|
@ -47,9 +50,14 @@
|
||||||
0DACFA9A27209FA70039EEA5 /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */; };
|
0DACFA9A27209FA70039EEA5 /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */; };
|
||||||
0DACFA9C27209FA70039EEA5 /* Roboto-ThinItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */; };
|
0DACFA9C27209FA70039EEA5 /* Roboto-ThinItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */; };
|
||||||
0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */; };
|
0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */; };
|
||||||
|
0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC487C22772574C00BE6A63 /* ValidationSucceededView.swift */; };
|
||||||
|
0DDB6A5127737D4A0012A410 /* ValidationFailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DDB6A5027737D4A0012A410 /* ValidationFailedView.swift */; };
|
||||||
0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */; };
|
0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */; };
|
||||||
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */; };
|
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */; };
|
||||||
0DFE93DF272C6D4B000FCCA5 /* RecoveryFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93DE272C6D4B000FCCA5 /* RecoveryFlowTests.swift */; };
|
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93DE272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift */; };
|
||||||
|
0DFE93E1272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93E0272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift */; };
|
||||||
|
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93E2272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift */; };
|
||||||
|
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */; };
|
||||||
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */; };
|
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */; };
|
||||||
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; };
|
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; };
|
||||||
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; };
|
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; };
|
||||||
|
@ -118,7 +126,7 @@
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0D185818272723FF0046B928 /* BlueChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueChip.swift; sourceTree = "<group>"; };
|
0D185818272723FF0046B928 /* ColoredChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredChip.swift; sourceTree = "<group>"; };
|
||||||
0D18581A272728D60046B928 /* PhraseChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhraseChip.swift; sourceTree = "<group>"; };
|
0D18581A272728D60046B928 /* PhraseChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhraseChip.swift; sourceTree = "<group>"; };
|
||||||
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = "<group>"; };
|
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = "<group>"; };
|
||||||
0D1922F726BDEB3500052649 /* MockServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServices.swift; sourceTree = "<group>"; };
|
0D1922F726BDEB3500052649 /* MockServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServices.swift; sourceTree = "<group>"; };
|
||||||
|
@ -127,6 +135,7 @@
|
||||||
0D354A0626D5A9D000315F45 /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
|
0D354A0626D5A9D000315F45 /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
|
||||||
0D354A0726D5A9D000315F45 /* KeyStoring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyStoring.swift; sourceTree = "<group>"; };
|
0D354A0726D5A9D000315F45 /* KeyStoring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyStoring.swift; sourceTree = "<group>"; };
|
||||||
0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseHandling.swift; sourceTree = "<group>"; };
|
0D354A0826D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseHandling.swift; sourceTree = "<group>"; };
|
||||||
|
0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableWhenScaled.swift; sourceTree = "<group>"; };
|
||||||
0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayView.swift; sourceTree = "<group>"; };
|
0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayView.swift; sourceTree = "<group>"; };
|
||||||
0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayStore.swift; sourceTree = "<group>"; };
|
0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayStore.swift; sourceTree = "<group>"; };
|
||||||
0D4E7A0526B364170058B01E /* secant-testnet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "secant-testnet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
0D4E7A0526B364170058B01E /* secant-testnet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "secant-testnet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -145,6 +154,8 @@
|
||||||
0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Rubik-VariableFont_wght.ttf"; sourceTree = "<group>"; };
|
0D535FDD271F4214009A9E3E /* Rubik-VariableFont_wght.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Rubik-VariableFont_wght.ttf"; sourceTree = "<group>"; };
|
||||||
0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumeratedChip.swift; sourceTree = "<group>"; };
|
0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumeratedChip.swift; sourceTree = "<group>"; };
|
||||||
0D5D16F426E24CCF00AD33D1 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
0D5D16F426E24CCF00AD33D1 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
||||||
|
0D6D628A276A528D002FB4CC /* DropDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+WhenDraggable.swift"; sourceTree = "<group>"; };
|
||||||
0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenBackground.swift; sourceTree = "<group>"; };
|
0D7DF08B271DCC0E00530046 /* ScreenBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenBackground.swift; sourceTree = "<group>"; };
|
||||||
0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantTextStyles.swift; sourceTree = "<group>"; };
|
0D8A43C3272AEEDE005A6414 /* SecantTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantTextStyles.swift; sourceTree = "<group>"; };
|
||||||
0D8A43C5272B129C005A6414 /* WordChipGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordChipGrid.swift; sourceTree = "<group>"; };
|
0D8A43C5272B129C005A6414 /* WordChipGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordChipGrid.swift; sourceTree = "<group>"; };
|
||||||
|
@ -164,9 +175,14 @@
|
||||||
0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Light.ttf"; sourceTree = "<group>"; };
|
0DACFA8D27209FA70039EEA5 /* Roboto-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Light.ttf"; sourceTree = "<group>"; };
|
||||||
0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-ThinItalic.ttf"; sourceTree = "<group>"; };
|
0DACFA8F27209FA70039EEA5 /* Roboto-ThinItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-ThinItalic.ttf"; sourceTree = "<group>"; };
|
||||||
0DB8AA80271DC7520035BC9D /* DesignGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignGuide.swift; sourceTree = "<group>"; };
|
0DB8AA80271DC7520035BC9D /* DesignGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignGuide.swift; sourceTree = "<group>"; };
|
||||||
|
0DC487C22772574C00BE6A63 /* ValidationSucceededView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidationSucceededView.swift; sourceTree = "<group>"; };
|
||||||
|
0DDB6A5027737D4A0012A410 /* ValidationFailedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationFailedView.swift; sourceTree = "<group>"; };
|
||||||
0DF2DC50272344E400FA31E2 /* EmptyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyChip.swift; sourceTree = "<group>"; };
|
0DF2DC50272344E400FA31E2 /* EmptyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyChip.swift; sourceTree = "<group>"; };
|
||||||
0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+InnerShadow.swift"; sourceTree = "<group>"; };
|
0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+InnerShadow.swift"; sourceTree = "<group>"; };
|
||||||
0DFE93DE272C6D4B000FCCA5 /* RecoveryFlowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryFlowTests.swift; sourceTree = "<group>"; };
|
0DFE93DE272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseBackupTests.swift; sourceTree = "<group>"; };
|
||||||
|
0DFE93E0272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseBackupValidationView.swift; sourceTree = "<group>"; };
|
||||||
|
0DFE93E2272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidation.swift; sourceTree = "<group>"; };
|
||||||
|
0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidationTests.swift; sourceTree = "<group>"; };
|
||||||
2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeaderView.swift; sourceTree = "<group>"; };
|
2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeaderView.swift; sourceTree = "<group>"; };
|
||||||
2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
|
2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
|
||||||
2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = "<group>"; };
|
2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -182,7 +198,6 @@
|
||||||
6654C7402715A47300901167 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
|
6654C7402715A47300901167 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
|
||||||
6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStoreTests.swift; sourceTree = "<group>"; };
|
6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStoreTests.swift; sourceTree = "<group>"; };
|
||||||
665C963E272C26E600BC04FB /* CircularFrameBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBackground.swift; sourceTree = "<group>"; };
|
665C963E272C26E600BC04FB /* CircularFrameBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBackground.swift; sourceTree = "<group>"; };
|
||||||
66779071273AAC26003A1540 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
|
|
||||||
669FDAE8272C23B3007B9422 /* CircularFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrame.swift; sourceTree = "<group>"; };
|
669FDAE8272C23B3007B9422 /* CircularFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrame.swift; sourceTree = "<group>"; };
|
||||||
669FDAEA272C23C2007B9422 /* CircularFrameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBadge.swift; sourceTree = "<group>"; };
|
669FDAEA272C23C2007B9422 /* CircularFrameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBadge.swift; sourceTree = "<group>"; };
|
||||||
66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; };
|
66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; };
|
||||||
|
@ -283,8 +298,11 @@
|
||||||
0D3D04052728B2D70032ABC1 /* BackupFlow */ = {
|
0D3D04052728B2D70032ABC1 /* BackupFlow */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0D6D628A276A528D002FB4CC /* DropDelegate.swift */,
|
||||||
0D3D04062728B2EC0032ABC1 /* Views */,
|
0D3D04062728B2EC0032ABC1 /* Views */,
|
||||||
0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */,
|
0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */,
|
||||||
|
0DFE93E2272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift */,
|
||||||
|
0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */,
|
||||||
);
|
);
|
||||||
path = BackupFlow;
|
path = BackupFlow;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -292,8 +310,11 @@
|
||||||
0D3D04062728B2EC0032ABC1 /* Views */ = {
|
0D3D04062728B2EC0032ABC1 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0DC487C22772574C00BE6A63 /* ValidationSucceededView.swift */,
|
||||||
0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */,
|
0D3D04072728B3440032ABC1 /* RecoveryPhraseDisplayView.swift */,
|
||||||
0D8A43C5272B129C005A6414 /* WordChipGrid.swift */,
|
0D8A43C5272B129C005A6414 /* WordChipGrid.swift */,
|
||||||
|
0DFE93E0272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift */,
|
||||||
|
0DDB6A5027737D4A0012A410 /* ValidationFailedView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -305,7 +326,6 @@
|
||||||
0D4E7A1926B364180058B01E /* secantTests */,
|
0D4E7A1926B364180058B01E /* secantTests */,
|
||||||
0D4E7A2426B364180058B01E /* secantUITests */,
|
0D4E7A2426B364180058B01E /* secantUITests */,
|
||||||
0D4E7A0626B364170058B01E /* Products */,
|
0D4E7A0626B364170058B01E /* Products */,
|
||||||
2EB660DF2747EA6000A06A07 /* Recovered References */,
|
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
@ -353,6 +373,7 @@
|
||||||
0D4E7A1926B364180058B01E /* secantTests */ = {
|
0D4E7A1926B364180058B01E /* secantTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */,
|
||||||
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */,
|
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */,
|
||||||
6654C7422715A48E00901167 /* OnboardingTests */,
|
6654C7422715A48E00901167 /* OnboardingTests */,
|
||||||
0D4E7A1A26B364180058B01E /* secantTests.swift */,
|
0D4E7A1A26B364180058B01E /* secantTests.swift */,
|
||||||
|
@ -384,7 +405,7 @@
|
||||||
children = (
|
children = (
|
||||||
0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */,
|
0D535FE1271F9476009A9E3E /* EnumeratedChip.swift */,
|
||||||
0DF2DC50272344E400FA31E2 /* EmptyChip.swift */,
|
0DF2DC50272344E400FA31E2 /* EmptyChip.swift */,
|
||||||
0D185818272723FF0046B928 /* BlueChip.swift */,
|
0D185818272723FF0046B928 /* ColoredChip.swift */,
|
||||||
0D18581A272728D60046B928 /* PhraseChip.swift */,
|
0D18581A272728D60046B928 /* PhraseChip.swift */,
|
||||||
);
|
);
|
||||||
path = Chips;
|
path = Chips;
|
||||||
|
@ -439,6 +460,7 @@
|
||||||
F9C165B3274031F600592F76 /* Bindings.swift */,
|
F9C165B3274031F600592F76 /* Bindings.swift */,
|
||||||
F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */,
|
F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */,
|
||||||
F93673D52742CB840099C6AF /* Previews.swift */,
|
F93673D52742CB840099C6AF /* Previews.swift */,
|
||||||
|
0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -473,12 +495,20 @@
|
||||||
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */ = {
|
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0DFE93DE272C6D4B000FCCA5 /* RecoveryFlowTests.swift */,
|
|
||||||
0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */,
|
0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */,
|
||||||
|
0DFE93DE272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift */,
|
||||||
);
|
);
|
||||||
path = BackupFlowTests;
|
path = BackupFlowTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */,
|
||||||
|
);
|
||||||
|
path = RecoveryPhraseValidationTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2E5C037F2738C55F008BFFD3 /* Onboarding */ = {
|
2E5C037F2738C55F008BFFD3 /* Onboarding */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -490,14 +520,6 @@
|
||||||
path = Onboarding;
|
path = Onboarding;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
2EB660DF2747EA6000A06A07 /* Recovered References */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
66779071273AAC26003A1540 /* OnboardingScreen.swift */,
|
|
||||||
);
|
|
||||||
name = "Recovered References";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
660558F4270C85F7009D6954 /* Generated */ = {
|
660558F4270C85F7009D6954 /* Generated */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -814,7 +836,7 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1250;
|
LastSwiftUpdateCheck = 1250;
|
||||||
LastUpgradeCheck = 1250;
|
LastUpgradeCheck = 1320;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
0D4E7A0426B364170058B01E = {
|
0D4E7A0426B364170058B01E = {
|
||||||
CreatedOnToolsVersion = 12.5;
|
CreatedOnToolsVersion = 12.5;
|
||||||
|
@ -945,16 +967,21 @@
|
||||||
files = (
|
files = (
|
||||||
2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */,
|
2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */,
|
||||||
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */,
|
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */,
|
||||||
|
0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */,
|
||||||
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
|
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
|
||||||
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
|
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
|
||||||
F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */,
|
F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */,
|
||||||
663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */,
|
663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */,
|
||||||
|
0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */,
|
||||||
0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */,
|
0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */,
|
||||||
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */,
|
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */,
|
||||||
0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */,
|
0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */,
|
||||||
|
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift in Sources */,
|
||||||
0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */,
|
0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */,
|
||||||
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,
|
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,
|
||||||
6654C73E2715A41300901167 /* OnboardingStore.swift in Sources */,
|
6654C73E2715A41300901167 /* OnboardingStore.swift in Sources */,
|
||||||
|
0DDB6A5127737D4A0012A410 /* ValidationFailedView.swift in Sources */,
|
||||||
|
0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */,
|
||||||
F9971A5327680DD000A2DB75 /* Profile.swift in Sources */,
|
F9971A5327680DD000A2DB75 /* Profile.swift in Sources */,
|
||||||
F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */,
|
F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */,
|
||||||
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
|
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
|
||||||
|
@ -968,6 +995,7 @@
|
||||||
F9971A4D27680DC400A2DB75 /* App.swift in Sources */,
|
F9971A4D27680DC400A2DB75 /* App.swift in Sources */,
|
||||||
F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */,
|
F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */,
|
||||||
F93874F1273C4DE200F0E875 /* HomeView.swift in Sources */,
|
F93874F1273C4DE200F0E875 /* HomeView.swift in Sources */,
|
||||||
|
0D7CE63427349B5D0020E050 /* View+WhenDraggable.swift in Sources */,
|
||||||
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */,
|
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */,
|
||||||
F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */,
|
F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */,
|
||||||
F9971A4E27680DC400A2DB75 /* AppView.swift in Sources */,
|
F9971A4E27680DC400A2DB75 /* AppView.swift in Sources */,
|
||||||
|
@ -991,7 +1019,7 @@
|
||||||
F9C165C02740403600592F76 /* ApproveView.swift in Sources */,
|
F9C165C02740403600592F76 /* ApproveView.swift in Sources */,
|
||||||
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
|
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
|
||||||
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
|
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
|
||||||
0D185819272723FF0046B928 /* BlueChip.swift in Sources */,
|
0D185819272723FF0046B928 /* ColoredChip.swift in Sources */,
|
||||||
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
|
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
|
||||||
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */,
|
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */,
|
||||||
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */,
|
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */,
|
||||||
|
@ -1005,6 +1033,7 @@
|
||||||
0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */,
|
0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */,
|
||||||
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
|
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
|
||||||
F9971A6027680DF600A2DB75 /* Scan.swift in Sources */,
|
F9971A6027680DF600A2DB75 /* Scan.swift in Sources */,
|
||||||
|
0DFE93E1272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift in Sources */,
|
||||||
F9C165CB2741AB5D00592F76 /* SendView.swift in Sources */,
|
F9C165CB2741AB5D00592F76 /* SendView.swift in Sources */,
|
||||||
F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */,
|
F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */,
|
||||||
6654C7412715A47300901167 /* Onboarding.swift in Sources */,
|
6654C7412715A47300901167 /* Onboarding.swift in Sources */,
|
||||||
|
@ -1017,10 +1046,11 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
0DFE93DF272C6D4B000FCCA5 /* RecoveryFlowTests.swift in Sources */,
|
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
|
||||||
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
|
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
|
||||||
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
|
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
|
||||||
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
|
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
|
||||||
|
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1170,7 +1200,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "\\";
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = RLPRR8CPQG;
|
DEVELOPMENT_TEAM = RLPRR8CPQG;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
@ -1194,7 +1224,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "\\";
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = RLPRR8CPQG;
|
DEVELOPMENT_TEAM = RLPRR8CPQG;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1250"
|
LastUpgradeVersion = "1320"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -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" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0xE3",
|
"blue" : "0xE3",
|
||||||
"green" : "0xD4",
|
"green" : "0xE7",
|
||||||
"red" : "0xC3"
|
"red" : "0xF9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "1.000",
|
"blue" : "0.890",
|
||||||
"green" : "1.000",
|
"green" : "0.906",
|
||||||
"red" : "1.000"
|
"red" : "0.976"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
|
@ -23,9 +23,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0xE6",
|
"blue" : "0x00",
|
||||||
"green" : "0xE5",
|
"green" : "0x00",
|
||||||
"red" : "0xE0"
|
"red" : "0x00"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
|
||||||
struct AppState: Equatable {
|
struct AppState: Equatable {
|
||||||
enum Route {
|
enum Route: Equatable {
|
||||||
case startup
|
case startup
|
||||||
case onboarding
|
case onboarding
|
||||||
case home
|
case home
|
||||||
|
case phraseValidation
|
||||||
|
case phraseDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
var homeState: HomeState
|
var homeState: HomeState
|
||||||
var onboardingState: OnboardingState
|
var onboardingState: OnboardingState
|
||||||
|
var phraseValidationState: RecoveryPhraseValidationState
|
||||||
|
var phraseDisplayState: RecoveryPhraseDisplayState
|
||||||
var route: Route = .startup
|
var route: Route = .startup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,10 +20,11 @@ enum AppAction: Equatable {
|
||||||
case updateRoute(AppState.Route)
|
case updateRoute(AppState.Route)
|
||||||
case home(HomeAction)
|
case home(HomeAction)
|
||||||
case onboarding(OnboardingAction)
|
case onboarding(OnboardingAction)
|
||||||
|
case phraseDisplay(RecoveryPhraseDisplayAction)
|
||||||
|
case phraseValidation(RecoveryPhraseValidationAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppEnvironment: Equatable {
|
struct AppEnvironment: Equatable {}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - AppReducer
|
// MARK: - AppReducer
|
||||||
|
|
||||||
|
@ -29,7 +35,9 @@ extension AppReducer {
|
||||||
[
|
[
|
||||||
routeReducer,
|
routeReducer,
|
||||||
homeReducer,
|
homeReducer,
|
||||||
onboardingReducer
|
onboardingReducer,
|
||||||
|
phraseValidationReducer.debug(),
|
||||||
|
phraseDisplayReducer.debug()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,13 +45,25 @@ extension AppReducer {
|
||||||
switch action {
|
switch action {
|
||||||
case let .updateRoute(route):
|
case let .updateRoute(route):
|
||||||
state.route = route
|
state.route = route
|
||||||
|
|
||||||
case .home(.reset):
|
case .home(.reset):
|
||||||
state.route = .startup
|
state.route = .startup
|
||||||
case .onboarding(.createNewWallet):
|
|
||||||
|
case .onboarding(.createNewWallet),
|
||||||
|
.phraseValidation(.proceedToHome):
|
||||||
state.route = .home
|
state.route = .home
|
||||||
|
|
||||||
|
case .phraseValidation(.displayBackedUpPhrase),
|
||||||
|
.phraseDisplay(.createPhrase):
|
||||||
|
state.route = .phraseDisplay
|
||||||
|
|
||||||
|
case .phraseDisplay(.finishedPressed):
|
||||||
|
state.route = .phraseValidation
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +78,18 @@ extension AppReducer {
|
||||||
action: /AppAction.onboarding,
|
action: /AppAction.onboarding,
|
||||||
environment: { _ in }
|
environment: { _ in }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private static let phraseValidationReducer: AppReducer = RecoveryPhraseValidationReducer.default.pullback(
|
||||||
|
state: \AppState.phraseValidationState,
|
||||||
|
action: /AppAction.phraseValidation,
|
||||||
|
environment: { _ in BackupPhraseEnvironment.demo }
|
||||||
|
)
|
||||||
|
|
||||||
|
private static let phraseDisplayReducer: AppReducer = RecoveryPhraseDisplayReducer.default.pullback(
|
||||||
|
state: \AppState.phraseDisplayState,
|
||||||
|
action: /AppAction.phraseDisplay,
|
||||||
|
environment: { _ in BackupPhraseEnvironment.demo }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AppStore
|
// MARK: - AppStore
|
||||||
|
@ -80,7 +112,11 @@ extension AppState {
|
||||||
static var placeholder: Self {
|
static var placeholder: Self {
|
||||||
.init(
|
.init(
|
||||||
homeState: .placeholder,
|
homeState: .placeholder,
|
||||||
onboardingState: .init()
|
onboardingState: .init(),
|
||||||
|
phraseValidationState: RecoveryPhraseValidationState.placeholder,
|
||||||
|
phraseDisplayState: RecoveryPhraseDisplayState(
|
||||||
|
phrase: .placeholder
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ struct AppView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
|
|
||||||
case .onboarding:
|
case .onboarding:
|
||||||
OnboardingScreen(
|
OnboardingScreen(
|
||||||
store: store.scope(
|
store: store.scope(
|
||||||
|
@ -25,10 +26,45 @@ struct AppView: View {
|
||||||
action: AppAction.onboarding
|
action: AppAction.onboarding
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
case .startup:
|
case .startup:
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
StartupView(sendAction: viewStore.send)
|
StartupView(sendAction: viewStore.send)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .phraseValidation:
|
||||||
|
NavigationView {
|
||||||
|
RecoveryPhraseBackupValidationView(
|
||||||
|
store: store.scope(
|
||||||
|
state: \.phraseValidationState,
|
||||||
|
action: AppAction.phraseValidation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toolbar(
|
||||||
|
content: {
|
||||||
|
ToolbarItem(
|
||||||
|
placement: .navigationBarLeading,
|
||||||
|
content: {
|
||||||
|
Button(
|
||||||
|
action: { viewStore.send(.updateRoute(.startup)) },
|
||||||
|
label: { Text("Back") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
case .phraseDisplay:
|
||||||
|
NavigationView {
|
||||||
|
RecoveryPhraseDisplayView(
|
||||||
|
store: store.scope(
|
||||||
|
state: \.phraseDisplayState,
|
||||||
|
action: AppAction.phraseDisplay
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,9 +79,18 @@ private struct StartupView: View {
|
||||||
Button("Go To Home") {
|
Button("Go To Home") {
|
||||||
sendAction(.updateRoute(.home))
|
sendAction(.updateRoute(.home))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Go To Onboarding") {
|
Button("Go To Onboarding") {
|
||||||
sendAction(.updateRoute(.onboarding))
|
sendAction(.updateRoute(.onboarding))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button("Go To Phrase Validation Demo") {
|
||||||
|
sendAction(.updateRoute(.phraseValidation))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Go To Phrase Display Demo") {
|
||||||
|
sendAction(.updateRoute(.phraseDisplay))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Startup")
|
.navigationBarTitle("Startup")
|
||||||
|
|
|
@ -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(
|
static let demo = Self(
|
||||||
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
||||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.demo.words)) },
|
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||||
pasteboard: .test
|
pasteboard: .test
|
||||||
)
|
)
|
||||||
|
|
||||||
static let live = Self(
|
static let live = Self(
|
||||||
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
||||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.demo.words)) },
|
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||||
pasteboard: .live
|
pasteboard: .live
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -64,25 +64,33 @@ extension BackupPhraseEnvironment {
|
||||||
typealias RecoveryPhraseDisplayStore = Store<RecoveryPhraseDisplayState, RecoveryPhraseDisplayAction>
|
typealias RecoveryPhraseDisplayStore = Store<RecoveryPhraseDisplayState, RecoveryPhraseDisplayAction>
|
||||||
|
|
||||||
struct RecoveryPhrase: Equatable {
|
struct RecoveryPhrase: Equatable {
|
||||||
struct Chunk: Hashable {
|
struct Group: Hashable {
|
||||||
var startIndex: Int
|
var startIndex: Int
|
||||||
var words: [String]
|
var words: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
let words: [String]
|
let words: [String]
|
||||||
|
|
||||||
private let chunkSize = 6
|
private let groupSize = 6
|
||||||
|
|
||||||
func toChunks() -> [Chunk] {
|
func toGroups() -> [Group] {
|
||||||
let chunks = words.count / chunkSize
|
let chunks = words.count / groupSize
|
||||||
return zip(0 ..< chunks, words.chunked(into: chunkSize)).map {
|
return zip(0 ..< chunks, words.chunked(into: groupSize)).map {
|
||||||
Chunk(startIndex: $0 * chunkSize + 1, words: $1)
|
Group(startIndex: $0 * groupSize + 1, words: $1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toString() -> String {
|
func toString() -> String {
|
||||||
words.joined(separator: " ")
|
words.joined(separator: " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func words(fromMissingIndices indices: [Int]) -> [PhraseChip.Kind] {
|
||||||
|
assert((indices.count - 1) * groupSize <= self.words.count)
|
||||||
|
|
||||||
|
return indices.enumerated().map { index, position in
|
||||||
|
.unassigned(word: self.words[(index * groupSize) + position])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RecoveryPhraseDisplayState: Equatable {
|
struct RecoveryPhraseDisplayState: Equatable {
|
||||||
|
|
|
@ -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 {
|
var body: some View {
|
||||||
WithViewStore(self.store) { viewStore in
|
WithViewStore(self.store) { viewStore in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack {
|
VStack(alignment: .center, spacing: 0) {
|
||||||
if let chunks = viewStore.phrase?.toChunks() {
|
if let groups = viewStore.phrase?.toGroups() {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
Text("Your Secret Recovery Phrase")
|
Text("Your Secret Recovery Phrase")
|
||||||
.titleText()
|
.titleText()
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
|
VStack(alignment: .center, spacing: 4) {
|
||||||
Text("The following 24 words represent your funds and the security used to protect them.")
|
Text("The following 24 words represent your funds and the security used to protect them.")
|
||||||
.bodyText()
|
.bodyText()
|
||||||
|
|
||||||
|
@ -28,12 +29,17 @@ struct RecoveryPhraseDisplayView: View {
|
||||||
.bodyText()
|
.bodyText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.top, 0)
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 35) {
|
||||||
ForEach(chunks, id: \.startIndex) { chunk in
|
ForEach(groups, id: \.startIndex) { group in
|
||||||
WordChipGrid(words: chunk.words, startingAt: chunk.startIndex)
|
VStack {
|
||||||
|
WordChipGrid(words: group.words, startingAt: group.startIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 5)
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Button(
|
Button(
|
||||||
|
@ -59,21 +65,21 @@ struct RecoveryPhraseDisplayView: View {
|
||||||
Text("Oops no words")
|
Text("Oops no words")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
|
.padding(.bottom, 20)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 0)
|
||||||
|
.applyScreenBackground()
|
||||||
}
|
}
|
||||||
|
.navigationBarTitleDisplayMode(.inline) // TODO: NavigationBar Style
|
||||||
// TODO: NavigationBar Style
|
.navigationBarHidden(true)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.applyScreenBackground()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: This should have a #DEBUG tag, but if so, it's not possible to compile this on release mode and submit it to testflight
|
// TODO: This should have a #DEBUG tag, but if so, it's not possible to compile this on release mode and submit it to testflight
|
||||||
extension RecoveryPhraseDisplayStore {
|
extension RecoveryPhraseDisplayStore {
|
||||||
static var demo: RecoveryPhraseDisplayStore {
|
static var demo: RecoveryPhraseDisplayStore {
|
||||||
RecoveryPhraseDisplayStore(
|
RecoveryPhraseDisplayStore(
|
||||||
initialState: .init(phrase: .demo),
|
initialState: .init(phrase: .placeholder),
|
||||||
reducer: .default,
|
reducer: .default,
|
||||||
environment: .demo
|
environment: .demo
|
||||||
)
|
)
|
||||||
|
@ -97,19 +103,19 @@ extension RecoveryPhrase {
|
||||||
"pizza", "just", "garlic"
|
"pizza", "just", "garlic"
|
||||||
]
|
]
|
||||||
|
|
||||||
static let demo = RecoveryPhrase(words: testPhrase)
|
static let placeholder = RecoveryPhrase(words: testPhrase)
|
||||||
static let empty = RecoveryPhrase(words: [])
|
static let empty = RecoveryPhrase(words: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RecoveryPhraseDisplayView_Previews: PreviewProvider {
|
struct RecoveryPhraseDisplayView_Previews: PreviewProvider {
|
||||||
static let scheduler = DispatchQueue.main
|
static let scheduler = DispatchQueue.main
|
||||||
|
|
||||||
static let store = RecoveryPhraseDisplayStore.demo
|
static let store = RecoveryPhraseDisplayStore.demo
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
RecoveryPhraseDisplayView(store: store)
|
RecoveryPhraseDisplayView(store: store)
|
||||||
}
|
}
|
||||||
|
.environment(\.sizeCategory, .accessibilityLarge)
|
||||||
|
|
||||||
NavigationView {
|
NavigationView {
|
||||||
RecoveryPhraseDisplayView(store: store)
|
RecoveryPhraseDisplayView(store: store)
|
||||||
|
|
|
@ -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
|
import SwiftUI
|
||||||
|
|
||||||
/**
|
/// A 3x(N/3) grid of numbered or empty chips.
|
||||||
A 3x2 grid of numbered or empty chips.
|
|
||||||
*/
|
|
||||||
struct WordChipGrid: View {
|
struct WordChipGrid: View {
|
||||||
static let spacing: CGFloat = 10
|
static let spacing: CGFloat = 10
|
||||||
var chips: [PhraseChip.Kind]
|
var chips: [PhraseChip.Kind]
|
||||||
|
var coloredChipColor: Color
|
||||||
var threeColumnGrid = Array(
|
var threeColumnGrid = Array(
|
||||||
repeating: GridItem(
|
repeating: GridItem(
|
||||||
.flexible(minimum: 60, maximum: 120),
|
.flexible(minimum: 100, maximum: 120),
|
||||||
spacing: Self.spacing,
|
spacing: Self.spacing,
|
||||||
alignment: .topLeading
|
alignment: .topLeading
|
||||||
),
|
),
|
||||||
|
@ -24,52 +22,37 @@ struct WordChipGrid: View {
|
||||||
)
|
)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVGrid(
|
LazyVGrid(columns: threeColumnGrid, alignment: .center, spacing: 10) {
|
||||||
columns: threeColumnGrid,
|
ForEach(chips, id: \.self) { item in
|
||||||
alignment: .leading,
|
PhraseChip(kind: item)
|
||||||
spacing: Self.spacing
|
|
||||||
) {
|
|
||||||
ForEach(chips, id: \.self) { wordChip in
|
|
||||||
chipView(for: wordChip)
|
|
||||||
.frame(
|
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: .infinity,
|
|
||||||
minHeight: 30
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(words: [String], startingAt index: Int) {
|
init(chips: [PhraseChip.Kind], coloredChipColor: Color) {
|
||||||
self.chips = zip(words, index..<index + words.count).map({ word, index in
|
self.chips = chips
|
||||||
word.isEmpty ? .empty : .ordered(position: index, word: word)
|
self.coloredChipColor = coloredChipColor
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func chipView(for chipKind: PhraseChip.Kind) -> some View {
|
init(words: [String], startingAt index: Int, coloredChipColor: Color = .clear) {
|
||||||
switch chipKind {
|
let chips = zip(words, index..<index + words.count).map { word, index in
|
||||||
case .empty:
|
word.isEmpty ? PhraseChip.Kind.empty : .ordered(position: index, word: word)
|
||||||
EmptyChip()
|
|
||||||
|
|
||||||
case let .ordered(position, word):
|
|
||||||
EnumeratedChip(index: position, text: word)
|
|
||||||
|
|
||||||
case .unassigned(let word):
|
|
||||||
BlueChip(word: word)
|
|
||||||
}
|
}
|
||||||
|
self.init(chips: chips, coloredChipColor: coloredChipColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WordChipGrid_Previews: PreviewProvider {
|
struct WordChipGrid_Previews: PreviewProvider {
|
||||||
private static var words = [
|
private static var words = [
|
||||||
"pyramid", "negative", "page",
|
"pyramid", "negative", "page",
|
||||||
"crown", "", "zebra"
|
"morning", "", "zebra"
|
||||||
]
|
]
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
WordChipGrid(words: words, startingAt: 1)
|
||||||
WordChipGrid(words: words, startingAt: 1)
|
.frame(maxHeight: .infinity)
|
||||||
}
|
.fixedSize()
|
||||||
.padding()
|
.environment(\.sizeCategory, .accessibilityLarge)
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,8 @@ extension HomeReducer {
|
||||||
action: /HomeAction.profile,
|
action: /HomeAction.profile,
|
||||||
environment: { _ in
|
environment: { _ in
|
||||||
return ProfileEnvironment()
|
return ProfileEnvironment()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.run(&state, action, ())
|
.run(&state, action, ())
|
||||||
case .reset:
|
case .reset:
|
||||||
return .none
|
return .none
|
||||||
|
@ -103,7 +104,6 @@ extension HomeViewStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: PlaceHolders
|
// MARK: PlaceHolders
|
||||||
|
|
||||||
extension HomeState {
|
extension HomeState {
|
||||||
static var placeholder: Self {
|
static var placeholder: Self {
|
||||||
.init(
|
.init(
|
||||||
|
|
|
@ -24,7 +24,8 @@ struct HomeView: View {
|
||||||
initialState: .placeholder,
|
initialState: .placeholder,
|
||||||
reducer: SendReducer.default(
|
reducer: SendReducer.default(
|
||||||
whenDone: { HomeViewStore(store).send(.updateRoute(nil)) }
|
whenDone: { HomeViewStore(store).send(.updateRoute(nil)) }
|
||||||
).debug(),
|
)
|
||||||
|
.debug(),
|
||||||
environment: ()
|
environment: ()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -45,13 +46,13 @@ struct HomeView: View {
|
||||||
List {
|
List {
|
||||||
Section(header: Text("Navigation Stack Routes")) {
|
Section(header: Text("Navigation Stack Routes")) {
|
||||||
ForEach(navigationRouteValues) { routeValue in
|
ForEach(navigationRouteValues) { routeValue in
|
||||||
Text("\(String(describing: routeValue.route))")
|
Text("\(String(describing: routeValue.route))")
|
||||||
.navigationLink(
|
.navigationLink(
|
||||||
isActive: viewStore.bindingForRoute(routeValue.route),
|
isActive: viewStore.bindingForRoute(routeValue.route),
|
||||||
destination: {
|
destination: {
|
||||||
view(for: routeValue.route)
|
view(for: routeValue.route)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ internal enum Asset {
|
||||||
internal enum BackgroundColors {
|
internal enum BackgroundColors {
|
||||||
internal static let numberedChip = ColorAsset(name: "numberedChip")
|
internal static let numberedChip = ColorAsset(name: "numberedChip")
|
||||||
internal static let phraseGridDarkGray = ColorAsset(name: "phraseGridDarkGray")
|
internal static let phraseGridDarkGray = ColorAsset(name: "phraseGridDarkGray")
|
||||||
|
internal static let red = ColorAsset(name: "red")
|
||||||
}
|
}
|
||||||
internal enum Buttons {
|
internal enum Buttons {
|
||||||
internal static let activeButton = ColorAsset(name: "ActiveButton")
|
internal static let activeButton = ColorAsset(name: "ActiveButton")
|
||||||
|
@ -67,6 +68,8 @@ internal enum Asset {
|
||||||
internal enum ScreenBackground {
|
internal enum ScreenBackground {
|
||||||
internal static let gradientEnd = ColorAsset(name: "gradientEnd")
|
internal static let gradientEnd = ColorAsset(name: "gradientEnd")
|
||||||
internal static let gradientStart = ColorAsset(name: "gradientStart")
|
internal static let gradientStart = ColorAsset(name: "gradientStart")
|
||||||
|
internal static let redGradientEnd = ColorAsset(name: "redGradientEnd")
|
||||||
|
internal static let redGradientStart = ColorAsset(name: "redGradientStart")
|
||||||
}
|
}
|
||||||
internal enum Shadow {
|
internal enum Shadow {
|
||||||
internal static let emptyChipInnerShadow = ColorAsset(name: "emptyChipInnerShadow")
|
internal static let emptyChipInnerShadow = ColorAsset(name: "emptyChipInnerShadow")
|
||||||
|
|
|
@ -15,9 +15,6 @@ struct OnboardingScreen: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
ZStack {
|
ZStack {
|
||||||
ScreenBackground()
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
|
|
||||||
OnboardingHeaderView(
|
OnboardingHeaderView(
|
||||||
store: store.scope(
|
store: store.scope(
|
||||||
state: { state in
|
state: { state in
|
||||||
|
@ -45,6 +42,7 @@ struct OnboardingScreen: View {
|
||||||
OnboardingFooterView(store: store)
|
OnboardingFooterView(store: store)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.applyScreenBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,12 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
/**
|
|
||||||
A Vertical LinearGradient that takes an array of Colors and renders them vertically in a centered fashion mostly used as a background for Screen views..
|
/// A Vertical LinearGradient that takes an array of Colors and renders them vertically
|
||||||
*/
|
/// in a centered fashion mostly used as a background for Screen views..
|
||||||
struct ScreenBackground: View {
|
struct ScreenBackground: View {
|
||||||
var colors = [
|
var colors: [Color]
|
||||||
Asset.Colors.ScreenBackground.gradientStart.color,
|
|
||||||
Asset.Colors.ScreenBackground.gradientEnd.color
|
|
||||||
]
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
colors: colors,
|
colors: colors,
|
||||||
|
@ -24,22 +22,40 @@ struct ScreenBackground: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ScreenBackgroundModifier: ViewModifier {
|
struct ScreenBackgroundModifier: ViewModifier {
|
||||||
|
var colors: [Color]
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
ScreenBackground()
|
ScreenBackground(colors: colors)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
/**
|
/// Adds a Vertical Linear Gradient with the default Colors of VLinearGradient.
|
||||||
Adds a Vertical Linear Gradient with the default Colors of VLinearGradient. Supports both Light and Dark Mode
|
/// Supports both Light and Dark Mode
|
||||||
*/
|
|
||||||
func applyScreenBackground() -> some View {
|
func applyScreenBackground() -> some View {
|
||||||
self.modifier(
|
self.modifier(
|
||||||
ScreenBackgroundModifier()
|
ScreenBackgroundModifier(
|
||||||
|
colors: [
|
||||||
|
Asset.Colors.ScreenBackground.gradientStart.color,
|
||||||
|
Asset.Colors.ScreenBackground.gradientEnd.color
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyErredScreenBackground() -> some View {
|
||||||
|
self.modifier(
|
||||||
|
ScreenBackgroundModifier(
|
||||||
|
colors: [
|
||||||
|
Asset.Colors.ScreenBackground.redGradientStart.color,
|
||||||
|
Asset.Colors.ScreenBackground.redGradientEnd.color
|
||||||
|
]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,28 +7,31 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct BlueChip: View {
|
struct ColoredChip: View {
|
||||||
var word: String
|
var word: String
|
||||||
|
var color = Asset.Colors.Buttons.activeButton.color
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(word)
|
Text(word)
|
||||||
.font(FontFamily.Rubik.regular.textStyle(.body))
|
.font(.custom(FontFamily.Rubik.regular.name, size: 15))
|
||||||
.frame(
|
.frame(
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
maxWidth: 120,
|
maxWidth: .infinity,
|
||||||
minHeight: 30,
|
minHeight: 30,
|
||||||
idealHeight: 40
|
maxHeight: .infinity
|
||||||
)
|
)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.foregroundColor(Asset.Colors.Text.activeButtonText.color)
|
.foregroundColor(Asset.Colors.Text.activeButtonText.color)
|
||||||
.padding(.horizontal, 4)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.background(Asset.Colors.Buttons.activeButton.color)
|
.background(color)
|
||||||
.cornerRadius(6)
|
.cornerRadius(6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BlueChip_Previews: PreviewProvider {
|
struct ColoredChip_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
BlueChip(word: "negative")
|
ColoredChip(word: "negative")
|
||||||
|
.frame(width: 115)
|
||||||
.applyScreenBackground()
|
.applyScreenBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,6 +8,8 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct EmptyChip: View {
|
struct EmptyChip: View {
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
RoundedRectangle(cornerRadius: 6, style: RoundedCornerStyle.continuous)
|
RoundedRectangle(cornerRadius: 6, style: RoundedCornerStyle.continuous)
|
||||||
.stroke(Asset.Colors.Text.activeButtonText.color, lineWidth: 0.5)
|
.stroke(Asset.Colors.Text.activeButtonText.color, lineWidth: 0.5)
|
||||||
|
@ -24,15 +26,24 @@ struct EmptyChip: View {
|
||||||
width: 4,
|
width: 4,
|
||||||
blur: 2
|
blur: 2
|
||||||
)
|
)
|
||||||
|
.background(chipBackground)
|
||||||
.frame(
|
.frame(
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
maxWidth: .infinity,
|
maxWidth: .infinity,
|
||||||
minHeight: 40,
|
minHeight: 40,
|
||||||
idealHeight: 40,
|
|
||||||
maxHeight: .infinity,
|
maxHeight: .infinity,
|
||||||
alignment: .leading
|
alignment: .leading
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var chipBackground: some View {
|
||||||
|
if colorScheme == .dark {
|
||||||
|
RoundedRectangle(cornerRadius: 6, style: RoundedCornerStyle.continuous)
|
||||||
|
.fill(Asset.Colors.ScreenBackground.gradientEnd.color)
|
||||||
|
} else {
|
||||||
|
Color.clear
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EmptyChip_Previews: PreviewProvider {
|
struct EmptyChip_Previews: PreviewProvider {
|
||||||
|
@ -49,7 +60,7 @@ struct EmptyChip_Previews: PreviewProvider {
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
ZStack {
|
ZStack {
|
||||||
ScreenBackground()
|
Color.gray
|
||||||
EmptyChip()
|
EmptyChip()
|
||||||
.frame(width: 100, height: 40, alignment: .leading)
|
.frame(width: 100, height: 40, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,102 +8,57 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct EnumeratedChip: View {
|
struct EnumeratedChip: View {
|
||||||
|
let basePadding: CGFloat = 14
|
||||||
|
|
||||||
@Clamped(1...24)
|
@Clamped(1...24)
|
||||||
var index: Int = 1
|
var index: Int = 1
|
||||||
|
|
||||||
var text: String
|
var text: String
|
||||||
|
var overlayPadding: CGFloat = 20
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NumberedText(number: index, text: text)
|
Text(text)
|
||||||
|
.foregroundColor(Asset.Colors.Text.button.color)
|
||||||
|
.font(.custom(FontFamily.Rubik.regular.name, size: 14))
|
||||||
.frame(
|
.frame(
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: .infinity,
|
maxWidth: .infinity,
|
||||||
minHeight: 30,
|
minHeight: 30,
|
||||||
maxHeight: .infinity,
|
maxHeight: .infinity,
|
||||||
alignment: .leading
|
alignment: .leading
|
||||||
)
|
)
|
||||||
.padding(.leading, 14)
|
.padding(.leading, basePadding + overlayPadding)
|
||||||
.padding(.vertical, 4)
|
.padding([.trailing, .vertical], 4)
|
||||||
.background(Asset.Colors.BackgroundColors.numberedChip.color)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.cornerRadius(6)
|
|
||||||
.shadow(color: Asset.Colors.Shadow.numberedTextShadow.color, radius: 3, x: 0, y: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NumberedText: View {
|
|
||||||
var number: Int = 1
|
|
||||||
var text: String
|
|
||||||
|
|
||||||
@ViewBuilder var numberedText: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
(Text("\(number)")
|
|
||||||
.baselineOffset(geometry.size.height / 4)
|
|
||||||
.foregroundColor(Asset.Colors.Text.highlightedSuperscriptText.color)
|
|
||||||
.font(.custom(FontFamily.Roboto.bold.name, size: 12)) +
|
|
||||||
|
|
||||||
Text(" \(text)")
|
|
||||||
.foregroundColor(Asset.Colors.Text.button.color)
|
|
||||||
.font(.custom(FontFamily.Rubik.regular.name, size: 14))
|
|
||||||
)
|
|
||||||
.shadow(
|
.shadow(
|
||||||
color: Asset.Colors.Shadow.numberedTextShadow.color,
|
color: Asset.Colors.Shadow.numberedTextShadow.color,
|
||||||
radius: 1,
|
radius: 1,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 1
|
y: 1
|
||||||
)
|
)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.background(Asset.Colors.BackgroundColors.numberedChip.color)
|
||||||
.frame(height: geometry.size.height, alignment: .center)
|
.cornerRadius(6)
|
||||||
}
|
.shadow(color: Asset.Colors.Shadow.numberedTextShadow.color, radius: 3, x: 0, y: 1)
|
||||||
}
|
.overlay(
|
||||||
|
GeometryReader { geometry in
|
||||||
var body: some View {
|
Text("\(index)")
|
||||||
numberedText
|
.foregroundColor(Asset.Colors.Text.highlightedSuperscriptText.color)
|
||||||
.layoutPriority(1)
|
.font(.custom(FontFamily.Roboto.bold.name, size: 10))
|
||||||
|
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
|
||||||
|
.padding(.leading, basePadding)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EnumeratedChip_Previews: PreviewProvider {
|
struct EnumeratedChip_Previews: PreviewProvider {
|
||||||
private static var threeColumnGrid = Array(
|
|
||||||
repeating: GridItem(
|
|
||||||
.flexible(minimum: 60, maximum: 120),
|
|
||||||
spacing: 15,
|
|
||||||
alignment: .topLeading
|
|
||||||
),
|
|
||||||
count: 3
|
|
||||||
)
|
|
||||||
|
|
||||||
private static var words = [
|
private static var words = [
|
||||||
"pyramid", "negative", "page",
|
"pyramid", "negative", "page",
|
||||||
"crown", "", "zebra"
|
"crown", "", "zebra"
|
||||||
]
|
]
|
||||||
|
|
||||||
@ViewBuilder static var grid: some View {
|
@ViewBuilder static var grid: some View {
|
||||||
LazyVGrid(
|
WordChipGrid(words: words, startingAt: 1)
|
||||||
columns: threeColumnGrid,
|
|
||||||
alignment: .leading,
|
|
||||||
spacing: 15
|
|
||||||
) {
|
|
||||||
ForEach(Array(zip(words.indices, words)), id: \.1) { i, word in
|
|
||||||
if word.isEmpty {
|
|
||||||
EmptyChip()
|
|
||||||
.frame(
|
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: .infinity,
|
|
||||||
minHeight: 40,
|
|
||||||
maxHeight: .infinity
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
EnumeratedChip(index: (i + 1), text: word)
|
|
||||||
.frame(
|
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: .infinity,
|
|
||||||
minHeight: 30,
|
|
||||||
maxHeight: .infinity
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|
|
@ -17,26 +17,13 @@ struct PhraseChip: View {
|
||||||
var kind: Kind
|
var kind: Kind
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
chipFor(for: kind)
|
|
||||||
.frame(
|
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: 120,
|
|
||||||
minHeight: 30,
|
|
||||||
idealHeight: 40
|
|
||||||
)
|
|
||||||
.animation(.easeIn)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder func chipFor(for kind: Kind) -> some View {
|
|
||||||
switch kind {
|
switch kind {
|
||||||
case .empty:
|
case .empty:
|
||||||
EmptyChip()
|
EmptyChip()
|
||||||
|
|
||||||
case let .ordered(position, word):
|
case let .ordered(position, word):
|
||||||
EnumeratedChip(index: position, text: word)
|
EnumeratedChip(index: position, text: word)
|
||||||
|
|
||||||
case .unassigned(let word):
|
case .unassigned(let word):
|
||||||
BlueChip(word: word)
|
ColoredChip(word: word)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,13 +32,13 @@ struct PhraseChip_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VStack {
|
||||||
PhraseChip(kind: .unassigned(word: "negative"))
|
PhraseChip(kind: .unassigned(word: "negative"))
|
||||||
.frame(height: 40)
|
.frame(width: 120, height: 40)
|
||||||
|
|
||||||
PhraseChip(kind: .empty)
|
PhraseChip(kind: .empty)
|
||||||
.frame(height: 40)
|
.frame(width: 120, height: 40)
|
||||||
|
|
||||||
PhraseChip(kind: .ordered(position: 23, word: "mutual"))
|
PhraseChip(kind: .ordered(position: 23, word: "mutual"))
|
||||||
.frame(height: 40)
|
.frame(width: 120, height: 40)
|
||||||
}
|
}
|
||||||
.applyScreenBackground()
|
.applyScreenBackground()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,39 @@ enum Badge: Equatable {
|
||||||
case shield
|
case shield
|
||||||
case list
|
case list
|
||||||
case person
|
case person
|
||||||
|
case error
|
||||||
|
|
||||||
var image: Image {
|
@ViewBuilder var image: some View {
|
||||||
switch self {
|
switch self {
|
||||||
case .shield: return Asset.Assets.Icons.shield.image
|
case .shield:
|
||||||
case .list: return Asset.Assets.Icons.list.image
|
Asset.Assets.Icons.shield.image
|
||||||
case .person: return Asset.Assets.Icons.profile.image
|
.resizable()
|
||||||
|
.renderingMode(.none)
|
||||||
|
case .list:
|
||||||
|
Asset.Assets.Icons.list.image
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.none)
|
||||||
|
case .person:
|
||||||
|
Asset.Assets.Icons.profile.image
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.none)
|
||||||
|
case .error:
|
||||||
|
ErrorBadge()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ErrorBadge: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("X")
|
||||||
|
.font(.custom(FontFamily.Rubik.bold.name, size: 36))
|
||||||
|
.foregroundColor(Asset.Colors.BackgroundColors.red.color)
|
||||||
|
.frame(width: 60, height: 55, alignment: .center)
|
||||||
|
.background(Asset.Colors.BackgroundColors.numberedChip.color)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct BadgesOverlay: Animatable, ViewModifier {
|
struct BadgesOverlay: Animatable, ViewModifier {
|
||||||
struct ViewState: Equatable {
|
struct ViewState: Equatable {
|
||||||
let index: Int
|
let index: Int
|
||||||
|
@ -44,8 +67,6 @@ struct BadgesOverlay: Animatable, ViewModifier {
|
||||||
ZStack {
|
ZStack {
|
||||||
ForEach(0..<viewStore.badges.count) { badgeIndex in
|
ForEach(0..<viewStore.badges.count) { badgeIndex in
|
||||||
viewStore.badges[viewStore.index].image
|
viewStore.badges[viewStore.index].image
|
||||||
.resizable()
|
|
||||||
.renderingMode(.none)
|
|
||||||
.frame(
|
.frame(
|
||||||
width: proxy.size.width * 0.35,
|
width: proxy.size.width * 0.35,
|
||||||
height: proxy.size.height * 0.35,
|
height: proxy.size.height * 0.35,
|
||||||
|
@ -82,7 +103,6 @@ struct BadgeOverlay: Animatable, ViewModifier {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
badge.image
|
badge.image
|
||||||
.resizable()
|
|
||||||
.frame(
|
.frame(
|
||||||
width: proxy.size.width * 0.35,
|
width: proxy.size.width * 0.35,
|
||||||
height: proxy.size.height * 0.35,
|
height: proxy.size.height * 0.35,
|
||||||
|
@ -128,6 +148,10 @@ struct Badge_Previews: PreviewProvider {
|
||||||
CircularFrame()
|
CircularFrame()
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.badgeIcon(.person)
|
.badgeIcon(.person)
|
||||||
|
|
||||||
|
CircularFrame()
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.badgeIcon(.error)
|
||||||
}
|
}
|
||||||
.preferredColorScheme(.light)
|
.preferredColorScheme(.light)
|
||||||
.previewLayout(.fixed(width: size + 50, height: size + 50))
|
.previewLayout(.fixed(width: size + 50, height: size + 50))
|
||||||
|
|
|
@ -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
|
import XCTest
|
||||||
@testable import secant_testnet
|
@testable import secant_testnet
|
||||||
|
|
||||||
class RecoveryFlowTests: XCTestCase {
|
class RecoveryPhraseBackupTests: XCTestCase {
|
||||||
func testGiven24WordsBIP39ChunkItIntoQuarters() throws {
|
func testGiven24WordsBIP39ChunkItIntoQuarters() throws {
|
||||||
let words = [
|
let words = [
|
||||||
"bring", "salute", "thank",
|
"bring", "salute", "thank",
|
||||||
|
@ -25,7 +25,7 @@ class RecoveryFlowTests: XCTestCase {
|
||||||
]
|
]
|
||||||
let phrase = RecoveryPhrase(words: words)
|
let phrase = RecoveryPhrase(words: words)
|
||||||
|
|
||||||
let chunks = phrase.toChunks()
|
let chunks = phrase.toGroups()
|
||||||
|
|
||||||
XCTAssertEqual(chunks.count, 4)
|
XCTAssertEqual(chunks.count, 4)
|
||||||
XCTAssertEqual(chunks[0].startIndex, 1)
|
XCTAssertEqual(chunks[0].startIndex, 1)
|
|
@ -18,13 +18,13 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase {
|
||||||
)
|
)
|
||||||
|
|
||||||
store.send(.copyToBufferPressed) {
|
store.send(.copyToBufferPressed) {
|
||||||
$0.phrase = .demo
|
$0.phrase = .placeholder
|
||||||
$0.showCopyToBufferAlert = true
|
$0.showCopyToBufferAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
store.environment.pasteboard.getString(),
|
store.environment.pasteboard.getString(),
|
||||||
RecoveryPhrase.demo.toString()
|
RecoveryPhrase.placeholder.toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,8 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase {
|
||||||
environment: .demo
|
environment: .demo
|
||||||
)
|
)
|
||||||
|
|
||||||
store.send(.phraseResponse(.success(.demo))) {
|
store.send(.phraseResponse(.success(.placeholder))) {
|
||||||
$0.phrase = .demo
|
$0.phrase = .placeholder
|
||||||
$0.showCopyToBufferAlert = false
|
$0.showCopyToBufferAlert = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase {
|
||||||
|
|
||||||
private extension RecoveryPhraseDisplayState {
|
private extension RecoveryPhraseDisplayState {
|
||||||
static let test = RecoveryPhraseDisplayState(
|
static let test = RecoveryPhraseDisplayState(
|
||||||
phrase: .demo,
|
phrase: .placeholder,
|
||||||
showCopyToBufferAlert: false
|
showCopyToBufferAlert: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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