From cc7b767a5af4f8e7eac0811588899e8507a09f67 Mon Sep 17 00:00:00 2001 From: Michal Fousek Date: Thu, 22 Sep 2022 21:20:46 +0200 Subject: [PATCH] [#379] Show alert before follow a Block explorer link (#423) Closes #379 - `TransactionDetailView` is updated and instead of `ViewStore` it now has `Store`. `Store` is required to show alert. It's not possible with `ViewStore`. - There are three more actions added to `WalletEventsFlowAction`. These are used to handle the new alert. - Block explorer URL is changed to https://zcashblockexplorer.com. New URL scheme is derived from how URLs looks now when some transaction is opened. --- .../Views/TransactionDetailView.swift | 141 ++++++++---------- .../WalletEventsFlowStore.swift | 33 ++++ .../WalletEventsFlowView.swift | 2 +- secant/Models/TransactionState.swift | 2 +- secant/Models/WalletEvent.swift | 4 +- .../WalletEventsSnapshotTests.swift | 8 +- 6 files changed, 106 insertions(+), 84 deletions(-) diff --git a/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift b/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift index 6ab158e..178e8cb 100644 --- a/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift +++ b/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift @@ -12,49 +12,51 @@ struct TransactionDetailView: View { } var transaction: TransactionState - var viewStore: WalletEventsFlowViewStore + var store: WalletEventsFlowStore var body: some View { - ScrollView { - header + WithViewStore(store) { viewStore in + ScrollView { + header - switch transaction.status { - case .paid(success: _): - plainText("You sent \(transaction.zecAmount.decimalString()) ZEC") - plainText("fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) - plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) - address(mark: .inactive) - if let text = transaction.memo { memo(text, viewStore, mark: .highlight) } - confirmed(mark: .success) - case .pending: - plainText("You are sending \(transaction.zecAmount.decimalString()) ZEC") - plainText("Includes network fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) - plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) - if let text = transaction.memo { memo(text, viewStore, mark: .inactive) } - confirming(mark: .highlight) - case .received: - plainText("You received \(transaction.zecAmount.decimalString()) ZEC") - plainText("fee \(transaction.fee.decimalString()) ZEC") - plainText("total amount \(transaction.totalAmount.decimalString()) ZEC") - address(mark: .inactive) - if let text = transaction.memo { memo(text, viewStore, mark: .highlight) } - confirmed(mark: .success) - case .failed: - plainText("You DID NOT send \(transaction.zecAmount.decimalString()) ZEC", mark: .fail) - plainText("Includes network fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) - plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) - if let text = transaction.memo { memo(text, viewStore, mark: .inactive) } - if let errorMessage = transaction.errorMessage { - plainTwoColumnText(left: "Failed", right: errorMessage, mark: .fail) + switch transaction.status { + case .paid(success: _): + plainText("You sent \(transaction.zecAmount.decimalString()) ZEC") + plainText("fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) + plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) + address(mark: .inactive, viewStore: viewStore) + if let text = transaction.memo { memo(text, viewStore, mark: .highlight) } + confirmed(mark: .success, viewStore: viewStore) + case .pending: + plainText("You are sending \(transaction.zecAmount.decimalString()) ZEC") + plainText("Includes network fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) + plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) + if let text = transaction.memo { memo(text, viewStore, mark: .inactive) } + confirming(mark: .highlight, viewStore: viewStore) + case .received: + plainText("You received \(transaction.zecAmount.decimalString()) ZEC") + plainText("fee \(transaction.fee.decimalString()) ZEC") + plainText("total amount \(transaction.totalAmount.decimalString()) ZEC") + address(mark: .inactive, viewStore: viewStore) + if let text = transaction.memo { memo(text, viewStore, mark: .highlight) } + confirmed(mark: .success, viewStore: viewStore) + case .failed: + plainText("You DID NOT send \(transaction.zecAmount.decimalString()) ZEC", mark: .fail) + plainText("Includes network fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) + plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) + if let text = transaction.memo { memo(text, viewStore, mark: .inactive) } + if let errorMessage = transaction.errorMessage { + plainTwoColumnText(left: "Failed", right: errorMessage, mark: .fail) + } } - } - Spacer() - - footer + Spacer() + + footer + } + .applyScreenBackground() + .navigationTitle("Transaction detail") } - .applyScreenBackground() - .navigationTitle("Transaction detail") } } @@ -92,7 +94,7 @@ extension TransactionDetailView { .transactionDetailRow(mark: mark) } - func address(mark: RowMark = .neutral) -> some View { + func address(mark: RowMark = .neutral, viewStore: WalletEventsFlowViewStore) -> some View { Button { viewStore.send(.copyToPastboard(transaction.address)) } label: { @@ -137,7 +139,7 @@ extension TransactionDetailView { } } - func confirmed(mark: RowMark = .neutral) -> some View { + func confirmed(mark: RowMark = .neutral, viewStore: WalletEventsFlowViewStore) -> some View { HStack { Text("Confirmed") Spacer() @@ -146,7 +148,7 @@ extension TransactionDetailView { .transactionDetailRow(mark: mark) } - func confirming(mark: RowMark = .neutral) -> some View { + func confirming(mark: RowMark = .neutral, viewStore: WalletEventsFlowViewStore) -> some View { HStack { Text("Confirming ~\(viewStore.requiredTransactionConfirmations)mins") Spacer() @@ -156,31 +158,30 @@ extension TransactionDetailView { } var footer: some View { - VStack { - Button { - viewStore.send(.copyToPastboard(transaction.id)) - } label: { - Text("txn: \(transaction.id)") - .foregroundColor(Asset.Colors.Text.transactionDetailText.color) - .font(.system(size: 14)) - .fontWeight(.thin) - .frame(maxWidth: .infinity, alignment: .center) - .padding(.horizontal, 60) - .padding(.vertical, 10) - .background(Asset.Colors.BackgroundColors.numberedChip.color) - .padding(.vertical, 30) - } - - Button { } label: { - // TODO: Warn users that they will leave the App when they follow a Block explorer - // https://github.com/zcash/secant-ios-wallet/issues/379 - if let viewOnlineURL = transaction.viewOnlineURL { - Link("View online", destination: viewOnlineURL) + WithViewStore(store) { viewStore in + VStack { + Button { + viewStore.send(.copyToPastboard(transaction.id)) + } label: { + Text("txn: \(transaction.id)") + .foregroundColor(Asset.Colors.Text.transactionDetailText.color) + .font(.system(size: 14)) + .fontWeight(.thin) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 60) + .padding(.vertical, 10) + .background(Asset.Colors.BackgroundColors.numberedChip.color) + .padding(.vertical, 30) } + + Button("View online") { + viewStore.send(.warnBeforeLeavingApp(transaction.viewOnlineURL)) + } + .activeButtonStyle + .frame(height: 50) + .padding(.horizontal, 30) } - .activeButtonStyle - .frame(height: 50) - .padding(.horizontal, 30) + .alert(self.store.scope(state: \.alert), dismiss: .dismissAlert) } } } @@ -266,19 +267,7 @@ struct TransactionDetail_Previews: PreviewProvider { timestamp: 1234567, zecAmount: Zatoshi(25_000_000) ), - viewStore: ViewStore( - WalletEventsFlowStore( - initialState: .placeHolder, - reducer: .default, - environment: - WalletEventsFlowEnvironment( - pasteboard: .test, - scheduler: DispatchQueue.main.eraseToAnyScheduler(), - SDKSynchronizer: MockWrappedSDKSynchronizer(), - zcashSDKEnvironment: .testnet - ) - ) - ) + store: WalletEventsFlowStore.placeholder ) .preferredColorScheme(.dark) } diff --git a/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift b/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift index 353d099..1e0362b 100644 --- a/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift +++ b/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift @@ -17,6 +17,7 @@ struct WalletEventsFlowState: Equatable { var route: Route? + @BindableState var alert: AlertState? var latestMinedHeight: BlockHeight? var isScrollable = false var requiredTransactionConfirmations = 0 @@ -28,12 +29,15 @@ struct WalletEventsFlowState: Equatable { enum WalletEventsFlowAction: Equatable { case copyToPastboard(String) + case dismissAlert case onAppear case onDisappear + case openBlockExplorer(URL?) case updateRoute(WalletEventsFlowState.Route?) case replyTo(String) case synchronizerStateChanged(WrappedSDKSynchronizerState) case updateWalletEvents([WalletEvent]) + case warnBeforeLeavingApp(URL?) } // MARK: - Environment @@ -100,6 +104,35 @@ extension WalletEventsFlowReducer { case .replyTo(let address): return .none + + case .dismissAlert: + state.alert = nil + return .none + + case .warnBeforeLeavingApp(let blockExplorerURL): + state.alert = AlertState( + title: TextState("You are exiting your wallet"), + message: TextState(""" + While usually an acceptable risk, you will possibly exposing your behavior and interest in this transaction by going online. \ + OH NOES! What will you do? + """), + primaryButton: .cancel( + TextState("NEVERMIND"), + action: .send(.dismissAlert) + ), + secondaryButton: .default( + TextState("SEE TX ONLINE"), + action: .send(.openBlockExplorer(blockExplorerURL)) + ) + ) + return .none + + case .openBlockExplorer(let blockExplorerURL): + state.alert = nil + if let url = blockExplorerURL { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + return .none } } } diff --git a/secant/Features/WalletEventsFlow/WalletEventsFlowView.swift b/secant/Features/WalletEventsFlow/WalletEventsFlowView.swift index 6568910..7630236 100644 --- a/secant/Features/WalletEventsFlow/WalletEventsFlowView.swift +++ b/secant/Features/WalletEventsFlow/WalletEventsFlowView.swift @@ -31,7 +31,7 @@ struct WalletEventsFlowView: View { ) .onDisappear(perform: { viewStore.send(.onDisappear) }) .navigationLinkEmpty(isActive: viewStore.bindingForSelectedWalletEvent(viewStore.selectedWalletEvent)) { - viewStore.selectedWalletEvent?.detailView(viewStore) + viewStore.selectedWalletEvent?.detailView(store) } } } diff --git a/secant/Models/TransactionState.swift b/secant/Models/TransactionState.swift index eeb3d7d..b146cd8 100644 --- a/secant/Models/TransactionState.swift +++ b/secant/Models/TransactionState.swift @@ -34,7 +34,7 @@ struct TransactionState: Equatable, Identifiable { var date: Date { Date(timeIntervalSince1970: timestamp) } var totalAmount: Zatoshi { Zatoshi(zecAmount.amount + fee.amount) } var viewOnlineURL: URL? { - URL(string: "https://blockchair.com/zcash/transaction/\(id)") + URL(string: "https://zcashblockexplorer.com/transactions/\(id)") } func confirmationsWith(_ latestMinedHeight: BlockHeight?) -> BlockHeight { diff --git a/secant/Models/WalletEvent.swift b/secant/Models/WalletEvent.swift index 0c3a018..70d21f4 100644 --- a/secant/Models/WalletEvent.swift +++ b/secant/Models/WalletEvent.swift @@ -52,13 +52,13 @@ extension WalletEvent { // MARK: - Details extension WalletEvent { - @ViewBuilder func detailView(_ viewStore: WalletEventsFlowViewStore) -> some View { + @ViewBuilder func detailView(_ store: WalletEventsFlowStore) -> some View { switch state { case .send(let transaction), .pending(let transaction), .received(let transaction), .failed(let transaction): - TransactionDetailView(transaction: transaction, viewStore: viewStore) + TransactionDetailView(transaction: transaction, store: store) case .shielded(let zatoshi): // TODO: implement design once shielding is supported, issue 390 // https://github.com/zcash/secant-ios-wallet/issues/390 diff --git a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift index d5b1166..58b7cc8 100644 --- a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift +++ b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift @@ -116,7 +116,7 @@ class WalletEventsSnapshotTests: XCTestCase { addAttachments( name: "\(#function)_WalletEventDetail", - TransactionDetailView(transaction: transaction, viewStore: ViewStore(walletEventsStore)) + TransactionDetailView(transaction: transaction, store: walletEventsStore) ) } @@ -173,7 +173,7 @@ class WalletEventsSnapshotTests: XCTestCase { addAttachments( name: "\(#function)_WalletEventDetail", - TransactionDetailView(transaction: transaction, viewStore: ViewStore(walletEventsStore)) + TransactionDetailView(transaction: transaction, store: walletEventsStore) ) } @@ -235,7 +235,7 @@ class WalletEventsSnapshotTests: XCTestCase { addAttachments( name: "\(#function)_WalletEventDetail", - TransactionDetailView(transaction: transaction, viewStore: ViewStore(walletEventsStore)) + TransactionDetailView(transaction: transaction, store: walletEventsStore) ) } @@ -293,7 +293,7 @@ class WalletEventsSnapshotTests: XCTestCase { addAttachments( name: "\(#function)_WalletEventDetail", - TransactionDetailView(transaction: transaction, viewStore: ViewStore(walletEventsStore)) + TransactionDetailView(transaction: transaction, store: walletEventsStore) ) } }