From c8e54550fc78f1af340e3fbb216c8e2d58a114dd Mon Sep 17 00:00:00 2001 From: Lukas Korba <62649321+lukaskorba-indiedev@users.noreply.github.com> Date: Wed, 23 Feb 2022 17:49:17 +0100 Subject: [PATCH] Requested changes done The failed color (red) updated. The haptic feedback (.error) now occurs when the chips are not at the right indices. --- secant.xcodeproj/project.pbxproj | 4 +++ .../BackupFlow/FeedbackGenerator.swift | 26 +++++++++++++++++++ .../RecoveryPhraseDisplayStore.swift | 7 +++-- .../BackupFlow/RecoveryPhraseValidation.swift | 16 +++++++++--- .../BackupFlow/View+WhenDraggable.swift | 2 +- .../RecoveryPhraseBackupValidationView.swift | 10 ++++--- secant/UIComponents/Chips/PhraseChip.swift | 6 ++--- 7 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 secant/Features/BackupFlow/FeedbackGenerator.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index ae19652..fe9a880 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */; }; 66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */; }; 66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */; }; + 9E4DC71027C6981300E657F4 /* FeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC70F27C6981300E657F4 /* FeedbackGenerator.swift */; }; F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9322DBF273B555C00C105B5 /* NavigationLinks.swift */; }; F93673D62742CB840099C6AF /* Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93673D52742CB840099C6AF /* Previews.swift */; }; F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874ED273C4DE200F0E875 /* HomeStore.swift */; }; @@ -209,6 +210,7 @@ 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = ""; }; 66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtonStyle.swift; sourceTree = ""; }; 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardButtonStyle.swift; sourceTree = ""; }; + 9E4DC70F27C6981300E657F4 /* FeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackGenerator.swift; sourceTree = ""; }; F9322DBF273B555C00C105B5 /* NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationLinks.swift; sourceTree = ""; }; F93673D52742CB840099C6AF /* Previews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Previews.swift; sourceTree = ""; }; F93874ED273C4DE200F0E875 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; }; @@ -325,6 +327,7 @@ 0D3D04092728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift */, 0DFE93E2272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift */, 0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */, + 9E4DC70F27C6981300E657F4 /* FeedbackGenerator.swift */, ); path = BackupFlow; sourceTree = ""; @@ -1062,6 +1065,7 @@ 0DFE93E1272C9ECB000FCCA5 /* RecoveryPhraseBackupValidationView.swift in Sources */, F9C165CB2741AB5D00592F76 /* SendView.swift in Sources */, 0D0781C4278750E30083ACD7 /* WelcomeView.swift in Sources */, + 9E4DC71027C6981300E657F4 /* FeedbackGenerator.swift in Sources */, F9971A6527680DFE00A2DB75 /* Settings.swift in Sources */, 0D0781C9278776D20083ACD7 /* ZcashSymbol.swift in Sources */, 6654C7412715A47300901167 /* Onboarding.swift in Sources */, diff --git a/secant/Features/BackupFlow/FeedbackGenerator.swift b/secant/Features/BackupFlow/FeedbackGenerator.swift new file mode 100644 index 0000000..fd88ce1 --- /dev/null +++ b/secant/Features/BackupFlow/FeedbackGenerator.swift @@ -0,0 +1,26 @@ +// +// FeedbackGenerator.swift +// secant-testnet +// +// Created by Lukáš Korba on 02/23/2022. +// + +import UIKit + +protocol FeedbackGenerator { + func generateFeedback() +} + +/// use in case of testing or when real haptic feedback is not appropriate +class SilentFeedbackGenerator: FeedbackGenerator { + func generateFeedback() { } +} + +/// haptic feedback for the failures (when we want to amplify importance of the failure) +class ImpactFeedbackGenerator: FeedbackGenerator { + let generator = UINotificationFeedbackGenerator() + + func generateFeedback() { + generator.notificationOccurred(.error) + } +} diff --git a/secant/Features/BackupFlow/RecoveryPhraseDisplayStore.swift b/secant/Features/BackupFlow/RecoveryPhraseDisplayStore.swift index bdba941..f480d03 100644 --- a/secant/Features/BackupFlow/RecoveryPhraseDisplayStore.swift +++ b/secant/Features/BackupFlow/RecoveryPhraseDisplayStore.swift @@ -40,6 +40,7 @@ struct BackupPhraseEnvironment { let mainQueue: AnySchedulerOf let newPhrase: () -> Effect let pasteboard: Pasteboard + let feedbackGenerator: FeedbackGenerator } extension BackupPhraseEnvironment { @@ -51,13 +52,15 @@ extension BackupPhraseEnvironment { static let demo = Self( mainQueue: DispatchQueue.main.eraseToAnyScheduler(), newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, - pasteboard: .test + pasteboard: .test, + feedbackGenerator: SilentFeedbackGenerator() ) static let live = Self( mainQueue: DispatchQueue.main.eraseToAnyScheduler(), newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, - pasteboard: .live + pasteboard: .live, + feedbackGenerator: ImpactFeedbackGenerator() ) } diff --git a/secant/Features/BackupFlow/RecoveryPhraseValidation.swift b/secant/Features/BackupFlow/RecoveryPhraseValidation.swift index c779565..68eae62 100644 --- a/secant/Features/BackupFlow/RecoveryPhraseValidation.swift +++ b/secant/Features/BackupFlow/RecoveryPhraseValidation.swift @@ -133,6 +133,7 @@ enum RecoveryPhraseValidationAction: Equatable { case move(wordChip: PhraseChip.Kind, intoGroup: Int) case succeed case fail + case failureFeedback case proceedToHome case displayBackedUpPhrase } @@ -147,7 +148,7 @@ extension RecoveryPhraseValidationReducer { case let .move(wordChip, group): guard - case let PhraseChip.Kind.unassigned(word) = wordChip, + case let PhraseChip.Kind.unassigned(word, color) = wordChip, let missingChipIndex = state.missingWordChips.firstIndex(of: wordChip) else { return .none } @@ -156,9 +157,13 @@ extension RecoveryPhraseValidationReducer { if state.isComplete { let value: RecoveryPhraseValidationAction = state.isValid ? .succeed : .fail - return Effect(value: value) - .delay(for: 1, scheduler: environment.mainQueue) - .eraseToEffect() + + return .concatenate( + .init(value: .failureFeedback), + Effect(value: value) + .delay(for: 1, scheduler: environment.mainQueue) + .eraseToEffect() + ) } return .none @@ -168,6 +173,9 @@ extension RecoveryPhraseValidationReducer { case .fail: state.route = .failure + case .failureFeedback: + environment.feedbackGenerator.generateFeedback() + case .updateRoute(let route): state.route = route diff --git a/secant/Features/BackupFlow/View+WhenDraggable.swift b/secant/Features/BackupFlow/View+WhenDraggable.swift index fa81c19..9fb8350 100644 --- a/secant/Features/BackupFlow/View+WhenDraggable.swift +++ b/secant/Features/BackupFlow/View+WhenDraggable.swift @@ -15,7 +15,7 @@ extension PhraseChip { /// Makes a PhraseChip draggable when it is of kind .unassigned @ViewBuilder func makeDraggable() -> some View { switch self.kind { - case .unassigned(let word): + case let .unassigned(word, _): self.onDrag { NSItemProvider(object: word as NSString) } diff --git a/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift b/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift index ea07ce1..dc34175 100644 --- a/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift +++ b/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift @@ -116,7 +116,8 @@ extension RecoveryPhraseValidationState { func wordsChips( for groupIndex: Int, groupSize: Int, - from wordGroup: RecoveryPhrase.Group + from wordGroup: RecoveryPhrase.Group, + state: RecoveryPhraseValidationState ) -> [PhraseChip.Kind] { let validationWord = validationWords.first(where: { $0.groupIndex == groupIndex }) @@ -126,7 +127,7 @@ extension RecoveryPhraseValidationState { } if let completedWord = validationWord?.word { - return .unassigned(word: completedWord) + return .unassigned(word: completedWord, color: state.coloredChipColor) } return .empty @@ -247,8 +248,9 @@ private extension WordChipGrid { ) { let chips = state.wordsChips( for: groupIndex, - groupSize: RecoveryPhraseValidationState.wordGroupSize, - from: wordGroup + groupSize: RecoveryPhraseValidationState.wordGroupSize, + from: wordGroup, + state: state ) self.init(chips: chips, coloredChipColor: state.coloredChipColor) diff --git a/secant/UIComponents/Chips/PhraseChip.swift b/secant/UIComponents/Chips/PhraseChip.swift index 491ba9a..ff4899e 100644 --- a/secant/UIComponents/Chips/PhraseChip.swift +++ b/secant/UIComponents/Chips/PhraseChip.swift @@ -10,7 +10,7 @@ import SwiftUI struct PhraseChip: View { enum Kind: Hashable { case empty - case unassigned(word: String) + case unassigned(word: String, color: Color = Asset.Colors.Buttons.activeButton.color) case ordered(position: Int, word: String) } @@ -22,8 +22,8 @@ struct PhraseChip: View { EmptyChip() case let .ordered(position, word): EnumeratedChip(index: position, text: word) - case .unassigned(let word): - ColoredChip(word: word) + case let .unassigned(word, color): + ColoredChip(word: word, color: color) } } }