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:
Francisco Gindre 2021-12-13 17:50:04 -03:00
parent 80bbdc8cda
commit b75def9cc1
34 changed files with 1878 additions and 235 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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