Scan flows done

This commit is contained in:
Lukas Korba 2025-03-12 13:38:59 +01:00
parent fcfca57eed
commit e5e1eb6e1b
10 changed files with 219 additions and 90 deletions

View File

@ -65,6 +65,12 @@ public struct AddressBookContactView: View {
}
store.send(.onAppear)
}
.alert(
store: store.scope(
state: \.$alert,
action: \.alert
)
)
}
.applyScreenBackground()
.zashiBack()

View File

@ -85,6 +85,7 @@ public struct AddressBook {
case deleteId(String)
case deleteIdConfirmed
case dismissAddContactRequired
case dismissDeleteContactRequired
case editId(String)
case fetchedABContacts(AddressBookContacts, Bool)
case fetchABContactsRequested
@ -188,9 +189,9 @@ public struct AddressBook {
state.deleteIdToConfirm = id
state.alert = AlertState.confirmDelete()
return .none
// broken
case .deleteIdConfirmed:
state.alert = nil
guard let deleteIdToConfirm = state.deleteIdToConfirm, let account = state.zashiWalletAccount else {
return .none
}
@ -206,8 +207,8 @@ public struct AddressBook {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
}
return .concatenate(
.send(.fetchedABContacts(abContacts, false))
// .send(.updateDestination(nil))
.send(.fetchedABContacts(abContacts, false)),
.send(.dismissDeleteContactRequired)
)
} catch {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
@ -306,6 +307,9 @@ public struct AddressBook {
case .dismissAddContactRequired:
return .none
case .dismissDeleteContactRequired:
return .none
}
}
}

View File

@ -67,12 +67,6 @@ public struct AddressBookView: View {
)
.navigationBarTitleDisplayMode(.inline)
.applyScreenBackground()
.alert(
store: store.scope(
state: \.$alert,
action: \.alert
)
)
}
}

View File

