[#556] Hide post-seed backup flow and rework screenshot tests (#591)

- flow is hidden by default
- when turned on, only new users (nuke wallet and start over) continues to the flow
- unit tests fixed and updated
- new unit tests
- phrase display screen simplified

[#556] Hide post-seed backup flow and rework screenshot tests (#591)

- never show the phrase for users who had it disabled at the time of wallet creation
This commit is contained in:
Lukas Korba 2023-03-01 19:33:58 +01:00 committed by GitHub
parent 2a560dea8d
commit b61fa213b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 339 additions and 75 deletions

View File

@ -446,6 +446,7 @@
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */; };
9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */; };
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; };
9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */; };
9E92AF0828530EBF007367AD /* View+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E92AF0728530EBF007367AD /* View+UIImage.swift */; };
9E94C62028AA7DEE008256E9 /* BalanceBreakdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */; };
9E94C62328AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */; };
@ -768,6 +769,7 @@
9E7FE0E5282E7B1100C374E8 /* StoredWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWallet.swift; sourceTree = "<group>"; };
9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanUIView.swift; sourceTree = "<group>"; };
9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanView.swift; sourceTree = "<group>"; };
9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidationFlowFeatureFlagTests.swift; sourceTree = "<group>"; };
9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = "<group>"; };
9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownTests.swift; sourceTree = "<group>"; };
9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = "<group>"; };
@ -1091,7 +1093,6 @@
children = (
9EF8135F27F043CC0075AF48 /* AppDelegate.swift */,
0D6D628A276A528D002FB4CC /* DropDelegate.swift */,
34F682E429A75EB60022C079 /* WalletConfig.swift */,
9EF8139B27F47AED0075AF48 /* InitializationState.swift */,
9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */,
9E612C7C2991476F00D09B09 /* SensitiveData.swift */,
@ -1099,6 +1100,7 @@
9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */,
9E5BF63B2818305D00BA3F17 /* TransactionState.swift */,
9E7FE0DC282D298900C374E8 /* ValidationWord.swift */,
34F682E429A75EB60022C079 /* WalletConfig.swift */,
9EAB46772860A1D2002904A0 /* WalletEvent.swift */,
);
path = Models;
@ -1145,6 +1147,7 @@
isa = PBXGroup;
children = (
0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */,
9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */,
);
path = RecoveryPhraseValidationTests;
sourceTree = "<group>";
@ -3052,6 +3055,7 @@
9E9ECC9828589E150099D5A2 /* WelcomeSnapshotTests.swift in Sources */,
9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */,
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
9E9ECC9A28589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests.swift in Sources */,
9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */,

View File

@ -15,5 +15,5 @@ extension DependencyValues {
}
struct RecoveryPhraseRandomizerClient {
let random: (RecoveryPhrase) -> RecoveryPhraseValidationFlowReducer.State
var random: (RecoveryPhrase) -> RecoveryPhraseValidationFlowReducer.State
}

View File

