[#517] QR codes integration into the wallet details and send feature (#518)

- Address details have been extended to show QR codes for the addresses
- Profile + Address views are now scroll views
- Send feature now supports scan QR feature, when valid zcash address is scanned the address is automatically filled
- snapshot tests & unit tests fixed and extended accordingly
This commit is contained in:
Lukas Korba 2023-01-12 13:04:36 +01:00 committed by GitHub
parent 64d509aedb
commit f978565f38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 10 deletions

View File

@ -13,8 +13,11 @@ struct AddressDetailsView: View {
var body: some View {
WithViewStore(store) { viewStore in
VStack {
ScrollView {
Text("Unified Address")
.fontWeight(.bold)
qrCode(viewStore.unifiedAddress)
.padding(30)
Text("\(viewStore.unifiedAddress)")
.onTapGesture {
@ -22,7 +25,10 @@ struct AddressDetailsView: View {
}
Text("Sapling Address")
.fontWeight(.bold)
.padding(.top, 20)
qrCode(viewStore.saplingAddress)
.padding(30)
Text("\(viewStore.saplingAddress)")
.onTapGesture {
@ -30,7 +36,10 @@ struct AddressDetailsView: View {
}
Text("Transparent Address")
.fontWeight(.bold)
.padding(.top, 20)
qrCode(viewStore.transparentAddress)
.padding(30)
Text("\(viewStore.transparentAddress)")
.onTapGesture {
@ -43,6 +52,29 @@ struct AddressDetailsView: View {
}
}
extension AddressDetailsView {
func qrCode(_ qrText: String) -> some View {
Group {
if let img = QRCodeGenerator.generate(from: qrText) {
Image(img, scale: 1, label: Text(String(format: NSLocalizedString("QR Code for %@", comment: ""), "\(qrText)") ))
.cornerRadius(20)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.white, lineWidth: 25)
.scaleEffect(1.1)
)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.black, lineWidth: 8)
.scaleEffect(1.1)
)
} else {
Image(systemName: "qrcode")
}
}
}
}
struct AddressDetails_Previews: PreviewProvider {
static var previews: some View {
AddressDetailsView(store: .placeholder)

View File

@ -6,7 +6,7 @@ struct ProfileView: View {
var body: some View {
WithViewStore(store) { viewStore in
VStack {
ScrollView {
qrCodeUA(viewStore.unifiedAddress)
.padding(.top, 30)

View File

@ -19,15 +19,17 @@ struct SendFlowReducer: ReducerProtocol {
enum Destination: Equatable {
case confirmation
case inProgress
case scanQR
case success
case failure
case done
}
var addMemoState: Bool
var destination: Destination?
var isSendingTransaction = false
var memoState: MultiLineTextFieldReducer.State
var destination: Destination?
var scanState: ScanReducer.State
var shieldedBalance = WalletBalance.zero
var transactionAddressInputState: TransactionAddressTextFieldReducer.State
var transactionAmountInputState: TransactionAmountTextFieldReducer.State
@ -78,6 +80,7 @@ struct SendFlowReducer: ReducerProtocol {
case memo(MultiLineTextFieldReducer.Action)
case onAppear
case onDisappear
case scan(ScanReducer.Action)
case sendConfirmationPressed
case sendTransactionResult(Result<TransactionState, NSError>)
case synchronizerStateChanged(SDKSynchronizerState)
@ -86,6 +89,7 @@ struct SendFlowReducer: ReducerProtocol {
case updateDestination(SendFlowReducer.State.Destination?)
}
@Dependency(\.audioServices) var audioServices
@Dependency(\.derivationTool) var derivationTool
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.mnemonic) var mnemonic
@ -110,6 +114,10 @@ struct SendFlowReducer: ReducerProtocol {
TransactionAmountTextFieldReducer()
}
Scope(state: \.scanState, action: /Action.scan) {
ScanReducer()
}
Reduce { state, action in
switch action {
case .addMemo:
@ -188,6 +196,9 @@ struct SendFlowReducer: ReducerProtocol {
case .transactionAmountInput:
return .none
case .transactionAddressInput(.scanQR):
return Effect(value: .updateDestination(.scanQR))
case .transactionAddressInput:
return .none
@ -213,6 +224,17 @@ struct SendFlowReducer: ReducerProtocol {
case .memo:
return .none
case .scan(.found(let address)):
state.transactionAddressInputState.textFieldState.text = address
// The is valid Zcash address check is already covered in the scan feature
// so we can be sure it's valid and thus `true` value here.
state.transactionAddressInputState.isValidAddress = true
audioServices.systemSoundVibrate()
return Effect(value: .updateDestination(nil))
case .scan:
return .none
}
}
}
@ -227,13 +249,20 @@ extension SendFlowStore {
action: SendFlowReducer.Action.addMemo
)
}
func memoStore() -> MultiLineTextFieldStore {
self.scope(
state: \.memoState,
action: SendFlowReducer.Action.memo
)
}
func scanStore() -> ScanStore {
self.scope(
state: \.scanState,
action: SendFlowReducer.Action.scan
)
}
}
// MARK: - ViewStore
@ -282,6 +311,15 @@ extension SendFlowViewStore {
embed: { _ in SendFlowReducer.State.Destination.failure }
)
}
var bindingForScanQR: Binding<Bool> {
self.destinationBinding.map(
extract: {
$0 == .scanQR
},
embed: { $0 ? SendFlowReducer.State.Destination.scanQR : nil }
)
}
}
// MARK: Placeholders
@ -290,8 +328,9 @@ extension SendFlowReducer.State {
static var placeholder: Self {
.init(
addMemoState: true,
memoState: .placeholder,
destination: nil,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .amount
)
@ -300,8 +339,9 @@ extension SendFlowReducer.State {
static var emptyPlaceholder: Self {
.init(
addMemoState: true,
memoState: .placeholder,
destination: nil,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
)

View File

@ -22,6 +22,12 @@ struct SendFlowView: View {
TransactionConfirmation(store: store)
}
)
.navigationLinkEmpty(
isActive: viewStore.bindingForScanQR,
destination: {
ScanView(store: store.scanStore())
}
)
}
}
}
@ -35,8 +41,9 @@ struct SendFLowView_Previews: PreviewProvider {
store: .init(
initialState: .init(
addMemoState: true,
memoState: .placeholder,
destination: nil,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
),

View File

@ -36,9 +36,13 @@ struct TransactionAddressTextField: View {
},
inputPrefixView: { EmptyView() },
inputAccessoryView: {
Image(Asset.Assets.Icons.qrCode.name)
.resizable()
.frame(width: 30, height: 30)
Button {
viewStore.send(.scanQR)
} label: {
Image(Asset.Assets.Icons.qrCode.name)
.resizable()
.frame(width: 30, height: 30)
}
}
)
}

View File

@ -18,6 +18,7 @@ struct TransactionAddressTextFieldReducer: ReducerProtocol {
enum Action: Equatable {
case clearAddress
case scanQR
case textField(TCATextFieldReducer.Action)
}
@ -29,6 +30,9 @@ struct TransactionAddressTextFieldReducer: ReducerProtocol {
case .clearAddress:
state.textFieldState.text = ""
return .none
case .scanQR:
return .none
case .textField(.set(let address)):
do {

View File

@ -300,6 +300,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
@ -339,6 +340,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
@ -397,6 +399,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
@ -436,6 +439,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
@ -474,6 +478,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
@ -511,6 +516,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
@ -549,6 +555,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: MultiLineTextFieldReducer.State(charLimit: 3),
scanState: .placeholder,
shieldedBalance: WalletBalance(verified: Zatoshi(1), total: Zatoshi(1)),
transactionAddressInputState:
TransactionAddressTextFieldReducer.State(
@ -590,6 +597,7 @@ class SendTests: XCTestCase {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldReducer.State(
@ -618,6 +626,32 @@ class SendTests: XCTestCase {
// .onDisappear cancels it, must have for the test to pass
store.send(.onDisappear)
}
func testScannedAddress() throws {
let sendState = SendFlowReducer.State(
addMemoState: true,
memoState: .placeholder,
scanState: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState: .placeholder
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer()
)
store.dependencies.audioServices = AudioServicesClient(systemSoundVibrate: { })
// We don't need to pass a valid address here, we just need to confirm some
// found string is received and the `isValidAddress` flag is set to `true`
store.send(.scan(.found("address"))) { state in
state.transactionAddressInputState.textFieldState.text = "address"
state.transactionAddressInputState.isValidAddress = true
}
store.receive(.updateDestination(nil))
}
}
private extension SendTests {