Skip to content

Commit ec1e72a

Browse files
committed
feat: show business information on OpenCryptoPay confirmation
1 parent 8163b54 commit ec1e72a

4 files changed

Lines changed: 184 additions & 31 deletions

File tree

‎lib/pages/open_crypto_pay/open_crypto_pay_send_handler.dart‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,31 @@ class OpenCryptoPaySendHandler {
7979

8080
bool get isQuoteExpired => _session?.isQuoteExpired ?? false;
8181

82+
List<String> get businessLines {
83+
final details = _session?.details;
84+
if (details == null) return const [];
85+
final recipient = details.recipient;
86+
final displayName = details.displayName;
87+
String line(List<String?> parts) =>
88+
parts.nonNulls.where((part) => part.isNotEmpty).join(" ");
89+
final lines = <String?>[
90+
displayName,
91+
if (recipient != null) ...[
92+
if (recipient.name != displayName) recipient.name,
93+
line([recipient.street, recipient.houseNumber]),
94+
line([recipient.zip, recipient.city]),
95+
recipient.country,
96+
recipient.phone,
97+
recipient.mail,
98+
recipient.website,
99+
if (recipient.registrationNumber case final number?
100+
when number.isNotEmpty)
101+
"Registration number: $number",
102+
],
103+
];
104+
return lines.nonNulls.where((line) => line.isNotEmpty).toList();
105+
}
106+
82107
bool isActivePaymentFor(String? recipientAddress) =>
83108
_session?.isActivePaymentFor(recipientAddress) ?? false;
84109

‎lib/pages/send_view/confirm_transaction_view.dart‎

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -313,24 +313,31 @@ class _ConfirmTransactionViewState
313313
}
314314
}
315315

