2021-10-26 16:14:03 -07:00
|
|
|
//
|
|
|
|
// RecoveryPhraseDisplayStore.swift
|
|
|
|
// secant-testnet
|
|
|
|
//
|
|
|
|
// Created by Francisco Gindre on 10/26/21.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import ComposableArchitecture
|
|
|
|
import UIKit
|
|
|
|
|
|
|
|
enum RecoveryPhraseError: Error {
|
|
|
|
/// This error is thrown then the Recovery Phrase can't be generated
|
|
|
|
case unableToGeneratePhrase
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Pasteboard {
|
|
|
|
let setString: (String) -> Void
|
|
|
|
let getString: () -> String?
|
|
|
|
}
|
|
|
|
|
|
|
|
extension Pasteboard {
|
|
|
|
private struct TestPasteboard {
|
|
|
|
static var general = TestPasteboard()
|
|
|
|
var string: String?
|
|
|
|
}
|
|
|
|
|
|
|
|
static let live = Pasteboard(
|
|
|
|
setString: { UIPasteboard.general.string = $0 },
|
|
|
|
getString: { UIPasteboard.general.string }
|
|
|
|
)
|
|
|
|
|
|
|
|
static let test = Pasteboard(
|
|
|
|
setString: { TestPasteboard.general.string = $0 },
|
|
|
|
getString: { TestPasteboard.general.string }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
struct BackupPhraseEnvironment {
|
|
|
|
let mainQueue: AnySchedulerOf<DispatchQueue>
|
|
|
|
let newPhrase: () -> Effect<RecoveryPhrase, RecoveryPhraseError>
|
|
|
|
let pasteboard: Pasteboard
|
2022-02-23 08:49:17 -08:00
|
|
|
let feedbackGenerator: FeedbackGenerator
|
2021-10-26 16:14:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
extension BackupPhraseEnvironment {
|
|
|
|
private struct DemoPasteboard {
|
|
|
|
static var general = Self()
|
|
|
|
var string: String?
|
|
|
|
}
|
|
|
|
|
|
|
|
static let demo = Self(
|
|
|
|
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
2021-12-13 12:50:04 -08:00
|
|
|
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
2022-02-23 08:49:17 -08:00
|
|
|
pasteboard: .test,
|
2022-02-23 09:39:39 -08:00
|
|
|
feedbackGenerator: .silent
|
2021-10-26 16:14:03 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
static let live = Self(
|
|
|
|
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
2021-12-13 12:50:04 -08:00
|
|
|
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
2022-02-23 08:49:17 -08:00
|
|
|
pasteboard: .live,
|
2022-02-23 09:39:39 -08:00
|
|
|
feedbackGenerator: .haptic
|
2021-10-26 16:14:03 -07:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
typealias RecoveryPhraseDisplayStore = Store<RecoveryPhraseDisplayState, RecoveryPhraseDisplayAction>
|
|
|
|
|
|
|
|
struct RecoveryPhrase: Equatable {
|
2021-12-13 12:50:04 -08:00
|
|
|
struct Group: Hashable {
|
2021-10-26 16:14:03 -07:00
|
|
|
var startIndex: Int
|
|
|
|
var words: [String]
|
|
|
|
}
|
|
|
|
|
|
|
|
let words: [String]
|
|
|
|
|
2021-12-13 12:50:04 -08:00
|
|
|
private let groupSize = 6
|
2021-10-26 16:14:03 -07:00
|
|
|
|
2021-12-13 12:50:04 -08:00
|
|
|
func toGroups() -> [Group] {
|
|
|
|
let chunks = words.count / groupSize
|
|
|
|
return zip(0 ..< chunks, words.chunked(into: groupSize)).map {
|
|
|
|
Group(startIndex: $0 * groupSize + 1, words: $1)
|
2021-10-26 16:14:03 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func toString() -> String {
|
|
|
|
words.joined(separator: " ")
|
|
|
|
}
|
2021-12-13 12:50:04 -08:00
|
|
|
|
|
|
|
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])
|
|
|
|
}
|
|
|
|
}
|
2021-10-26 16:14:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
struct RecoveryPhraseDisplayState: Equatable {
|
|
|
|
var phrase: RecoveryPhrase?
|
|
|
|
var showCopyToBufferAlert = false
|
|
|
|
}
|
|
|
|
|
|
|
|
enum RecoveryPhraseDisplayAction: Equatable {
|
|
|
|
case createPhrase
|
|
|
|
case copyToBufferPressed
|
|
|
|
case finishedPressed
|
|
|
|
case phraseResponse(Result<RecoveryPhrase, RecoveryPhraseError>)
|
|
|
|
}
|
|
|
|
|
|
|
|
typealias RecoveryPhraseDisplayReducer = Reducer<RecoveryPhraseDisplayState, RecoveryPhraseDisplayAction, BackupPhraseEnvironment>
|
|
|
|
|
|
|
|
extension RecoveryPhraseDisplayReducer {
|
|
|
|
static let `default` = RecoveryPhraseDisplayReducer { state, action, environment in
|
|
|
|
switch action {
|
|
|
|
case .createPhrase:
|
|
|
|
return environment.newPhrase()
|
|
|
|
.receive(on: environment.mainQueue)
|
|
|
|
.catchToEffect(RecoveryPhraseDisplayAction.phraseResponse)
|
|
|
|
case .copyToBufferPressed:
|
|
|
|
guard let phrase = state.phrase?.toString() else { return .none }
|
|
|
|
environment.pasteboard.setString(phrase)
|
|
|
|
state.showCopyToBufferAlert = true
|
|
|
|
return .none
|
|
|
|
case .finishedPressed:
|
|
|
|
// TODO: remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/47
|
|
|
|
return .none
|
|
|
|
case let .phraseResponse(.success(phrase)):
|
|
|
|
state.phrase = phrase
|
|
|
|
return .none
|
|
|
|
case .phraseResponse(.failure):
|
|
|
|
// TODO: remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/129
|
|
|
|
return .none
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension Array {
|
|
|
|
func chunked(into size: Int) -> [[Element]] {
|
|
|
|
return stride(from: 0, to: count, by: size).map {
|
|
|
|
Array(self[$0 ..< Swift.min($0 + size, count)])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|