secant-ios-wallet/secant/Features/BackupFlow/RecoveryPhraseValidation.swift

183 lines
6.1 KiB
Swift
Raw Normal View History

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
2021-12-13 12:50:04 -08:00
//
// 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`
2022-02-18 13:39:04 -08:00
/// if a resulting phrase can't be formed because the validation state is not complete.
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
2021-12-13 12:50:04 -08:00
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
}
}