316+
/// Firo private (spark) sends carry the recipient in sparkRecipients.
317+
String? get _recipientAddress =>
318+
widget.txData.recipients?.firstOrNull?.address ??
319+
widget.txData.sparkRecipients?.firstOrNull?.address;
320+
321+
OpenCryptoPaySendHandler? get _activeOcp {
322+
final ocp = widget.openCryptoPayHandler;
323+
return ocp != null && ocp.isActivePaymentFor(_recipientAddress)
324+
? ocp
325+
: null;
326+
}
327+
316328
Future<void> _attemptSend(BuildContext context) async {
317329
final wallet = ref.read(pWallets).getWallet(walletId);
318330
final coin = wallet.info.coin;
319331

320-
final ocp = widget.openCryptoPayHandler;
321-
// Firo private (spark) sends carry the recipient in sparkRecipients.
322-
final recipientAddress =
323-
widget.txData.recipients?.firstOrNull?.address ??
324-
widget.txData.sparkRecipients?.firstOrNull?.address;
325-
final isOcp = ocp != null && ocp.isActivePaymentFor(recipientAddress);
332+
final ocp = _activeOcp;
326333

327-
if (isOcp && ocp.isQuoteExpired) {
334+
if (ocp != null && ocp.isQuoteExpired) {
328335
// Abort before anything is broadcast or submitted (both proof types).
329336
await ocp.showQuoteExpiredError(context, paymentNotSent: true);
330337
return;
331338
}
332339

333-
if (isOcp && !ocp.requiresBroadcast) {
340+
if (ocp != null && !ocp.requiresBroadcast) {
334341
// Signed-hex proof type: the provider broadcasts the transaction, so
335342
// do NOT broadcast here. The txid proof type falls through to the
336343
// normal confirmSend flow below.
@@ -467,7 +474,7 @@ class _ConfirmTransactionViewState
467474

468475
widget.onSuccess.call();
469476

470-
if (isOcp && txids.isNotEmpty) {
477+
if (ocp != null && txids.isNotEmpty) {
471478
// Broadcast (txid) proof type: submit the txid to the
472479
// OpenCryptoPay provider.
473480
unawaited(ocp.submitProof(context, txids.first));
@@ -746,6 +753,7 @@ class _ConfirmTransactionViewState
746753

747754
final String unit;
748755
final wallet = ref.watch(pWallets).getWallet(walletId);
756+
final businessLines = _activeOcp?.businessLines ?? const <String>[];
749757
if (widget.isTokenTx) {
750758
if (wallet is SolanaWallet) {
751759
// For Solana tokens, use the Solana token wallet provider or TxData as fallback.
@@ -894,21 +902,31 @@ class _ConfirmTransactionViewState
894902
Text(
895903
widget.isPaynymTransaction
896904
? widget.txData.paynymAccountLite!.nymName
897-
: widget
898-
.txData
899-
.recipients
900-
?.firstOrNull
901-
?.address ??
902-
widget
903-
.txData
904-
.sparkRecipients!
905-
.first
906-
.address,
905+
: _recipientAddress!,
907906
style: STextStyles.itemSubtitle12(context),
908907
),
909908
],
910909
),
911910
),
911+
if (businessLines.isNotEmpty) const SizedBox(height: 12),
912+
if (businessLines.isNotEmpty)
913+
RoundedWhiteContainer(
914+
child: Column(
915+
crossAxisAlignment: CrossAxisAlignment.stretch,
916+
children: [
917+
Text(
918+
"Business",
919+
style: STextStyles.smallMed12(context),
920+
),
921+
const SizedBox(height: 4),
922+
for (final line in businessLines)
923+
SelectableText(
924+
line,
925+
style: STextStyles.itemSubtitle12(context),
926+
),
927+
],
928+
),
929+
),
912930
const SizedBox(height: 12),
913931
RoundedWhiteContainer(
914932
child: Row(
@@ -1244,16 +1262,7 @@ class _ConfirmTransactionViewState
12441262
// TODO: [prio=med] spark transaction specifics - better handling
12451263
widget.isPaynymTransaction
12461264
? widget.txData.paynymAccountLite!.nymName
1247-
: widget
1248-
.txData
1249-
.recipients
1250-
?.firstOrNull
1251-
?.address ??
1252-
widget
1253-
.txData
1254-
.sparkRecipients!
1255-
.first
1256-
.address,
1265+
: _recipientAddress!,
12571266
style:
12581267
STextStyles.desktopTextExtraExtraSmall(
12591268
context,
@@ -1266,6 +1275,42 @@ class _ConfirmTransactionViewState
12661275
],
12671276
),
12681277
),
1278+
if (businessLines.isNotEmpty)
1279+
Container(
1280+
height: 1,
1281+
color: Theme.of(
1282+
context,
1283+
).extension<StackColors>()!.background,
1284+
),
1285+
if (businessLines.isNotEmpty)
1286+
Padding(
1287+
padding: const EdgeInsets.all(12),
1288+
child: Column(
1289+
mainAxisSize: MainAxisSize.min,
1290+
crossAxisAlignment: CrossAxisAlignment.start,
1291+
children: [
1292+
Text(
1293+
"Business",
1294+
style: STextStyles.desktopTextExtraExtraSmall(
1295+
context,
1296+
),
1297+
),
1298+
const SizedBox(height: 2),
1299+
for (final line in businessLines)
1300+
SelectableText(
1301+
line,
1302+
style:
1303+
STextStyles.desktopTextExtraExtraSmall(
1304+
context,
1305+
).copyWith(
1306+
color: Theme.of(
1307+
context,
1308+
).extension<StackColors>()!.textDark,
1309+
),
1310+
),
1311+
],
1312+
),
1313+
),
12691314
if (widget.isPaynymTransaction)
12701315
Container(
12711316
height: 1,

‎scripts/app_config/templates/pubspec.template.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ dependencies:
274274
opencryptopay:
275275
git:
276276
url: https://github.com/Cyrix126/opencryptopay
277-
ref: 7295eeab494a2f79a5b390fa90260f93bdacfa34
277+
ref: 46cadf907ccdd21149e37a86ab04175298ff542b
278278

279279
dev_dependencies:
280280
flutter_test:

‎test/pages/open_crypto_pay/open_crypto_pay_send_handler_test.dart‎

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,30 @@ const _hashHint =
3737
'Use this data to create a transaction, sign and broadcast it. Then '
3838
'send the transaction id back via the endpoint.';
3939

40-
Map<String, dynamic> _paymentInfoJson({required String quoteExpiration}) => {
40+
const _recipientJson = {
41+
"name": "Test Shop AG",
42+
"address": {
43+
"street": "Bahnhofstrasse",
44+
"houseNumber": "7",
45+
"city": "Zug",
46+
"zip": "6300",
47+
"country": "CH",
48+
},
49+
"phone": "+41792684224",
50+
"mail": "mail@example.org",
51+
"website": "https://example.org/",
52+
"registrationNumber": "CHE-429.856.521",
53+
};
54+
55+
Map<String, dynamic> _paymentInfoJson({
56+
required String quoteExpiration,
57+
Map<String, dynamic>? recipient,
58+
}) => {
4159
"id": "pl_test",
4260
"tag": "payRequest",
4361
"callback": _callbackUrl,
4462
"displayName": "Test Shop",
63+
if (recipient != null) "recipient": recipient,
4564
"quote": {
4665
"id": "plq_test",
4766
"expiration": quoteExpiration,
@@ -295,6 +314,70 @@ void main() {
295314
expect(setup.handler.isActivePaymentFor("bc1qsomeotheraddress"), isFalse);
296315
expect(setup.handler.requiresBroadcast, isTrue);
297316
expect(setup.handler.isQuoteExpired, isFalse);
317+
expect(setup.handler.businessLines, ["Test Shop"]);
318+
});
319+
320+
testWidgets("lists the business information of the pending payment", (
321+
tester,
322+
) async {
323+
final harness = await _pumpHarness(tester);
324+
final setup = _makeHandler(
325+
harness: harness,
326+
coin: Bitcoin(CryptoCurrencyNetwork.main),
327+
client: _mockOcpServer(
328+
paymentInfo: _paymentInfoJson(
329+
quoteExpiration: _futureExpiration(),
330+
recipient: _recipientJson,
331+
),
332+
txDetails: _btcDetailsJson(hint: _hashHint),
333+
),
334+
);
335+
336+
await _handle(tester, harness, setup.handler);
337+
338+
expect(setup.handler.businessLines, [
339+
"Test Shop",
340+
"Test Shop AG",
341+
"Bahnhofstrasse 7",
342+
"6300 Zug",
343+
"CH",
344+
"+41792684224",
345+
"mail@example.org",
346+
"https://example.org/",
347+
"Registration number: CHE-429.856.521",
348+
]);
349+
});
350+
351+
testWidgets("skips empty and missing business fields", (tester) async {
352+
final harness = await _pumpHarness(tester);
353+
final setup = _makeHandler(
354+
harness: harness,
355+
coin: Bitcoin(CryptoCurrencyNetwork.main),
356+
client: _mockOcpServer(
357+
paymentInfo: _paymentInfoJson(
358+
quoteExpiration: _futureExpiration(),
359+
recipient: {
360+
"name": "Test Shop",
361+
"address": {
362+
"street": "Bahnhofstrasse",
363+
"houseNumber": "",
364+
"city": "Zug",
365+
},
366+
"phone": "",
367+
"registrationNumber": "",
368+
},
369+
),
370+
txDetails: _btcDetailsJson(hint: _hashHint),
371+
),
372+
);
373+
374+
await _handle(tester, harness, setup.handler);
375+
376+
expect(setup.handler.businessLines, [
377+
"Test Shop",
378+
"Bahnhofstrasse",
379+
"Zug",
380+
]);
298381
});
299382

300383
testWidgets("signed-hex hint results in requiresBroadcast false", (

0 commit comments

Comments
 (0)