@ -34,6 +34,8 @@ public struct Home {
public var moreRequest = false
public var syncProgressState: SyncProgress.State
public var walletConfig: WalletConfig
// public var scanBinding = false
// public var scanState = Scan.State.initial
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
public var transactionListState: TransactionList.State
public var uAddress: UnifiedAddress? = nil
@ -98,6 +100,7 @@ public struct Home {
case resolveReviewRequest
case retrySync
case reviewRequestFinished
// case scan(Scan.Action)
case scanTapped
case seeAllTransactionsTapped
case sendTapped
@ -131,6 +134,10 @@ public struct Home {
TransactionList()
}
// Scope(state: \.scanState, action: \.scan) {
// Scan()
// }
Scope(state: \.syncProgressState, action: \.syncProgress) {
SyncProgress()
}
@ -142,6 +149,7 @@ public struct Home {
Reduce { state, action in
switch action {
case .onAppear:
// state.scanState.checkers = [.zcashAddressScanChecker, .requestZecScanChecker]
state.appId = PartnerKeys.cbProjectId
state.walletBalancesState.migratingDatabase = state.migratingDatabase
state.migratingDatabase = false
@ -164,9 +172,13 @@ public struct Home {
.cancel(id: CancelEventId)
)
case .receiveTapped, .sendTapped, .scanTapped:
case .receiveTapped, .sendTapped:
return .none
case .scanTapped:
// state.scanBinding = true
return .none
case .moreTapped:
state.moreRequest = true
return .none
@ -261,6 +273,10 @@ public struct Home {
state.alert = nil
return .none
// case .scan(.cancelTapped):
// state.scanBinding = false
// return .none
case .alert:
return .none
@ -273,6 +289,9 @@ public struct Home {
case .currencyConversionSetupTapped:
return .none
// case .scan:
// return .none
// Accounts
case .accountSwitchTapped:

View File

@ -9,6 +9,7 @@ import SyncProgress
import Utils
import Models
import WalletBalances
import Scan
public struct HomeView: View {
@Environment(\.colorScheme) var colorScheme
@ -225,6 +226,15 @@ public struct HomeView: View {
.onAppear {
store.send(.onAppear)
}
// .popover(isPresented: $store.scanBinding) {
// ScanView(
// store:
// store.scope(
// state: \.scanState,
// action: \.scan
// )
// )
// }
.onChange(of: store.canRequestReview) { canRequestReview in
if canRequestReview {
if let currentScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {

View File

@ -107,10 +107,26 @@ extension Root {
case .path(.element(id: _, action: .addressBook(.addManualButtonTapped))):
state.path.append(.addressBookContact(AddressBook.State.initial))
return .none
case .path(.element(id: _, action: .addressBook(.scanButtonTapped))):
var scanState = Scan.State.initial
scanState.checkers = [.zcashAddressScanChecker]
state.path.append(.scan(scanState))
return .none
// MARK: - Address Book Contact
case .path(.element(id: _, action: .addressBookContact(.dismissAddContactRequired))):
let _ = state.path.popLast()
for element in state.path {
if element.is(\.scan) {
let _ = state.path.popLast()
return .none
}
}
return .none
case .path(.element(id: _, action: .addressBookContact(.dismissDeleteContactRequired))):
let _ = state.path.popLast()
return .none
@ -149,19 +165,25 @@ extension Root {
.cancellable(id: CancelFlexaId, cancelInFlight: true)
// MARK: - Home
case .home(.receiveTapped):
state.path.append(.receive(Receive.State.initial))
return .none
case .home(.settingsTapped):
state.path.append(.settings(Settings.State.initial))
return .none
case .home(.receiveTapped):
state.path.append(.receive(Receive.State.initial))
return .none
case .home(.sendTapped):
state.path.append(.sendForm(SendForm.State.initial))
return .none
case .home(.scanTapped):
var scanState = Scan.State.initial
scanState.checkers = [.zcashAddressScanChecker, .requestZecScanChecker]
state.path.append(.scan(scanState))
return .none
case .home(.flexaTapped):
return .send(.flexaOpenRequest)
@ -241,6 +263,61 @@ extension Root {
}
return .none
case .path(.element(id: _, action: .scan(.found(let address)))):
// Handling of scan used in address book to add a contact
// This handling must preceed the next one with Send Form check
for (id, element) in zip(state.path.ids, state.path) {
if element.is(\.addressBook) {
var addressBookState = AddressBook.State.initial
addressBookState.address = address.data
addressBookState.isValidZcashAddress = true
addressBookState.isNameFocused = true
state.path.append(.addressBookContact(addressBookState))
audioServices.systemSoundVibrate()
return .none
}
}
// Handling of scan used in the Send Form
for (id, element) in zip(state.path.ids, state.path) {
if element.is(\.sendForm) {
state.path[id: id, case: \.sendForm]?.address = address
state.path[id: id, case: \.sendForm]?.isValidAddress = true
state.path[id: id, case: \.sendForm]?.isValidTransparentAddress = derivationTool.isTransparentAddress(
address.data,
zcashSDKEnvironment.network.networkType
)
state.path[id: id, case: \.sendForm]?.isValidTexAddress = derivationTool.isTexAddress(
address.data,
zcashSDKEnvironment.network.networkType
)
audioServices.systemSoundVibrate()
let _ = state.path.popLast()
return .none
}
}
// Scan from Home, Send Form is the following flow
if state.path.ids.count == 1 {
var sendFormState = SendForm.State.initial
sendFormState.address = address
sendFormState.isValidAddress = true
sendFormState.isValidTransparentAddress = derivationTool.isTransparentAddress(
address.data,
zcashSDKEnvironment.network.networkType
)
sendFormState.isValidTexAddress = derivationTool.isTexAddress(
address.data,
zcashSDKEnvironment.network.networkType
)
state.path.append(.sendForm(sendFormState))
audioServices.systemSoundVibrate()
}
return .none
case .path(.element(id: _, action: .scan(.cancelTapped))):
let _ = state.path.popLast()
return .none
// MARK: - Send Confirmation
case .path(.element(id: _, action: .sendConfirmation(.cancelTapped))):
@ -332,6 +409,12 @@ extension Root {
addressBookState.isInSelectMode = true
state.path.append(.addressBook(addressBookState))
return .none
case .path(.element(id: _, action: .sendForm(.scanTapped))):
var scanState = Scan.State.initial
scanState.checkers = [.zcashAddressScanChecker, .requestZecScanChecker]
state.path.append(.scan(scanState))
return .none
case .path(.element(id: _, action: .sendForm(.confirmationRequired(let confirmationType)))):
var sendConfirmationState = SendConfirmation.State.initial

View File

@ -27,79 +27,83 @@ public struct ScanView: View {
public var body: some View {
WithPerceptionTracking {
ZStack {
GeometryReader { proxy in
QRCodeScanView(
rectOfInterest: ScanView.normalizedRectsOfInterest().real,
onQRScanningDidFail: { store.send(.scanFailed(.invalidQRCode)) },
onQRScanningSucceededWithCode: { store.send(.scan($0.redacted)) }
)
frameOfInterest(proxy.size)
WithPerceptionTracking {
if store.isTorchAvailable {
torchButton(size: proxy.size)
}
if showSheet {
ZashiImagePicker(selectedImage: $image, showSheet: $showSheet)
} else {
GeometryReader { proxy in
QRCodeScanView(
rectOfInterest: ScanView.normalizedRectsOfInterest().real,
onQRScanningDidFail: { store.send(.scanFailed(.invalidQRCode)) },
onQRScanningSucceededWithCode: { store.send(.scan($0.redacted)) }
)
if !store.forceLibraryToHide {
libraryButton(size: proxy.size)
}
}
WithPerceptionTracking {
if store.progress != nil {
WithPerceptionTracking {
progress(size: proxy.size, progress: store.countedProgress)
frameOfInterest(proxy.size)
WithPerceptionTracking {
if store.isTorchAvailable {
torchButton(size: proxy.size)
}
if !store.forceLibraryToHide {
libraryButton(size: proxy.size)
}
}
}
}
VStack {
WithPerceptionTracking {
if let instructions = store.instructions {
Text(instructions)
.font(.custom(FontFamily.Inter.semiBold.name, size: 20))
.foregroundColor(Asset.Colors.ZDesign.shark200.color)
.padding(.top, 64)
.lineLimit(nil)
.multilineTextAlignment(.center)
.lineSpacing(3)
.screenHorizontalPadding()
}
Spacer()
HStack(alignment: .top, spacing: 0) {
if !store.info.isEmpty {
Asset.Assets.infoOutline.image
.zImage(size: 20, color: Asset.Colors.ZDesign.shark200.color)
.padding(.trailing, 12)
Text(store.info)
.font(.custom(FontFamily.Inter.medium.name, size: 12))
.foregroundColor(Asset.Colors.ZDesign.shark200.color)
.padding(.top, 2)
Spacer(minLength: 0)
}
}
.padding(.bottom, 15)
if !store.isCameraEnabled {
primaryButton(L10n.Scan.openSettings) {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
WithPerceptionTracking {
if store.progress != nil {
WithPerceptionTracking {
progress(size: proxy.size, progress: store.countedProgress)
}
}
} else {
primaryButton(L10n.General.cancel) {
store.send(.cancelTapped)
}
}
VStack {
WithPerceptionTracking {
if let instructions = store.instructions {
Text(instructions)
.font(.custom(FontFamily.Inter.semiBold.name, size: 20))
.foregroundColor(Asset.Colors.ZDesign.shark200.color)
.padding(.top, 64)
.lineLimit(nil)
.multilineTextAlignment(.center)
.lineSpacing(3)
.screenHorizontalPadding()
}
Spacer()
HStack(alignment: .top, spacing: 0) {
if !store.info.isEmpty {
Asset.Assets.infoOutline.image
.zImage(size: 20, color: Asset.Colors.ZDesign.shark200.color)
.padding(.trailing, 12)
Text(store.info)
.font(.custom(FontFamily.Inter.medium.name, size: 12))
.foregroundColor(Asset.Colors.ZDesign.shark200.color)
.padding(.top, 2)
Spacer(minLength: 0)
}
}
.padding(.bottom, 15)
if !store.isCameraEnabled {
primaryButton(L10n.Scan.openSettings) {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
} else {
primaryButton(L10n.General.cancel) {
store.send(.cancelTapped)
}
}
}
}
.screenHorizontalPadding()
}
.screenHorizontalPadding()
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea()
@ -112,12 +116,16 @@ public struct ScanView: View {
store.send(.libraryImage(img))
}
}
.overlay {
if showSheet {
ZashiImagePicker(selectedImage: $image, showSheet: $showSheet)
.ignoresSafeArea()
}
}
// .navigationLinkEmpty(isActive: $showSheet) {
// ZashiImagePicker(selectedImage: $image, showSheet: $showSheet)
// //.ignoresSafeArea()
// }
// .overlay {
// if showSheet {
// ZashiImagePicker(selectedImage: $image, showSheet: $showSheet)
// .ignoresSafeArea()
// }
// }
}
}

View File

@ -30,8 +30,7 @@ struct ZashiImagePicker: UIViewControllerRepresentable {
parent.showSheet = false
}
}
@Environment(\.presentationMode) private var presentationMode
@Binding var selectedImage: UIImage?
@Binding var showSheet: Bool
@ -39,7 +38,7 @@ struct ZashiImagePicker: UIViewControllerRepresentable {
context: UIViewControllerRepresentableContext<ZashiImagePicker>
) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = .photoLibrary
imagePicker.delegate = context.coordinator

View File

@ -49,6 +49,7 @@ public struct SendForm {
public var isCurrencyConversionEnabled = false
public var isNotAddressInAddressBook = false
public var isPaymentRequestInProgress = false
public var isPopToRootBack = false
public var isValidAddress = false
public var isValidTransparentAddress = false
public var isValidTexAddress = false
@ -212,6 +213,7 @@ public struct SendForm {
case requestsAddressFocusResolved
case resetForm
case reviewTapped
case scanTapped
// case scan(Scan.Action)
case sendFailed(ZcashError, Confirmation)
case syncAmounts(Bool)
@ -494,6 +496,9 @@ public struct SendForm {
case .dismissRequired:
return .none
case .scanTapped:
return .none
}
}
}

View File

@ -77,7 +77,7 @@ public struct SendFormView: View {
}
fieldButton(icon: Asset.Assets.Icons.qr.image) {
// store.send(.updateDestination(.scanQR))
store.send(.scanTapped)
}
}
}
@ -214,7 +214,8 @@ public struct SendFormView: View {
}
.padding(.vertical, 1)
.applyScreenBackground()
.zashiBack { store.send(.dismissRequired) }
.zashiBack(hidden: store.isPopToRootBack) { store.send(.dismissRequired) }
.zashiBackV2(hidden: !store.isPopToRootBack) { store.send(.dismissRequired) }
.alert(store: store.scope(
state: \.$alert,
action: \.alert