[#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.
This commit is contained in:
Michal Fousek 2022-09-22 21:20:46 +02:00 committed by GitHub
parent 8d45f3ba6e
commit cc7b767a5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 84 deletions

View File

@ -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)
}

View File

@ -17,6 +17,7 @@ struct WalletEventsFlowState: Equatable {
var route: Route?
@BindableState var alert: AlertState<WalletEventsFlowAction>?
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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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)
)
}
}