@ -116,10 +116,10 @@ struct WalletStorage {
}
}
func markUserPassedPhraseBackupTest() throws {
func markUserPassedPhraseBackupTest(_ flag: Bool = true) throws {
do {
var wallet = try exportWallet()
wallet.hasUserPassedPhraseBackupTest = true
wallet.hasUserPassedPhraseBackupTest = flag
guard let data = try encode(object: wallet) else {
throw KeychainError.encoding

View File

@ -30,6 +30,7 @@ struct WalletStorageClient {
/// - bip39: Mnemonic/Seed phrase from `MnemonicSwift`
/// - birthday: BlockHeight from SDK
/// - language: Mnemonic's language
/// - hasUserPassedPhraseBackupTest: If user passed the puzzle phrase backup
/// - Throws:
/// - `WalletStorageError.unsupportedLanguage`: when mnemonic's language is anything other than English
/// - `WalletStorageError.alreadyImported` when valid wallet is already in the storage
@ -66,7 +67,7 @@ struct WalletStorageClient {
/// - Throws:
/// - `WalletStorage.KeychainError.encoding`: when encoding the wallet's data failed.
/// - `WalletStorageError.storageError` when some unrecognized error occurred.
let markUserPassedPhraseBackupTest: () throws -> Void
let markUserPassedPhraseBackupTest: (Bool) throws -> Void
/// Use carefully: deletes the stored wallet.
/// There's no fate but what we make for ourselves - Sarah Connor.

View File

@ -32,8 +32,8 @@ extension WalletStorageClient: DependencyKey {
updateBirthday: { birthday in
try walletStorage.updateBirthday(birthday)
},
markUserPassedPhraseBackupTest: {
try walletStorage.markUserPassedPhraseBackupTest()
markUserPassedPhraseBackupTest: { flag in
try walletStorage.markUserPassedPhraseBackupTest(flag)
},
nukeWallet: {
walletStorage.nukeWallet()

View File

@ -25,7 +25,7 @@ extension WalletStorageClient {
exportWallet: { .placeholder },
areKeysPresent: { false },
updateBirthday: { _ in },
markUserPassedPhraseBackupTest: { },
markUserPassedPhraseBackupTest: { _ in },
nukeWallet: { }
)
}

View File

@ -107,7 +107,7 @@ struct ImportWalletReducer: ReducerProtocol {
try walletStorage.importWallet(state.importedSeedPhrase.data, birthday.data, .english, false)
// update the backup phrase validation flag
try walletStorage.markUserPassedPhraseBackupTest()
try walletStorage.markUserPassedPhraseBackupTest(true)
// notify user
// TODO: [#221] Proper Error/Success handling (https://github.com/zcash/secant-ios-wallet/issues/221)

View File

@ -33,7 +33,6 @@ struct RecoveryPhraseDisplayReducer: ReducerProtocol {
return .none
case .finishedPressed:
// TODO: [#47] remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/47
return .none
case let .phraseResponse(phrase):

View File

@ -13,57 +13,57 @@ struct RecoveryPhraseDisplayView: View {
var body: some View {
WithViewStore(self.store) { viewStore in
ScrollView {
VStack(alignment: .center, spacing: 0) {
if let groups = viewStore.phrase?.toGroups() {
VStack(spacing: 20) {
Text("recoveryPhraseDisplay.title")
.titleText()
.multilineTextAlignment(.center)
VStack(alignment: .center, spacing: 4) {
Text("recoveryPhraseDisplay.description")
.bodyText()
Text("recoveryPhraseDisplay.backItUp")
.bodyText()
}
}
.padding(.top, 0)
.padding(.bottom, 20)
VStack(alignment: .center, spacing: 0) {
if let groups = viewStore.phrase?.toGroups(groupSizeOverride: 2) {
VStack(spacing: 20) {
Text("recoveryPhraseDisplay.title")
.titleText()
.multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 35) {
ForEach(groups, id: \.startIndex) { group in
VStack {
WordChipGrid(words: group.words, startingAt: group.startIndex)
}
}
VStack(alignment: .center, spacing: 4) {
Text("recoveryPhraseDisplay.description")
.bodyText()
}
.padding(.horizontal, 5)
VStack {
Button(
action: { viewStore.send(.finishedPressed) },
label: { Text("recoveryPhraseDisplay.button.finished") }
)
.activeButtonStyle
.frame(height: 60)
Button(
action: {
viewStore.send(.copyToBufferPressed)
},
label: {
Text("recoveryPhraseDisplay.button.copyToBuffer")
.bodyText()
}
)
.frame(height: 60)
}
.padding()
} else {
Text("recoveryPhraseDisplay.noWords")
}
.padding(.top, 0)
.padding(.bottom, 20)
Spacer()
VStack(alignment: .leading, spacing: 10) {
ForEach(groups, id: \.startIndex) { group in
VStack {
HStack(alignment: .center) {
HStack {
Spacer()
Text("\(group.startIndex). \(group.words[0].data)")
Spacer()
}
.padding(.leading, 20)
HStack {
Spacer()
Text("\(group.startIndex + 1). \(group.words[1].data)")
Spacer()
}
.padding(.trailing, 20)
}
}
}
}
Spacer()
VStack {
Button(
action: { viewStore.send(.finishedPressed) },
label: { Text("recoveryPhraseDisplay.button.wroteItDown") }
)
.activeButtonStyle
.frame(height: 60)
}
.padding()
} else {
Text("recoveryPhraseDisplay.noWords")
}
}
.padding(.bottom, 20)

View File

@ -62,12 +62,15 @@ extension RootReducer {
case .phraseDisplay(.finishedPressed):
// user is still supposed to do the backup phrase validation test
if state.destinationState.previousDestination == .welcome
|| state.destinationState.previousDestination == .onboarding {
if (state.destinationState.previousDestination == .welcome
|| state.destinationState.previousDestination == .onboarding
|| state.destinationState.previousDestination == .startup)
&& state.walletConfig.isEnabled(.testBackupPhraseFlow) {
state.destinationState.destination = .phraseValidation
}
// user wanted to see the backup phrase once again (at validation finished screen)
if state.destinationState.previousDestination == .phraseValidation {
if state.destinationState.previousDestination == .phraseValidation
|| !state.walletConfig.isEnabled(.testBackupPhraseFlow) {
state.destinationState.destination = .home
}

View File

@ -149,8 +149,8 @@ extension RootReducer {
}
var landingDestination = RootReducer.DestinationState.Destination.home
if !storedWallet.hasUserPassedPhraseBackupTest {
if !storedWallet.hasUserPassedPhraseBackupTest && state.walletConfig.isEnabled(.testBackupPhraseFlow) {
do {
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase.value())
@ -178,7 +178,7 @@ extension RootReducer {
let birthday = zcashSDKEnvironment.latestCheckpoint
// store the wallet to the keychain
try walletStorage.importWallet(newRandomPhrase, birthday, .english, false)
try walletStorage.importWallet(newRandomPhrase, birthday, .english, !state.walletConfig.isEnabled(.testBackupPhraseFlow))
// start the backup phrase validation test
let randomRecoveryPhraseWords = try mnemonic.asWords(newRandomPhrase)
@ -198,7 +198,7 @@ extension RootReducer {
case .phraseValidation(.succeed):
do {
try walletStorage.markUserPassedPhraseBackupTest()
try walletStorage.markUserPassedPhraseBackupTest(true)
} catch {
// TODO: [#221] error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
}

View File

@ -23,10 +23,11 @@ struct RecoveryPhrase: Equatable, Redactable {
private let groupSize = 6
func toGroups() -> [Group] {
let chunks = words.count / groupSize
return zip(0 ..< chunks, words.chunked(into: groupSize)).map {
Group(startIndex: $0 * groupSize + 1, words: $1)
func toGroups(groupSizeOverride: Int? = nil) -> [Group] {
let internalGroupSize = groupSizeOverride ?? groupSize
let chunks = words.count / internalGroupSize
return zip(0 ..< chunks, words.chunked(into: internalGroupSize)).map {
Group(startIndex: $0 * internalGroupSize + 1, words: $1)
}
}

View File

@ -10,11 +10,13 @@ enum FeatureFlag: String, CaseIterable, Codable {
case testFlag1
case testFlag2
case onboardingFlow
case testBackupPhraseFlow
var enabledByDefault: Bool {
switch self {
case .testFlag1, .testFlag2: return false
case .onboardingFlow: return false
case .testBackupPhraseFlow: return false
}
}
}

View File

@ -20,9 +20,8 @@
// MARK: - Secret Recovery Phrase Display
"recoveryPhraseDisplay.title" = "Your Secret Recovery Phrase";
"recoveryPhraseDisplay.description" = "The following 24 words represent your funds and the security used to protect them.";
"recoveryPhraseDisplay.backItUp" = "Back them up now! There will be a test.";
"recoveryPhraseDisplay.button.finished" = "Finished!";
"recoveryPhraseDisplay.description" = "The following 24 words represent your funds and the security used to protect them. Back them up now!";
"recoveryPhraseDisplay.button.wroteItDown" = "I wrote it down!";
"recoveryPhraseDisplay.button.copyToBuffer" = "Copy To Buffer";
"recoveryPhraseDisplay.noWords" = "Oops no words";

View File

@ -47,7 +47,7 @@ extension Text {
func body(content: Content) -> some View {
content
.foregroundColor(Asset.Colors.Text.heading.color)
.font(.custom(FontFamily.Rubik.medium.name, size: 33, relativeTo: .callout))
.font(.custom(FontFamily.Rubik.medium.name, size: 24, relativeTo: .callout))
.shadow(color: Asset.Colors.Text.captionTextShadow.color, radius: 1, x: 0, y: 1)
}
}

View File

@ -0,0 +1,232 @@
//
// RecoveryPhraseValidationFlowFeatureFlagTests.swift
// secantTests
//
// Created by Lukáš Korba on 01.03.2023.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
// swiftlint:disable:next type_name
class RecoveryPhraseValidationFlowFeatureFlagTests: XCTestCase {
override func setUp() {
super.setUp()
UserDefaultsWalletConfigStorage().clearAll()
}
func testRecoveryPhraseValidationFlowOffByDefault() throws {
XCTAssertFalse(WalletConfig.default.isEnabled(.testBackupPhraseFlow))
}
func testRecoveryPhraseValidationFlow_SkipPuzzleUserConfirmedBackup() {
let store = TestStore(
initialState: .placeholder,
reducer: RootReducer()
)
store.send(.phraseDisplay(.finishedPressed)) { state in
state.destinationState.internalDestination = .home
state.destinationState.previousDestination = .welcome
}
}
func testRecoveryPhraseValidationFlow_StartPuzzle() {
var rootState = RootReducer.State.placeholder
var defaultRawFlags = WalletConfig.default.flags
defaultRawFlags[.testBackupPhraseFlow] = true
rootState.walletConfig = WalletConfig(flags: defaultRawFlags)
let store = TestStore(
initialState: rootState,
reducer: RootReducer()
)
store.send(.phraseDisplay(.finishedPressed))
}
func testRecoveryPhraseValidationFlow_SkipPuzzleStartOfTheApp() {
var rootState = RootReducer.State.placeholder
rootState.storedWallet = .placeholder
let store = TestStore(
initialState: rootState,
reducer: RootReducer()
)
store.dependencies.mainQueue = .immediate
store.send(.initialization(.checkBackupPhraseValidation)) { state in
state.appInitializationState = .initialized
}
store.receive(.destination(.updateDestination(.home))) { state in
state.destinationState.internalDestination = .home
state.destinationState.previousDestination = .welcome
}
}
func testRecoveryPhraseValidationFlow_StartPuzzleStartOfTheApp() {
var rootState = RootReducer.State.placeholder
rootState.storedWallet = .placeholder
var defaultRawFlags = WalletConfig.default.flags
defaultRawFlags[.testBackupPhraseFlow] = true
rootState.walletConfig = WalletConfig(flags: defaultRawFlags)
let store = TestStore(
initialState: rootState,
reducer: RootReducer()
)
let mnemonic =
"""
still champion voice habit trend flight \
survey between bitter process artefact blind \
carbon truly provide dizzy crush flush \
breeze blouse charge solid fish spread
"""
let randomRecoveryPhrase = RecoveryPhraseValidationFlowReducer.State.placeholder
store.dependencies.mainQueue = .immediate
store.dependencies.mnemonic = .noOp
store.dependencies.mnemonic.asWords = { _ in mnemonic.components(separatedBy: " ") }
store.dependencies.randomRecoveryPhrase.random = { _ in randomRecoveryPhrase }
store.send(.initialization(.checkBackupPhraseValidation)) { state in
state.appInitializationState = .initialized
state.phraseDisplayState.phrase = RecoveryPhrase(words: mnemonic.components(separatedBy: " ").map { $0.redacted })
}
store.receive(.destination(.updateDestination(.phraseDisplay))) { state in
state.destinationState.internalDestination = .phraseDisplay
state.destinationState.previousDestination = .welcome
}
}
@MainActor func testDidFinishLaunching_to_InitializedWalletPhraseBackupPuzzleOff() async throws {
// setup the store and environment to be fully mocked
let recoveryPhrase = RecoveryPhrase(words: try MnemonicClient.mock.randomMnemonicWords().map { $0.redacted })
let phraseValidationState = RecoveryPhraseValidationFlowReducer.State(
phrase: recoveryPhrase,
missingIndices: [2, 0, 3, 5],
missingWordChips: [
.unassigned(word: "voice".redacted),
.empty,
.unassigned(word: "survey".redacted),
.unassigned(word: "spread".redacted)
],
validationWords: [
.init(groupIndex: 2, word: "dizzy".redacted)
],
destination: nil
)
let recoveryPhraseRandomizer = RecoveryPhraseRandomizerClient(
random: { _ in
let missingIndices = [2, 0, 3, 5]
let missingWordChipKind = [
PhraseChip.Kind.unassigned(
word: "voice".redacted,
color: Asset.Colors.Buttons.activeButton.color
),
PhraseChip.Kind.empty,
PhraseChip.Kind.unassigned(
word: "survey".redacted,
color: Asset.Colors.Buttons.activeButton.color
),
PhraseChip.Kind.unassigned(
word: "spread".redacted,
color: Asset.Colors.Buttons.activeButton.color
)
]
return RecoveryPhraseValidationFlowReducer.State(
phrase: recoveryPhrase,
missingIndices: missingIndices,
missingWordChips: missingWordChipKind,
validationWords: [
ValidationWord(
groupIndex: 2,
word: "dizzy".redacted
)
]
)
}
)
let appState = RootReducer.State(
destinationState: .placeholder,
homeState: .placeholder,
onboardingState: .init(
walletConfig: .default,
importWalletState: .placeholder
),
phraseValidationState: phraseValidationState,
phraseDisplayState: RecoveryPhraseDisplayReducer.State(
phrase: recoveryPhrase
),
sandboxState: .placeholder,
walletConfig: .default,
welcomeState: .placeholder
)
let store = TestStore(
initialState: appState,
reducer: RootReducer()
)
let testQueue = DispatchQueue.test
store.dependencies.databaseFiles = .noOp
store.dependencies.databaseFiles.areDbFilesPresentFor = { _ in true }
store.dependencies.derivationTool = .liveValue
store.dependencies.mainQueue = .immediate// testQueue.eraseToAnyScheduler()
store.dependencies.mnemonic = .mock
store.dependencies.randomRecoveryPhrase = recoveryPhraseRandomizer
store.dependencies.walletStorage.exportWallet = { .placeholder }
store.dependencies.walletStorage.areKeysPresent = { true }
store.dependencies.walletConfigProvider = .noOp
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
await testQueue.advance(by: 0.02)
await store.receive(.initialization(.checkWalletConfig))
await store.receive(.walletConfigLoaded(WalletConfig.default))
await store.receive(.initialization(.initialSetups))
await testQueue.advance(by: 0.02)
await store.receive(.initialization(.configureCrashReporter))
await store.receive(.initialization(.checkWalletInitialization))
await store.receive(.initialization(.respondToWalletInitializationState(.initialized)))
await testQueue.advance(by: 3.00)
await store.receive(.initialization(.initializeSDK)) { state in
state.storedWallet = .placeholder
}
await store.receive(.initialization(.checkBackupPhraseValidation)) { state in
state.appInitializationState = .initialized
}
await store.receive(.destination(.updateDestination(.home))) { state in
state.destinationState.previousDestination = .welcome
state.destinationState.internalDestination = .home
}
await store.finish()
}
}

View File

@ -63,6 +63,10 @@ class AppInitializationTests: XCTestCase {
}
)
var defaultRawFlags = WalletConfig.default.flags
defaultRawFlags[.testBackupPhraseFlow] = true
let walletConfig = WalletConfig(flags: defaultRawFlags)
let appState = RootReducer.State(
destinationState: .placeholder,
homeState: .placeholder,
@ -75,7 +79,7 @@ class AppInitializationTests: XCTestCase {
phrase: recoveryPhrase
),
sandboxState: .placeholder,
walletConfig: .default,
walletConfig: walletConfig,
welcomeState: .placeholder
)

View File

@ -38,7 +38,7 @@ class SettingsTests: XCTestCase {
updateBirthday: { _ in
throw WalletStorage.KeychainError.encoding
},
markUserPassedPhraseBackupTest: {
markUserPassedPhraseBackupTest: { _ in
throw WalletStorage.KeychainError.encoding
},
nukeWallet: { }

View File

@ -8,6 +8,7 @@
import Combine
import XCTest
@testable import secant_testnet
import ComposableArchitecture
class WalletConfigProviderTests: XCTestCase {
var cancellables: [AnyCancellable] = []
@ -114,6 +115,24 @@ class WalletConfigProviderTests: XCTestCase {
return configuration
}
func testPropagationOfFlagUpdate() throws {
let store = TestStore(
initialState: .placeholder,
reducer: RootReducer()
)
// Change any of the flags from the default value
var defaultRawFlags = WalletConfig.default.flags
defaultRawFlags[.onboardingFlow] = true
let flags = WalletConfig(flags: defaultRawFlags)
// The new flag's value has to be propagated to all `walletConfig` instances
store.send(.debug(.walletConfigLoaded(flags))) { state in
state.walletConfig = flags
state.onboardingState.walletConfig = flags
}
}
}
struct WalletConfigSourceProviderMock: WalletConfigSourceProvider {