@@ -7,6 +7,7 @@ import 'package:tuple/tuple.dart';
77
88import '../../models/send_view_auto_fill_data.dart' ;
99import '../../notifications/show_flush_bar.dart' ;
10+ import '../../services/open_crypto_pay/method_support.dart' ;
1011import '../../services/open_crypto_pay/models.dart' ;
1112import '../../services/open_crypto_pay/open_crypto_pay_api.dart' ;
1213import '../../themes/stack_colors.dart' ;
@@ -21,6 +22,8 @@ import '../../widgets/loading_indicator.dart';
2122import '../../widgets/rounded_white_container.dart' ;
2223import '../send_view/send_view.dart' ;
2324
25+ enum OpenCryptoPayConfirmResult { quoteExpired }
26+
2427/// Fetches the transaction details for the selected method/asset, shows a
2528/// summary, then forwards to the standard [SendView] prefilled with the
2629/// payment address and amount.
@@ -51,6 +54,14 @@ class _OpenCryptoPayConfirmViewState
5154 bool _isLoading = true ;
5255 String ? _errorMessage;
5356
57+ DateTime ? get _expiresAt =>
58+ _txDetails? .expiryDate ?? widget.paymentDetails.quote? .expiration;
59+
60+ bool get _isExpired {
61+ final expiresAt = _expiresAt;
62+ return expiresAt != null && expiresAt.isBefore (DateTime .now ());
63+ }
64+
5465 @override
5566 void initState () {
5667 super .initState ();
@@ -64,37 +75,65 @@ class _OpenCryptoPayConfirmViewState
6475 });
6576
6677 try {
78+ final quote = widget.paymentDetails.quote;
79+ if (quote == null ) {
80+ throw Exception ("No quote provided by the payment provider" );
81+ }
6782 _txDetails = await OpenCryptoPayApi .instance.getTransactionDetails (
6883 callbackUrl: widget.paymentDetails.callback,
69- quoteId: widget.paymentDetails. quote! .id,
84+ quoteId: quote.id,
7085 method: widget.selectedMethod.method,
7186 asset: widget.selectedAsset.asset,
7287 );
7388 } catch (e, s) {
74- Logging .instance.e ("OpenCryptoPay tx fetch failed" , error: e, stackTrace: s);
89+ Logging .instance.e (
90+ "OpenCryptoPay tx fetch failed" ,
91+ error: e,
92+ stackTrace: s,
93+ );
7594 _errorMessage = 'Failed to fetch transaction details: $e ' ;
7695 } finally {
7796 if (mounted) setState (() => _isLoading = false );
7897 }
7998 }
8099
81- /// Parses address and amount from the transaction URI. Strips the EVM
82- /// `@chainId` suffix that [AddressUtils] leaves attached.
83- ({String ? address, Decimal ? amount}) _parseTransactionUri (String uri) {
100+ /// Parses address and amount from the transaction URI. For EVM URIs this
101+ /// also extracts the EIP-681 `@chainId` suffix that [AddressUtils] leaves
102+ /// attached to the address.
103+ ({String ? address, Decimal ? amount, int ? chainId, String ? scheme})
104+ _parseTransactionUri (String uri) {
105+ final parsedUri = Uri .tryParse (uri);
84106 final data = AddressUtils .parsePaymentUri (uri, logging: Logging .instance);
85- var address = data? .address ?? Uri .tryParse (uri)? .path;
107+ var address = data? .address ?? parsedUri? .path;
108+ int ? chainId;
86109 if (address != null ) {
87110 final at = address.indexOf ('@' );
88- if (at != - 1 ) address = address.substring (0 , at);
111+ if (at != - 1 ) {
112+ chainId = int .tryParse (address.substring (at + 1 ));
113+ address = address.substring (0 , at);
114+ }
89115 if (address.isEmpty) address = null ;
90116 }
91117 final amount = data? .amount != null
92118 ? Decimal .tryParse (data! .amount! )
93119 : Decimal .tryParse (widget.selectedAsset.amount);
94- return (address: address, amount: amount);
120+ return (
121+ address: address,
122+ amount: amount,
123+ chainId: chainId,
124+ scheme: data? .scheme ?? parsedUri? .scheme,
125+ );
95126 }
96127
97128 Future <void > _proceedToSend () async {
129+ if (_isExpired) {
130+ _warn ("Quote expired, refreshing..." );
131+ if (mounted) {
132+ Navigator .of (context).pop (OpenCryptoPayConfirmResult .quoteExpired);
133+ }
134+ return ;
135+ }
136+
98137 final uri = _txDetails? .uri;
99138 if (uri == null ) {
100139 _warn ("No transaction URI provided by the payment provider" );
@@ -106,8 +145,45 @@ class _OpenCryptoPayConfirmViewState
106145 _warn ("Could not parse payment address" );
107146 return ;
108147 }
148+ if (parsed.amount == null ) {
149+ _warn ("Could not parse payment amount" );
150+ return ;
151+ }
152+ if (parsed.scheme != null &&
153+ parsed.scheme! .isNotEmpty &&
154+ parsed.scheme != widget.coin.uriScheme) {
155+ _warn ("Payment URI does not match this wallet" );
156+ return ;
157+ }
158+ if (_txDetails? .blockchain != null &&
159+ _txDetails! .blockchain != widget.selectedMethod.method) {
160+ _warn ("Payment details do not match the selected method" );
161+ return ;
162+ }
163+ if (widget.selectedMethod.method == 'Ethereum' &&
164+ parsed.chainId != null &&
165+ parsed.chainId != 1 ) {
166+ _warn ("Payment URI is for a different Ethereum network" );
167+ return ;
168+ }
169+
170+ final submissionFlow = OpenCryptoPayMethodSupport .submissionFlowFor (
171+ widget.selectedMethod.method,
172+ );
173+ if (submissionFlow == null ||
174+ submissionFlow == OpenCryptoPaySubmissionFlow .external ) {
175+ _warn ("This Open CryptoPay method is not supported yet" );
176+ return ;
177+ }
178+
179+ final expiresAt = _expiresAt;
180+ if (expiresAt == null ) {
181+ _warn ("No quote expiration provided by the payment provider" );
182+ return ;
183+ }
109184
110- final recipient = widget.paymentDetails.recipient? .name ??
185+ final recipient =
186+ widget.paymentDetails.recipient? .name ??
111187 widget.paymentDetails.displayName ??
112188 "OpenCryptoPay" ;
113189
@@ -127,6 +203,11 @@ class _OpenCryptoPayConfirmViewState
127203 quoteId: widget.paymentDetails.quote! .id,
128204 method: widget.selectedMethod.method,
129205 asset: widget.selectedAsset.asset,
206+ expiresAt: expiresAt,
207+ submissionFlow: submissionFlow,
208+ minFee: widget.selectedMethod.minFee,
209+ recipientAddress: parsed.address! ,
210+ amount: parsed.amount! ,
130211 ),
131212 ),
132213 ),
@@ -147,11 +228,11 @@ class _OpenCryptoPayConfirmViewState
147228 Widget build (BuildContext context) {
148229 return Background (
149230 child: Scaffold (
150- backgroundColor:
151- Theme .of (context).extension < StackColors > ()! .background,
231+ backgroundColor: Theme .of (context).extension < StackColors > ()! .background,
152232 appBar: AppBar (
153- backgroundColor:
154- Theme .of (context).extension < StackColors > ()! .backgroundAppBar,
233+ backgroundColor: Theme .of (
234+ context,
235+ ).extension < StackColors > ()! .backgroundAppBar,
155236 leading: const AppBarBackButton (),
156237 title: Text (
157238 "Confirm Payment" ,
0 commit comments