@@ -43,8 +43,8 @@ enum _PaymentLinksLocalPage {
4343///
4444/// The presentation remains local to this screen, while all secret creation,
4545/// recovery persistence, sync, and transaction work stays behind
46- /// [PaymentLinkOperations] . Artwork and message are not protocol fields in the
47- /// current v1 link, so received links intentionally render a generic card .
46+ /// [PaymentLinkOperations] . Artwork is an optional, backward-compatible v1
47+ /// field. The message remains local to the sender in the current protocol .
4848class PaymentLinksDesktopScreen extends ConsumerStatefulWidget {
4949 const PaymentLinksDesktopScreen ({super .key});
5050
@@ -91,6 +91,7 @@ class _PaymentLinksDesktopScreenState
9191 PaymentLinkRedeemVisualState .paste;
9292 PaymentLinkCardsTab _activeCardsTab = PaymentLinkCardsTab .created;
9393 List <PaymentLinkRecoveryRecord > _recoveries = const [];
94+ List <_ReceivedPaymentLinkRecord > _receivedCards = const [];
9495 VizorPaymentLink ? _readyLink;
9596 VizorPaymentLink ? _receivedLink;
9697 PaymentLinkRecoveryRecord ? _reclaimCandidate;
@@ -169,6 +170,7 @@ class _PaymentLinksDesktopScreenState
169170 if (link == null || ! mounted) return ;
170171 setState (() {
171172 _receivedLink = link;
173+ _rememberReceivedLink (link);
172174 _redeemState = PaymentLinkRedeemVisualState .paste;
173175 _page = _PaymentLinksLocalPage .received;
174176 _showHelp = false ;
@@ -191,6 +193,45 @@ class _PaymentLinksDesktopScreenState
191193 }
192194 }
193195
196+ void _rememberReceivedLink (VizorPaymentLink link) {
197+ final existingIndex = _receivedCards.indexWhere (
198+ (record) => record.address == link.address,
199+ );
200+ final existing = existingIndex < 0 ? null : _receivedCards[existingIndex];
201+ final status = existing? .status ?? _ReceivedPaymentLinkStatus .readyToClaim;
202+ final updated = _ReceivedPaymentLinkRecord .fromLink (
203+ link,
204+ status: status,
205+ retainClaimLink: status != _ReceivedPaymentLinkStatus .received,
206+ );
207+ final records = List <_ReceivedPaymentLinkRecord >.of (_receivedCards);
208+ if (existingIndex >= 0 ) records.removeAt (existingIndex);
209+ _receivedCards = [updated, ...records];
210+ }
211+
212+ void _setReceivedCardStatus (
213+ String address,
214+ _ReceivedPaymentLinkStatus status, {
215+ bool clearClaimLink = false ,
216+ }) {
217+ _receivedCards = [
218+ for (final record in _receivedCards)
219+ if (record.address == address)
220+ record.withStatus (status, clearClaimLink: clearClaimLink)
221+ else
222+ record,
223+ ];
224+ }
225+
226+ void _openReceivedCard (_ReceivedPaymentLinkRecord record) {
227+ final link = record.claimLink;
228+ if (link == null || _operationInProgress) return ;
229+ setState (() {
230+ _receivedLink = link;
231+ _page = _PaymentLinksLocalPage .received;
232+ });
233+ }
234+
194235 bool get _hasPositiveAmount {
195236 final amount = parseZecAmount (_amountController.text);
196237 return amount != null && amount > BigInt .zero;
@@ -278,6 +319,7 @@ class _PaymentLinksDesktopScreenState
278319 _keystoneFundingRequest = _PaymentLinkKeystoneFundingRequest (
279320 amountZatoshi: amount,
280321 sourceAccountUuid: sourceAccountUuid,
322+ artworkId: _selectedArtwork.protocolId,
281323 );
282324 });
283325 return ;
@@ -290,6 +332,7 @@ class _PaymentLinksDesktopScreenState
290332 .createFundedLink (
291333 amountZatoshi: amount,
292334 sourceAccountUuid: sourceAccountUuid,
335+ artworkId: _selectedArtwork.protocolId,
293336 );
294337 await _loadRecoveries (showError: false );
295338 if (! mounted) return ;
@@ -386,6 +429,7 @@ class _PaymentLinksDesktopScreenState
386429 if (link == null || ! mounted) return ;
387430 setState (() {
388431 _receivedLink = link;
432+ _rememberReceivedLink (link);
389433 _redeemState = PaymentLinkRedeemVisualState .paste;
390434 _page = _PaymentLinksLocalPage .received;
391435 });
@@ -409,17 +453,36 @@ class _PaymentLinksDesktopScreenState
409453 Future <void > _claimReceivedLink () async {
410454 final link = _receivedLink;
411455 if (link == null || _operationInProgress) return ;
412- setState (() => _operationInProgress = true );
456+ setState (() {
457+ _operationInProgress = true ;
458+ _receivedLink = null ;
459+ _setReceivedCardStatus (
460+ link.address,
461+ _ReceivedPaymentLinkStatus .receiving,
462+ );
463+ _activeCardsTab = PaymentLinkCardsTab .received;
464+ _page = _PaymentLinksLocalPage .home;
465+ _showHelp = false ;
466+ });
413467 try {
414468 await ref.read (paymentLinkOperationsProvider).claimLink (link);
415469 if (! mounted) return ;
416470 setState (() {
417- _receivedLink = null ;
418- _page = _PaymentLinksLocalPage .home;
471+ _setReceivedCardStatus (
472+ link.address,
473+ _ReceivedPaymentLinkStatus .received,
474+ clearClaimLink: true ,
475+ );
419476 });
420477 showAppToast (context, 'Gift claimed' );
421478 } catch (_) {
422479 if (mounted) {
480+ setState (() {
481+ _setReceivedCardStatus (
482+ link.address,
483+ _ReceivedPaymentLinkStatus .readyToClaim,
484+ );
485+ });
423486 _showError (
424487 'Gift Card claim failed. It may still be waiting for confirmation '
425488 'or may already be spent.' ,
@@ -489,6 +552,7 @@ class _PaymentLinksDesktopScreenState
489552 child: PaymentLinkKeystoneSigningOverlay (
490553 amountZatoshi: keystoneRequest.amountZatoshi,
491554 sourceAccountUuid: keystoneRequest.sourceAccountUuid,
555+ artworkId: keystoneRequest.artworkId,
492556 onCancel: _cancelKeystoneFunding,
493557 onFundingBroadcast: _completeKeystoneFunding,
494558 ),
@@ -539,7 +603,9 @@ class _PaymentLinksDesktopScreenState
539603 }
540604
541605 Widget _buildHome () {
542- if (_recoveries.isNotEmpty) return _buildCardsList ();
606+ if (_recoveries.isNotEmpty || _receivedCards.isNotEmpty) {
607+ return _buildCardsList ();
608+ }
543609 return PaymentLinksHomeDesktopView (
544610 illustration: Image .asset (
545611 'assets/illustrations/payment_links/payment_link_empty_card.png' ,
@@ -564,6 +630,8 @@ class _PaymentLinksDesktopScreenState
564630 )
565631 .map (_buildRecoveryRow)
566632 .toList ()
633+ : _receivedCards.isNotEmpty
634+ ? _receivedCards.map (_buildReceivedRow).toList ()
567635 : < Widget > [
568636 Padding (
569637 padding: const EdgeInsets .symmetric (vertical: AppSpacing .sm),
@@ -615,7 +683,7 @@ class _PaymentLinksDesktopScreenState
615683 return PaymentLinkCardListRow (
616684 key: ValueKey ('payment_link_recovery_${record .link .address }' ),
617685 thumbnail: Image .asset (
618- PaymentLinkCardArtwork .gift .assetPath,
686+ PaymentLinkCardArtwork .fromProtocolId (record.link.artworkId) .assetPath,
619687 fit: BoxFit .cover,
620688 excludeFromSemantics: true ,
621689 ),
@@ -631,6 +699,30 @@ class _PaymentLinksDesktopScreenState
631699 );
632700 }
633701
702+ Widget _buildReceivedRow (_ReceivedPaymentLinkRecord record) {
703+ final statusText = switch (record.status) {
704+ _ReceivedPaymentLinkStatus .readyToClaim => 'Claim' ,
705+ _ReceivedPaymentLinkStatus .receiving => 'Receiving' ,
706+ _ReceivedPaymentLinkStatus .received => 'Received' ,
707+ };
708+ final canClaim =
709+ record.status == _ReceivedPaymentLinkStatus .readyToClaim &&
710+ record.claimLink != null &&
711+ ! _operationInProgress;
712+ return PaymentLinkCardListRow (
713+ key: ValueKey ('payment_link_received_${record .address }' ),
714+ thumbnail: Image .asset (
715+ PaymentLinkCardArtwork .fromProtocolId (record.artworkId).assetPath,
716+ fit: BoxFit .cover,
717+ excludeFromSemantics: true ,
718+ ),
719+ amountText: '${formatZecAmount (record .amountZatoshi )} ZEC' ,
720+ dateText: _formatCardDate (record.createdAt),
721+ statusText: statusText,
722+ onAction: canClaim ? () => _openReceivedCard (record) : null ,
723+ );
724+ }
725+
634726 Widget _buildAmount () {
635727 final maxAmountText = _maxAmountText;
636728 return PaymentLinkAmountDesktopView (
@@ -730,7 +822,7 @@ class _PaymentLinksDesktopScreenState
730822 if (link == null ) return _buildHome ();
731823 return PaymentLinkReceivedDesktopView (
732824 card: PaymentLinkGiftCard (
733- artwork: PaymentLinkCardArtwork .gift ,
825+ artwork: PaymentLinkCardArtwork .fromProtocolId (link.artworkId) ,
734826 amountText: formatZecAmount (link.amountZatoshi),
735827 showCaret: false ,
736828 ),
@@ -804,8 +896,59 @@ class _PaymentLinkKeystoneFundingRequest {
804896 const _PaymentLinkKeystoneFundingRequest ({
805897 required this .amountZatoshi,
806898 required this .sourceAccountUuid,
899+ required this .artworkId,
807900 });
808901
809902 final BigInt amountZatoshi;
810903 final String sourceAccountUuid;
904+ final String artworkId;
905+ }
906+
907+ enum _ReceivedPaymentLinkStatus { readyToClaim, receiving, received }
908+
909+ class _ReceivedPaymentLinkRecord {
910+ const _ReceivedPaymentLinkRecord ({
911+ required this .address,
912+ required this .amountZatoshi,
913+ required this .createdAt,
914+ required this .artworkId,
915+ required this .status,
916+ required this .claimLink,
917+ });
918+
919+ factory _ReceivedPaymentLinkRecord .fromLink (
920+ VizorPaymentLink link, {
921+ _ReceivedPaymentLinkStatus status = _ReceivedPaymentLinkStatus .readyToClaim,
922+ bool retainClaimLink = true ,
923+ }) {
924+ return _ReceivedPaymentLinkRecord (
925+ address: link.address,
926+ amountZatoshi: link.amountZatoshi,
927+ createdAt: link.createdAt,
928+ artworkId: link.artworkId,
929+ status: status,
930+ claimLink: retainClaimLink ? link : null ,
931+ );
932+ }
933+
934+ final String address;
935+ final BigInt amountZatoshi;
936+ final DateTime createdAt;
937+ final String ? artworkId;
938+ final _ReceivedPaymentLinkStatus status;
939+ final VizorPaymentLink ? claimLink;
940+
941+ _ReceivedPaymentLinkRecord withStatus (
942+ _ReceivedPaymentLinkStatus nextStatus, {
943+ bool clearClaimLink = false ,
944+ }) {
945+ return _ReceivedPaymentLinkRecord (
946+ address: address,
947+ amountZatoshi: amountZatoshi,
948+ createdAt: createdAt,
949+ artworkId: artworkId,
950+ status: nextStatus,
951+ claimLink: clearClaimLink ? null : claimLink,
952+ );
953+ }
811954}
0 commit comments