From b4893a3ab8d13f1272179e947944e1440d1484f7 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Mon, 6 Apr 2026 18:45:15 +0200 Subject: [PATCH 1/4] refacto: merging qrcode data and requests data --- lib/paiement/class/qr_code_data.dart | 67 ------------------- lib/paiement/class/request_validation.dart | 47 ------------- lib/paiement/class/scan_info.dart | 38 +++++++++++ ...re_data.dart => secured_content_data.dart} | 14 ++-- lib/paiement/class/signed_content.dart | 49 ++++++++++++++ lib/paiement/providers/barcode_provider.dart | 10 +-- .../providers/payment_requests_provider.dart | 4 +- lib/paiement/providers/scan_provider.dart | 13 ++-- .../repositories/requests_repository.dart | 4 +- .../repositories/stores_repository.dart | 19 +++--- lib/paiement/tools/functions.dart | 34 +++------- lib/paiement/tools/key_service.dart | 18 +++++ .../ui/pages/main_page/main_page.dart | 35 +++------- lib/paiement/ui/pages/scan_page/scanner.dart | 16 ++++- 14 files changed, 167 insertions(+), 201 deletions(-) delete mode 100644 lib/paiement/class/qr_code_data.dart delete mode 100644 lib/paiement/class/request_validation.dart create mode 100644 lib/paiement/class/scan_info.dart rename lib/paiement/class/{qr_code_signature_data.dart => secured_content_data.dart} (75%) create mode 100644 lib/paiement/class/signed_content.dart diff --git a/lib/paiement/class/qr_code_data.dart b/lib/paiement/class/qr_code_data.dart deleted file mode 100644 index 7032642d25..0000000000 --- a/lib/paiement/class/qr_code_data.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:titan/tools/functions.dart'; - -class QrCodeData { - final String id; - final int tot; - final DateTime iat; - final String key; - final bool store; - final String signature; - - QrCodeData({ - required this.id, - required this.tot, - required this.iat, - required this.key, - required this.store, - required this.signature, - }); - - QrCodeData.fromJson(Map json) - : id = json['id'], - tot = json['tot'], - iat = processDateFromAPI(json['iat']), - key = json['key'], - store = json['store'], - signature = json['signature']; - - Map toJson() => { - 'id': id, - 'tot': tot, - 'iat': processDateToAPI(iat), - 'key': key, - 'store': store, - 'signature': signature, - }; - - @override - String toString() { - return 'QrCodeData {id: $id, tot: $tot, iat: $iat, key: $key, store: $store, signature: $signature}'; - } - - QrCodeData.empty() - : id = '', - tot = 0, - iat = DateTime.now(), - key = '', - store = false, - signature = ''; - - QrCodeData copyWith({ - String? id, - int? tot, - DateTime? iat, - String? key, - bool? store, - String? signature, - }) { - return QrCodeData( - id: id ?? this.id, - tot: tot ?? this.tot, - iat: iat ?? this.iat, - key: key ?? this.key, - store: store ?? this.store, - signature: signature ?? this.signature, - ); - } -} diff --git a/lib/paiement/class/request_validation.dart b/lib/paiement/class/request_validation.dart deleted file mode 100644 index 646e7ac0cf..0000000000 --- a/lib/paiement/class/request_validation.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:titan/tools/functions.dart'; - -class RequestValidationData { - final String requestId; - final String key; - final DateTime iat; - final int tot; - - RequestValidationData({ - required this.requestId, - required this.key, - required this.iat, - required this.tot, - }); - - Map toJson() => { - 'request_id': requestId, - 'key': key, - 'iat': processDateToAPI(iat), - 'tot': tot, - }; - - @override - String toString() { - return 'RequestValidationData {requestId: $requestId, key: $key, iat: $iat, tot: $tot}'; - } -} - -class RequestValidation extends RequestValidationData { - final String signature; - - RequestValidation({ - required super.requestId, - required super.key, - required super.iat, - required super.tot, - required this.signature, - }); - - @override - Map toJson() => {...super.toJson(), 'signature': signature}; - - @override - String toString() { - return 'RequestValidation {requestId: $requestId, key: $key, iat: $iat, tot: $tot, signature: $signature}'; - } -} diff --git a/lib/paiement/class/scan_info.dart b/lib/paiement/class/scan_info.dart new file mode 100644 index 0000000000..0c1ebfa8d9 --- /dev/null +++ b/lib/paiement/class/scan_info.dart @@ -0,0 +1,38 @@ +import 'package:titan/paiement/class/signed_content.dart'; + +class ScanInfo extends SignedContent { + final bool bypassMembership; + + ScanInfo({ + required super.id, + required super.tot, + required super.iat, + required super.key, + required super.store, + required super.signature, + this.bypassMembership = false, + }); + + ScanInfo.fromSignedContent( + SignedContent content, { + this.bypassMembership = false, + }) : super( + id: content.id, + tot: content.tot, + iat: content.iat, + key: content.key, + store: content.store, + signature: content.signature, + ); + + @override + Map toJson() => { + ...super.toJson(), + 'bypass_membership': bypassMembership, + }; + + @override + String toString() { + return 'ScanInfo {id: $id, tot: $tot, iat: $iat, key: $key, store: $store, signature: $signature, bypassMembership: $bypassMembership}'; + } +} diff --git a/lib/paiement/class/qr_code_signature_data.dart b/lib/paiement/class/secured_content_data.dart similarity index 75% rename from lib/paiement/class/qr_code_signature_data.dart rename to lib/paiement/class/secured_content_data.dart index ac184e0f65..c69eb121eb 100644 --- a/lib/paiement/class/qr_code_signature_data.dart +++ b/lib/paiement/class/secured_content_data.dart @@ -1,13 +1,13 @@ import 'package:titan/tools/functions.dart'; -class QrCodeSignatureData { +class SecuredContentData { final String id; final int tot; final DateTime iat; final String key; final bool store; - QrCodeSignatureData({ + SecuredContentData({ required this.id, required this.tot, required this.iat, @@ -15,7 +15,7 @@ class QrCodeSignatureData { required this.store, }); - QrCodeSignatureData.fromJson(Map json) + SecuredContentData.fromJson(Map json) : id = json['id'], tot = json['tot'], iat = processDateFromAPI(json['iat']), @@ -32,24 +32,24 @@ class QrCodeSignatureData { @override String toString() { - return 'QrCodeSignatureData {id: $id, tot: $tot, iat: $iat, key: $key, store: $store}'; + return 'SecuredContentData {id: $id, tot: $tot, iat: $iat, key: $key, store: $store}'; } - QrCodeSignatureData.empty() + SecuredContentData.empty() : id = '', tot = 0, iat = DateTime.now(), key = '', store = false; - QrCodeSignatureData copyWith({ + SecuredContentData copyWith({ String? id, int? tot, DateTime? iat, String? key, bool? store, }) { - return QrCodeSignatureData( + return SecuredContentData( id: id ?? this.id, tot: tot ?? this.tot, iat: iat ?? this.iat, diff --git a/lib/paiement/class/signed_content.dart b/lib/paiement/class/signed_content.dart new file mode 100644 index 0000000000..ee744b9ec8 --- /dev/null +++ b/lib/paiement/class/signed_content.dart @@ -0,0 +1,49 @@ +import 'package:titan/paiement/class/secured_content_data.dart'; + +class SignedContent extends SecuredContentData { + final String signature; + + SignedContent({ + required super.id, + required super.tot, + required super.iat, + required super.key, + required super.store, + required this.signature, + }); + + SignedContent.fromJson(super.json) + : signature = json['signature'], + super.fromJson(); + + @override + Map toJson() => {...super.toJson(), 'signature': signature}; + + @override + String toString() { + return 'SignedContent {id: $id, tot: $tot, iat: $iat, key: $key, store: $store, signature: $signature}'; + } + + SignedContent.empty() + : signature = '', + super.empty(); + + @override + SignedContent copyWith({ + String? id, + int? tot, + DateTime? iat, + String? key, + bool? store, + String? signature, + }) { + return SignedContent( + id: id ?? this.id, + tot: tot ?? this.tot, + iat: iat ?? this.iat, + key: key ?? this.key, + store: store ?? this.store, + signature: signature ?? this.signature, + ); + } +} diff --git a/lib/paiement/providers/barcode_provider.dart b/lib/paiement/providers/barcode_provider.dart index 6b7589119f..e9b2cddd88 100644 --- a/lib/paiement/providers/barcode_provider.dart +++ b/lib/paiement/providers/barcode_provider.dart @@ -1,13 +1,13 @@ import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/paiement/class/qr_code_data.dart'; +import 'package:titan/paiement/class/signed_content.dart'; -class BarcodeNotifier extends StateNotifier { +class BarcodeNotifier extends StateNotifier { BarcodeNotifier() : super(null); - QrCodeData updateBarcode(String barcode) { - state = QrCodeData.fromJson(jsonDecode(barcode)); + SignedContent updateBarcode(String barcode) { + state = SignedContent.fromJson(jsonDecode(barcode)); return state!; } @@ -16,7 +16,7 @@ class BarcodeNotifier extends StateNotifier { } } -final barcodeProvider = StateNotifierProvider(( +final barcodeProvider = StateNotifierProvider(( ref, ) { return BarcodeNotifier(); diff --git a/lib/paiement/providers/payment_requests_provider.dart b/lib/paiement/providers/payment_requests_provider.dart index c214344187..df8423fab5 100644 --- a/lib/paiement/providers/payment_requests_provider.dart +++ b/lib/paiement/providers/payment_requests_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/paiement/class/payment_request.dart'; -import 'package:titan/paiement/class/request_validation.dart'; +import 'package:titan/paiement/class/signed_content.dart'; import 'package:titan/paiement/repositories/requests_repository.dart'; import 'package:titan/tools/providers/list_notifier.dart'; @@ -15,7 +15,7 @@ class PaymentRequestsNotifier extends ListNotifier { Future acceptRequest( PaymentRequest request, - RequestValidation validation, + SignedContent validation, ) async { return await update( (_) => requestsRepository.acceptRequest(request.id, validation), diff --git a/lib/paiement/providers/scan_provider.dart b/lib/paiement/providers/scan_provider.dart index da897202f8..ac53d0c2d5 100644 --- a/lib/paiement/providers/scan_provider.dart +++ b/lib/paiement/providers/scan_provider.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/paiement/class/qr_code_data.dart'; +import 'package:titan/paiement/class/scan_info.dart'; import 'package:titan/paiement/class/transaction.dart'; import 'package:titan/paiement/repositories/stores_repository.dart'; import 'package:titan/tools/providers/single_notifier.dart'; @@ -11,14 +11,13 @@ class ScanNotifier extends SingleNotifier { Future?> scan( String storeId, - QrCodeData data, { - bool? bypass, - }) async { - return await load(() => storesRepository.scan(storeId, data, bypass)); + ScanInfo data, + ) async { + return await load(() => storesRepository.scan(storeId, data)); } - Future canScan(String storeId, QrCodeData data, {bool? bypass}) async { - return storesRepository.canScan(storeId, data, bypass); + Future canScan(String storeId, ScanInfo data) async { + return storesRepository.canScan(storeId, data); } void reset() { diff --git a/lib/paiement/repositories/requests_repository.dart b/lib/paiement/repositories/requests_repository.dart index 812cbcade0..467401543a 100644 --- a/lib/paiement/repositories/requests_repository.dart +++ b/lib/paiement/repositories/requests_repository.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/paiement/class/payment_request.dart'; -import 'package:titan/paiement/class/request_validation.dart'; +import 'package:titan/paiement/class/signed_content.dart'; import 'package:titan/tools/repository/repository.dart'; class RequestsRepository extends Repository { @@ -19,7 +19,7 @@ class RequestsRepository extends Repository { Future acceptRequest( String requestId, - RequestValidation validation, + SignedContent validation, ) async { await create(validation.toJson(), suffix: 'requests/$requestId/accept'); return true; diff --git a/lib/paiement/repositories/stores_repository.dart b/lib/paiement/repositories/stores_repository.dart index 7fa3f50b58..0b050c4b85 100644 --- a/lib/paiement/repositories/stores_repository.dart +++ b/lib/paiement/repositories/stores_repository.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/paiement/class/history.dart'; -import 'package:titan/paiement/class/qr_code_data.dart'; +import 'package:titan/paiement/class/scan_info.dart'; import 'package:titan/paiement/class/store.dart'; import 'package:titan/paiement/class/transaction.dart'; import 'package:titan/tools/functions.dart'; @@ -38,20 +38,17 @@ class StoresRepository extends Repository { ); } - Future scan(String id, QrCodeData data, bool? bypass) async { + Future scan(String id, ScanInfo data) async { return Transaction.fromJson( - await create({ - ...data.toJson(), - "bypass_membership": bypass ?? false, - }, suffix: "/$id/scan"), + await create(data.toJson(), suffix: "/$id/scan"), ); } - Future canScan(String id, QrCodeData data, bool? bypass) async { - final response = await create({ - ...data.toJson(), - "bypass_membership": bypass ?? false, - }, suffix: "/$id/scan/check"); + Future canScan(String id, ScanInfo data) async { + final response = await create( + data.toJson(), + suffix: "/$id/scan/check", + ); return response["success"] == true; } } diff --git a/lib/paiement/tools/functions.dart b/lib/paiement/tools/functions.dart index d5dc5925bf..47afd179fe 100644 --- a/lib/paiement/tools/functions.dart +++ b/lib/paiement/tools/functions.dart @@ -3,8 +3,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:titan/paiement/class/history.dart'; -import 'package:titan/paiement/class/qr_code_data.dart'; -import 'package:titan/paiement/class/qr_code_signature_data.dart'; +import 'package:titan/paiement/class/secured_content_data.dart'; import 'package:titan/paiement/class/wallet_device.dart'; import 'package:titan/paiement/tools/key_service.dart'; @@ -69,31 +68,16 @@ Future getQRCodeContent( KeyService keyService, bool store, ) async { - final keyId = await keyService.getKeyId(); - final keyPair = await keyService.getKeyPair(); - final now = DateTime.now(); final total = (double.parse(payAmount.replaceAll(',', '.')) * 100).round(); - final data = jsonEncode( - QrCodeSignatureData( - id: id, - tot: total, - iat: now, - key: keyId!, - store: store, - ).toJson(), - ); - return jsonEncode( - QrCodeData( - id: id, - tot: total, - iat: now, - key: keyId, - store: store, - signature: base64Encode( - (await keyService.signMessage(keyPair!, data.codeUnits)).bytes, - ), - ).toJson(), + final content = SecuredContentData( + id: id, + tot: total, + iat: DateTime.now(), + key: (await keyService.getKeyId())!, + store: store, ); + final signed = await keyService.signContent(content); + return jsonEncode(signed!.toJson()); } String transferTypeToString(TransferType type) { diff --git a/lib/paiement/tools/key_service.dart b/lib/paiement/tools/key_service.dart index 11a247dbfd..3a33cd2390 100644 --- a/lib/paiement/tools/key_service.dart +++ b/lib/paiement/tools/key_service.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:titan/paiement/class/secured_content_data.dart'; +import 'package:titan/paiement/class/signed_content.dart'; import 'package:titan/tools/functions.dart'; class KeyService { @@ -87,4 +89,20 @@ class KeyService { ) async { return await algorithm.sign(message, keyPair: keyPair); } + + Future signContent(SecuredContentData content) async { + final keyId = await getKeyId(); + final keyPair = await getKeyPair(); + if (keyId == null || keyPair == null) return null; + final data = jsonEncode(content.toJson()); + final signature = await signMessage(keyPair, data.codeUnits); + return SignedContent( + id: content.id, + tot: content.tot, + iat: content.iat, + key: keyId, + store: content.store, + signature: base64.encode(signature.bytes), + ); + } } diff --git a/lib/paiement/ui/pages/main_page/main_page.dart b/lib/paiement/ui/pages/main_page/main_page.dart index 569b07504c..a4bf8f6636 100644 --- a/lib/paiement/ui/pages/main_page/main_page.dart +++ b/lib/paiement/ui/pages/main_page/main_page.dart @@ -1,12 +1,10 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; import 'package:titan/paiement/class/payment_request.dart'; -import 'package:titan/paiement/class/request_validation.dart'; +import 'package:titan/paiement/class/secured_content_data.dart'; import 'package:titan/paiement/providers/has_accepted_tos_provider.dart'; import 'package:titan/paiement/providers/my_wallet_provider.dart'; import 'package:titan/paiement/providers/payment_requests_provider.dart'; @@ -105,9 +103,15 @@ class PaymentMainPage extends HookConsumerWidget { itemDescription: request.storeNote ?? '', itemPrice: request.total, onConfirm: () async { - final keyId = await keyService.getKeyId(); - final keyPair = await keyService.getKeyPair(); - if (keyId == null || keyPair == null) { + final content = SecuredContentData( + id: request.id, + tot: request.total, + iat: DateTime.now(), + key: (await keyService.getKeyId()) ?? '', + store: false, + ); + final validation = await keyService.signContent(content); + if (validation == null) { if (context.mounted) { Navigator.of(context).pop(); displayToast( @@ -118,25 +122,6 @@ class PaymentMainPage extends HookConsumerWidget { } return; } - final now = DateTime.now(); - final validationData = RequestValidationData( - requestId: request.id, - key: keyId, - iat: now, - tot: request.total, - ); - final dataToSign = jsonEncode(validationData.toJson()); - final signature = await keyService.signMessage( - keyPair, - dataToSign.codeUnits, - ); - final validation = RequestValidation( - requestId: request.id, - key: keyId, - iat: now, - tot: request.total, - signature: base64Encode(signature.bytes), - ); final success = await paymentRequestsNotifier.acceptRequest( request, validation, diff --git a/lib/paiement/ui/pages/scan_page/scanner.dart b/lib/paiement/ui/pages/scan_page/scanner.dart index 69223197e7..4e011592f8 100644 --- a/lib/paiement/ui/pages/scan_page/scanner.dart +++ b/lib/paiement/ui/pages/scan_page/scanner.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/class/scan_info.dart'; import 'package:titan/paiement/providers/barcode_provider.dart'; import 'package:titan/paiement/providers/bypass_provider.dart'; import 'package:titan/paiement/providers/last_time_scanned.dart'; @@ -87,10 +88,16 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { barcodes.barcodes.firstOrNull!.rawValue!, ); if (!bypass) { - final canScan = await scanNotifier.canScan(store.id, data); + final canScan = await scanNotifier.canScan( + store.id, + ScanInfo.fromSignedContent(data), + ); if (!canScan) { showWithoutMembershipDialog(() async { - final value = await scanNotifier.scan(store.id, data, bypass: true); + final value = await scanNotifier.scan( + store.id, + ScanInfo.fromSignedContent(data, bypassMembership: true), + ); if (value == null) { displayToastWithContext( TypeMsg.error, @@ -105,7 +112,10 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { return; } } - final value = await scanNotifier.scan(store.id, data); + final value = await scanNotifier.scan( + store.id, + ScanInfo.fromSignedContent(data), + ); if (value == null) { displayToastWithContext( TypeMsg.error, From bd3f7e63738a1ae801b26795b90ccb2f498d64ba Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Mon, 6 Apr 2026 19:53:38 +0200 Subject: [PATCH 2/4] feat: adding activities history --- lib/l10n/app_en.arb | 7 + lib/l10n/app_fr.arb | 7 + lib/l10n/app_localizations.dart | 42 +++++ lib/l10n/app_localizations_en.dart | 21 +++ lib/l10n/app_localizations_fr.dart | 21 +++ .../providers/request_history_provider.dart | 23 +++ .../repositories/requests_repository.dart | 5 +- lib/paiement/router.dart | 10 ++ lib/paiement/ui/components/request_card.dart | 155 ++++++++++++++++++ .../ui/components/request_detail_modal.dart | 104 ++++++++++++ .../ui/components/show_request_modal.dart | 93 +++++++++++ .../ui/components/transaction_card.dart | 2 +- .../main_page/account_card/account_card.dart | 8 + .../ui/pages/main_page/main_page.dart | 79 +-------- .../request_history_page.dart | 151 +++++++++++++++++ 15 files changed, 655 insertions(+), 73 deletions(-) create mode 100644 lib/paiement/providers/request_history_provider.dart create mode 100644 lib/paiement/ui/components/request_card.dart create mode 100644 lib/paiement/ui/components/request_detail_modal.dart create mode 100644 lib/paiement/ui/components/show_request_modal.dart create mode 100644 lib/paiement/ui/pages/request_history_page/request_history_page.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 08a18c67c0..77b330e958 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -968,6 +968,13 @@ "paiementPaymentRequestError": "Error processing payment request", "paiementAccept": "Accept", "paiementRefuse": "Refuse", + "paiementRequestHistory": "Activities", + "paiementRequestStatusPending": "Pending", + "paiementRequestStatusRefused": "Refused", + "paiementRequestStatusExpired": "Expired", + "paiementRequestStatusAccepted": "Accepted", + "paiementRequestDetails": "Request details", + "paiementNoRequests": "No payment requests", "paiementSuccessfullyAddedStore": "Store successfully added", "paiementSuccessfullyModifiedStore": "Store successfully updated", "paiementThe": "The", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2100ec00ca..3a84087fe4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -976,6 +976,13 @@ "paiementPaymentRequestError": "Erreur lors du traitement de la demande", "paiementAccept": "Accepter", "paiementRefuse": "Refuser", + "paiementRequestHistory": "Activités", + "paiementRequestStatusPending": "En attente", + "paiementRequestStatusRefused": "Refusée", + "paiementRequestStatusExpired": "Expirée", + "paiementRequestStatusAccepted": "Acceptée", + "paiementRequestDetails": "Détails de la demande", + "paiementNoRequests": "Aucune demande de paiement", "paiementSuccessfullyAddedStore": "Magasin ajoutée avec succès", "paiementSuccessfullyModifiedStore": "Magasin modifiée avec succès", "paiementThe": "Le", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index afb31aa700..ee0f907d24 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5246,6 +5246,48 @@ abstract class AppLocalizations { /// **'Refuser'** String get paiementRefuse; + /// No description provided for @paiementRequestHistory. + /// + /// In fr, this message translates to: + /// **'Activités'** + String get paiementRequestHistory; + + /// No description provided for @paiementRequestStatusPending. + /// + /// In fr, this message translates to: + /// **'En attente'** + String get paiementRequestStatusPending; + + /// No description provided for @paiementRequestStatusRefused. + /// + /// In fr, this message translates to: + /// **'Refusée'** + String get paiementRequestStatusRefused; + + /// No description provided for @paiementRequestStatusExpired. + /// + /// In fr, this message translates to: + /// **'Expirée'** + String get paiementRequestStatusExpired; + + /// No description provided for @paiementRequestStatusAccepted. + /// + /// In fr, this message translates to: + /// **'Acceptée'** + String get paiementRequestStatusAccepted; + + /// No description provided for @paiementRequestDetails. + /// + /// In fr, this message translates to: + /// **'Détails de la demande'** + String get paiementRequestDetails; + + /// No description provided for @paiementNoRequests. + /// + /// In fr, this message translates to: + /// **'Aucune demande de paiement'** + String get paiementNoRequests; + /// No description provided for @paiementSuccessfullyAddedStore. /// /// In fr, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 601a6c0542..faa222a206 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2669,6 +2669,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get paiementRefuse => 'Refuse'; + @override + String get paiementRequestHistory => 'Activities'; + + @override + String get paiementRequestStatusPending => 'Pending'; + + @override + String get paiementRequestStatusRefused => 'Refused'; + + @override + String get paiementRequestStatusExpired => 'Expired'; + + @override + String get paiementRequestStatusAccepted => 'Accepted'; + + @override + String get paiementRequestDetails => 'Request details'; + + @override + String get paiementNoRequests => 'No payment requests'; + @override String get paiementSuccessfullyAddedStore => 'Store successfully added'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 54b359d2b0..b986277dc5 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2699,6 +2699,27 @@ class AppLocalizationsFr extends AppLocalizations { @override String get paiementRefuse => 'Refuser'; + @override + String get paiementRequestHistory => 'Activités'; + + @override + String get paiementRequestStatusPending => 'En attente'; + + @override + String get paiementRequestStatusRefused => 'Refusée'; + + @override + String get paiementRequestStatusExpired => 'Expirée'; + + @override + String get paiementRequestStatusAccepted => 'Acceptée'; + + @override + String get paiementRequestDetails => 'Détails de la demande'; + + @override + String get paiementNoRequests => 'Aucune demande de paiement'; + @override String get paiementSuccessfullyAddedStore => 'Magasin ajoutée avec succès'; diff --git a/lib/paiement/providers/request_history_provider.dart b/lib/paiement/providers/request_history_provider.dart new file mode 100644 index 0000000000..a9f81f949c --- /dev/null +++ b/lib/paiement/providers/request_history_provider.dart @@ -0,0 +1,23 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/paiement/repositories/requests_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; + +class RequestHistoryNotifier extends ListNotifier { + final RequestsRepository requestsRepository; + RequestHistoryNotifier({required this.requestsRepository}) + : super(const AsyncValue.loading()); + + Future>> getRequestHistory() async { + return await loadList(() => requestsRepository.getRequests(used: true)); + } +} + +final requestHistoryProvider = StateNotifierProvider< + RequestHistoryNotifier, + AsyncValue> +>((ref) { + final requestsRepository = ref.watch(requestsRepositoryProvider); + return RequestHistoryNotifier(requestsRepository: requestsRepository) + ..getRequestHistory(); +}); diff --git a/lib/paiement/repositories/requests_repository.dart b/lib/paiement/repositories/requests_repository.dart index 467401543a..e7bb7feddd 100644 --- a/lib/paiement/repositories/requests_repository.dart +++ b/lib/paiement/repositories/requests_repository.dart @@ -9,10 +9,11 @@ class RequestsRepository extends Repository { // ignore: overridden_fields final ext = 'mypayment/'; - Future> getRequests() async { + Future> getRequests({bool? used}) async { + final query = used != null ? '?used=$used' : ''; return List.from( (await getList( - suffix: 'requests', + suffix: 'requests$query', )).map((e) => PaymentRequest.fromJson(e)), ); } diff --git a/lib/paiement/router.dart b/lib/paiement/router.dart index 4895966a5a..a6a065cb0e 100644 --- a/lib/paiement/router.dart +++ b/lib/paiement/router.dart @@ -23,6 +23,8 @@ import 'package:titan/paiement/ui/pages/stats_page/stats_page.dart' deferred as stats_page; import 'package:titan/paiement/ui/pages/store_stats_page/store_stats_page.dart' deferred as store_stats_page; +import 'package:titan/paiement/ui/pages/request_history_page/request_history_page.dart' + deferred as request_history_page; import 'package:titan/paiement/ui/pages/transfer_structure_page/transfer_structure_page.dart' deferred as transfer_structure_page; import 'package:titan/tools/functions.dart'; @@ -44,6 +46,7 @@ class PaymentRouter { static const String transferStructure = '/transferStructure'; static const String storeAdmin = '/storeAdmin'; static const String storeStats = '/storeStats'; + static const String requestHistory = '/requestHistory'; static final Module module = Module( getName: (context) => getPaymentName(), getDescription: (context) => @@ -116,6 +119,13 @@ class PaymentRouter { builder: () => store_stats_page.StoreStatsPage(), middleware: [DeferredLoadingMiddleware(store_stats_page.loadLibrary)], ), + QRoute( + path: PaymentRouter.requestHistory, + builder: () => request_history_page.RequestHistoryPage(), + middleware: [ + DeferredLoadingMiddleware(request_history_page.loadLibrary), + ], + ), QRoute( path: PaymentRouter.fund, builder: () => fund_page.WebViewExample(), diff --git a/lib/paiement/ui/components/request_card.dart b/lib/paiement/ui/components/request_card.dart new file mode 100644 index 0000000000..2be8cd3d96 --- /dev/null +++ b/lib/paiement/ui/components/request_card.dart @@ -0,0 +1,155 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; + +class RequestCard extends ConsumerWidget { + final PaymentRequest request; + final VoidCallback? onTap; + const RequestCard({super.key, required this.request, this.onTap}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); + final localizeWithContext = AppLocalizations.of(context)!; + + final HeroIcons icon; + final List colors; + final String? statusLabel; + + switch (request.status) { + case RequestStatus.proposed: + icon = HeroIcons.clock; + colors = [ + const Color.fromARGB(255, 255, 165, 0), + const Color.fromARGB(255, 204, 130, 0), + ]; + statusLabel = localizeWithContext.paiementRequestStatusPending; + case RequestStatus.accepted: + icon = HeroIcons.checkCircle; + colors = [ + const Color.fromARGB(255, 1, 127, 128), + const Color.fromARGB(255, 0, 102, 103), + ]; + statusLabel = null; + case RequestStatus.refused: + icon = HeroIcons.xCircle; + colors = [ + const Color.fromARGB(255, 204, 70, 25), + const Color.fromARGB(255, 163, 56, 20), + ]; + statusLabel = localizeWithContext.paiementRequestStatusRefused; + case RequestStatus.expired: + icon = HeroIcons.clock; + colors = [ + const Color.fromARGB(255, 128, 128, 128), + const Color.fromARGB(255, 100, 100, 100), + ]; + statusLabel = localizeWithContext.paiementRequestStatusExpired; + } + + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + height: 70, + padding: const EdgeInsets.symmetric(horizontal: 20), + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Container( + width: 54, + height: 54, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: colors, + center: Alignment.topLeft, + radius: 1, + ), + ), + child: HeroIcon(icon, color: Colors.white, size: 25), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: AutoSizeText( + request.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xff204550), + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + if (statusLabel != null) ...[ + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: colors[0].withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(5), + ), + child: Text( + statusLabel, + style: TextStyle( + color: colors[0], + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 5), + Text( + "${localizeWithContext.paiementThe} ${DateFormat.yMMMMEEEEd(Localizations.localeOf(context).toString()).format(request.creation)} ${localizeWithContext.paiementAt} ${DateFormat.Hm(Localizations.localeOf(context).toString()).format(request.creation)}", + style: const TextStyle( + color: Color(0xff204550), + fontSize: 12, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + Text( + "${request.status == RequestStatus.accepted ? "- " : ""}${formatter.format(request.total / 100)}", + style: TextStyle( + color: const Color(0xff204550), + fontSize: 18, + fontWeight: FontWeight.bold, + decoration: request.status == RequestStatus.refused || + request.status == RequestStatus.expired + ? TextDecoration.lineThrough + : TextDecoration.none, + decorationColor: const Color(0xff204550).withValues(alpha: 0.8), + decorationThickness: 2.85, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/request_detail_modal.dart b/lib/paiement/ui/components/request_detail_modal.dart new file mode 100644 index 0000000000..705beb1e5c --- /dev/null +++ b/lib/paiement/ui/components/request_detail_modal.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/product_card.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; + +class RequestDetailModal extends StatelessWidget { + final PaymentRequest request; + const RequestDetailModal({super.key, required this.request}); + + @override + Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + final dateFormatter = DateFormat.yMMMMEEEEd().add_Hm(); + + final String statusLabel; + final Color statusColor; + final HeroIcons statusIcon; + + switch (request.status) { + case RequestStatus.accepted: + statusLabel = localizeWithContext.paiementRequestStatusAccepted; + statusColor = const Color.fromARGB(255, 1, 127, 128); + statusIcon = HeroIcons.checkCircle; + case RequestStatus.refused: + statusLabel = localizeWithContext.paiementRequestStatusRefused; + statusColor = const Color.fromARGB(255, 204, 70, 25); + statusIcon = HeroIcons.xCircle; + case RequestStatus.expired: + statusLabel = localizeWithContext.paiementRequestStatusExpired; + statusColor = const Color.fromARGB(255, 128, 128, 128); + statusIcon = HeroIcons.clock; + case RequestStatus.proposed: + statusLabel = localizeWithContext.paiementRequestStatusPending; + statusColor = const Color.fromARGB(255, 255, 165, 0); + statusIcon = HeroIcons.clock; + } + + return BottomModalTemplate( + title: localizeWithContext.paiementRequestDetails, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ProductCard( + title: request.name, + description: request.storeNote ?? '', + priceInCents: request.total, + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: statusColor.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + HeroIcon(statusIcon, color: statusColor, size: 24), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + statusLabel, + style: TextStyle( + color: statusColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + dateFormatter.format(request.creation), + style: TextStyle( + color: statusColor.withValues(alpha: 0.8), + fontSize: 13, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 24), + Button.secondary( + text: localizeWithContext.paiementClose, + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/show_request_modal.dart b/lib/paiement/ui/components/show_request_modal.dart new file mode 100644 index 0000000000..5a60d43faf --- /dev/null +++ b/lib/paiement/ui/components/show_request_modal.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/paiement/class/secured_content_data.dart'; +import 'package:titan/paiement/providers/payment_requests_provider.dart'; +import 'package:titan/paiement/tools/key_service.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; + +Future showRequestModal({ + required BuildContext context, + required WidgetRef ref, + required PaymentRequest request, + VoidCallback? onSuccess, +}) async { + final keyService = KeyService(); + final paymentRequestsNotifier = ref.read(paymentRequestsProvider.notifier); + + await showCustomBottomModal( + context: context, + ref: ref, + modal: PaimentDelegateModal( + itemTitle: request.name, + itemDescription: request.storeNote ?? '', + itemPrice: request.total, + onConfirm: () async { + final keyId = await keyService.getKeyId(); + final keyPair = await keyService.getKeyPair(); + if (keyId == null || keyPair == null) { + if (context.mounted) { + Navigator.of(context).pop(); + displayToast( + context, + TypeMsg.error, + AppLocalizations.of(context)!.paiementPaymentRequestError, + ); + } + return; + } + final now = DateTime.now(); + final validationData = SecuredContentData( + id: request.id, + key: keyId, + iat: now, + tot: request.total, + store: true, + ); + final validation = await keyService.signContent(validationData); + if (validation == null) { + if (context.mounted) { + Navigator.of(context).pop(); + displayToast( + context, + TypeMsg.error, + AppLocalizations.of(context)!.paiementPaymentRequestError, + ); + } + return; + } + final success = await paymentRequestsNotifier.acceptRequest( + request, + validation, + ); + if (context.mounted) { + Navigator.of(context).pop(); + displayToast( + context, + success ? TypeMsg.msg : TypeMsg.error, + success + ? AppLocalizations.of(context)!.paiementPaymentRequestAccepted + : AppLocalizations.of(context)!.paiementPaymentRequestError, + ); + if (success) onSuccess?.call(); + } + }, + onRefuse: () async { + final success = await paymentRequestsNotifier.refuseRequest(request); + if (context.mounted) { + Navigator.of(context).pop(); + displayToast( + context, + success ? TypeMsg.msg : TypeMsg.error, + success + ? AppLocalizations.of(context)!.paiementPaymentRequestRefused + : AppLocalizations.of(context)!.paiementPaymentRequestError, + ); + } + }, + ), + ); +} diff --git a/lib/paiement/ui/components/transaction_card.dart b/lib/paiement/ui/components/transaction_card.dart index 5c033d2cbc..39653edce0 100644 --- a/lib/paiement/ui/components/transaction_card.dart +++ b/lib/paiement/ui/components/transaction_card.dart @@ -125,7 +125,7 @@ class TransactionCard extends ConsumerWidget { ), if (transaction.refund == null) const SizedBox(height: 5), Text( - "${AppLocalizations.of(context)!.paiementThe} ${DateFormat.yMMMMEEEEd(locale.toString()).format(transaction.creation)} + ${AppLocalizations.of(context)!.paiementAt} ${DateFormat.Hm(locale.toString()).format(transaction.creation)}", + "${AppLocalizations.of(context)!.paiementThe} ${DateFormat.yMMMMEEEEd(Localizations.localeOf(context).toString()).format(transaction.creation)} ${AppLocalizations.of(context)!.paiementAt} ${DateFormat.Hm(Localizations.localeOf(context).toString()).format(transaction.creation)}", style: const TextStyle( color: Color(0xff204550), fontSize: 12, diff --git a/lib/paiement/ui/pages/main_page/account_card/account_card.dart b/lib/paiement/ui/pages/main_page/account_card/account_card.dart index 318baaf465..d5ac5309e0 100644 --- a/lib/paiement/ui/pages/main_page/account_card/account_card.dart +++ b/lib/paiement/ui/pages/main_page/account_card/account_card.dart @@ -196,6 +196,14 @@ class AccountCard extends HookConsumerWidget { QR.to(PaymentRouter.root + PaymentRouter.stats); }, ), + MainCardButton( + colors: buttonGradient, + icon: HeroIcons.listBullet, + title: localizeWithContext.paiementRequestHistory, + onPressed: () async { + QR.to(PaymentRouter.root + PaymentRouter.requestHistory); + }, + ), MainCardButton( colors: buttonGradient, icon: HeroIcons.creditCard, diff --git a/lib/paiement/ui/pages/main_page/main_page.dart b/lib/paiement/ui/pages/main_page/main_page.dart index a4bf8f6636..053b60a281 100644 --- a/lib/paiement/ui/pages/main_page/main_page.dart +++ b/lib/paiement/ui/pages/main_page/main_page.dart @@ -14,8 +14,7 @@ import 'package:titan/paiement/providers/my_history_provider.dart'; import 'package:titan/paiement/providers/my_stores_provider.dart'; import 'package:titan/paiement/providers/register_provider.dart'; import 'package:titan/paiement/providers/should_display_tos_dialog.dart'; -import 'package:titan/paiement/tools/key_service.dart'; -import 'package:titan/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart'; +import 'package:titan/paiement/ui/components/show_request_modal.dart'; import 'package:titan/paiement/ui/pages/main_page/account_card/account_card.dart'; import 'package:titan/paiement/ui/pages/main_page/tos_dialog.dart'; import 'package:titan/paiement/ui/pages/main_page/account_card/last_transactions.dart'; @@ -27,7 +26,6 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; class PaymentMainPage extends HookConsumerWidget { const PaymentMainPage({super.key}); @@ -93,74 +91,15 @@ class PaymentMainPage extends HookConsumerWidget { } } - Future showRequestModal(PaymentRequest request) async { - final keyService = KeyService(); - await showCustomBottomModal( + Future onShowRequestModal(PaymentRequest request) async { + await showRequestModal( context: context, ref: ref, - modal: PaimentDelegateModal( - itemTitle: request.name, - itemDescription: request.storeNote ?? '', - itemPrice: request.total, - onConfirm: () async { - final content = SecuredContentData( - id: request.id, - tot: request.total, - iat: DateTime.now(), - key: (await keyService.getKeyId()) ?? '', - store: false, - ); - final validation = await keyService.signContent(content); - if (validation == null) { - if (context.mounted) { - Navigator.of(context).pop(); - displayToast( - context, - TypeMsg.error, - AppLocalizations.of(context)!.paiementPaymentRequestError, - ); - } - return; - } - final success = await paymentRequestsNotifier.acceptRequest( - request, - validation, - ); - if (context.mounted) { - Navigator.of(context).pop(); - displayToast( - context, - success ? TypeMsg.msg : TypeMsg.error, - success - ? AppLocalizations.of( - context, - )!.paiementPaymentRequestAccepted - : AppLocalizations.of(context)!.paiementPaymentRequestError, - ); - if (success) { - await myHistoryNotifier.getHistory(); - await myWalletNotifier.getMyWallet(); - } - } - }, - onRefuse: () async { - final success = await paymentRequestsNotifier.refuseRequest( - request, - ); - if (context.mounted) { - Navigator.of(context).pop(); - displayToast( - context, - success ? TypeMsg.msg : TypeMsg.error, - success - ? AppLocalizations.of( - context, - )!.paiementPaymentRequestRefused - : AppLocalizations.of(context)!.paiementPaymentRequestError, - ); - } - }, - ), + request: request, + onSuccess: () async { + await myHistoryNotifier.getHistory(); + await myWalletNotifier.getMyWallet(); + }, ); } @@ -172,7 +111,7 @@ class PaymentMainPage extends HookConsumerWidget { if (pendingRequests.isNotEmpty && !hasShownRequestModal.value) { hasShownRequestModal.value = true; WidgetsBinding.instance.addPostFrameCallback((_) { - showRequestModal(pendingRequests.first); + onShowRequestModal(pendingRequests.first); }); } }); diff --git a/lib/paiement/ui/pages/request_history_page/request_history_page.dart b/lib/paiement/ui/pages/request_history_page/request_history_page.dart new file mode 100644 index 0000000000..b946554c41 --- /dev/null +++ b/lib/paiement/ui/pages/request_history_page/request_history_page.dart @@ -0,0 +1,151 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/paiement/providers/request_history_provider.dart'; +import 'package:titan/paiement/ui/components/request_card.dart'; +import 'package:titan/paiement/ui/components/request_detail_modal.dart'; +import 'package:titan/paiement/ui/components/show_request_modal.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/paiement/ui/pages/main_page/account_card/day_divider.dart'; +import 'package:titan/paiement/ui/paiement.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class RequestHistoryPage extends HookConsumerWidget { + const RequestHistoryPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final requestHistory = ref.watch(requestHistoryProvider); + final requestHistoryNotifier = ref.read(requestHistoryProvider.notifier); + final localizeWithContext = AppLocalizations.of(context)!; + + useEffect(() { + final timers = []; + requestHistory.whenData((requests) { + final now = DateTime.now(); + for (final request in requests) { + if (request.status != RequestStatus.proposed) continue; + final expiresAt = request.creation.add(const Duration(minutes: 15)); + final remaining = expiresAt.difference(now); + if (remaining.isNegative) continue; + timers.add( + Timer(remaining, () => requestHistoryNotifier.getRequestHistory()), + ); + } + }); + return () { + for (final timer in timers) { + timer.cancel(); + } + }; + }, [requestHistory]); + + return PaymentTemplate( + child: LayoutBuilder( + builder: (context, constraints) => Refresher( + controller: ScrollController(), + onRefresh: () async { + await requestHistoryNotifier.getRequestHistory(); + }, + child: SizedBox( + height: constraints.maxHeight, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + const SizedBox(height: 25), + Container( + padding: const EdgeInsets.symmetric(horizontal: 30), + alignment: Alignment.centerLeft, + child: Text( + localizeWithContext.paiementRequestHistory, + style: const TextStyle( + color: Color(0xff204550), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 15), + AsyncChild( + value: requestHistory, + builder: (context, requests) { + if (requests.isEmpty) { + return Padding( + padding: const EdgeInsets.all(40), + child: Text( + localizeWithContext.paiementNoRequests, + style: const TextStyle( + color: Color(0xff204550), + fontSize: 16, + ), + ), + ); + } + final sortedRequests = List.from( + requests, + )..sort((a, b) => b.creation.compareTo(a.creation)); + + final Map> groupedByDay = {}; + final Map stringDate = {}; + for (var request in sortedRequests) { + final day = timeago.format( + request.creation, + locale: 'fr_short', + ); + if (groupedByDay[day] == null) { + groupedByDay[day] = []; + stringDate[day] = request.creation; + } + groupedByDay[day]!.add(request); + } + final sortedKeys = stringDate.keys.toList() + ..sort( + (a, b) => stringDate[b]!.compareTo(stringDate[a]!), + ); + + return Column( + children: [ + for (var day in sortedKeys) ...[ + DayDivider(date: day), + for (var request in groupedByDay[day]!) + RequestCard( + request: request, + onTap: request.status == RequestStatus.proposed + ? () async { + await showRequestModal( + context: context, + ref: ref, + request: request, + ); + await requestHistoryNotifier + .getRequestHistory(); + } + : () => showCustomBottomModal( + context: context, + ref: ref, + modal: RequestDetailModal( + request: request, + ), + ), + ), + ], + ], + ); + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} From ac63a2eb525a597b978beea1b3dc6ea12f26d6d6 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Mon, 6 Apr 2026 19:54:29 +0200 Subject: [PATCH 3/4] fix: formatting --- lib/paiement/class/signed_content.dart | 4 +- .../providers/request_history_provider.dart | 17 +- lib/paiement/providers/scan_provider.dart | 5 +- .../repositories/requests_repository.dart | 5 +- .../repositories/stores_repository.dart | 5 +- lib/paiement/ui/components/request_card.dart | 159 +++++++++--------- .../ui/components/request_detail_modal.dart | 4 +- .../request_history_page.dart | 15 +- 8 files changed, 101 insertions(+), 113 deletions(-) diff --git a/lib/paiement/class/signed_content.dart b/lib/paiement/class/signed_content.dart index ee744b9ec8..35456064a5 100644 --- a/lib/paiement/class/signed_content.dart +++ b/lib/paiement/class/signed_content.dart @@ -24,9 +24,7 @@ class SignedContent extends SecuredContentData { return 'SignedContent {id: $id, tot: $tot, iat: $iat, key: $key, store: $store, signature: $signature}'; } - SignedContent.empty() - : signature = '', - super.empty(); + SignedContent.empty() : signature = '', super.empty(); @override SignedContent copyWith({ diff --git a/lib/paiement/providers/request_history_provider.dart b/lib/paiement/providers/request_history_provider.dart index a9f81f949c..61007e65eb 100644 --- a/lib/paiement/providers/request_history_provider.dart +++ b/lib/paiement/providers/request_history_provider.dart @@ -13,11 +13,12 @@ class RequestHistoryNotifier extends ListNotifier { } } -final requestHistoryProvider = StateNotifierProvider< - RequestHistoryNotifier, - AsyncValue> ->((ref) { - final requestsRepository = ref.watch(requestsRepositoryProvider); - return RequestHistoryNotifier(requestsRepository: requestsRepository) - ..getRequestHistory(); -}); +final requestHistoryProvider = + StateNotifierProvider< + RequestHistoryNotifier, + AsyncValue> + >((ref) { + final requestsRepository = ref.watch(requestsRepositoryProvider); + return RequestHistoryNotifier(requestsRepository: requestsRepository) + ..getRequestHistory(); + }); diff --git a/lib/paiement/providers/scan_provider.dart b/lib/paiement/providers/scan_provider.dart index ac53d0c2d5..50869d12d5 100644 --- a/lib/paiement/providers/scan_provider.dart +++ b/lib/paiement/providers/scan_provider.dart @@ -9,10 +9,7 @@ class ScanNotifier extends SingleNotifier { ScanNotifier({required this.storesRepository}) : super(const AsyncValue.loading()); - Future?> scan( - String storeId, - ScanInfo data, - ) async { + Future?> scan(String storeId, ScanInfo data) async { return await load(() => storesRepository.scan(storeId, data)); } diff --git a/lib/paiement/repositories/requests_repository.dart b/lib/paiement/repositories/requests_repository.dart index e7bb7feddd..8dfa09bed9 100644 --- a/lib/paiement/repositories/requests_repository.dart +++ b/lib/paiement/repositories/requests_repository.dart @@ -18,10 +18,7 @@ class RequestsRepository extends Repository { ); } - Future acceptRequest( - String requestId, - SignedContent validation, - ) async { + Future acceptRequest(String requestId, SignedContent validation) async { await create(validation.toJson(), suffix: 'requests/$requestId/accept'); return true; } diff --git a/lib/paiement/repositories/stores_repository.dart b/lib/paiement/repositories/stores_repository.dart index 0b050c4b85..a14f377559 100644 --- a/lib/paiement/repositories/stores_repository.dart +++ b/lib/paiement/repositories/stores_repository.dart @@ -45,10 +45,7 @@ class StoresRepository extends Repository { } Future canScan(String id, ScanInfo data) async { - final response = await create( - data.toJson(), - suffix: "/$id/scan/check", - ); + final response = await create(data.toJson(), suffix: "/$id/scan/check"); return response["success"] == true; } } diff --git a/lib/paiement/ui/components/request_card.dart b/lib/paiement/ui/components/request_card.dart index 2be8cd3d96..289bd72c4b 100644 --- a/lib/paiement/ui/components/request_card.dart +++ b/lib/paiement/ui/components/request_card.dart @@ -60,96 +60,97 @@ class RequestCard extends ConsumerWidget { onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( - height: 70, - padding: const EdgeInsets.symmetric(horizontal: 20), - width: MediaQuery.of(context).size.width, - child: Row( - children: [ - Container( - width: 54, - height: 54, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: colors, - center: Alignment.topLeft, - radius: 1, + height: 70, + padding: const EdgeInsets.symmetric(horizontal: 20), + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Container( + width: 54, + height: 54, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: colors, + center: Alignment.topLeft, + radius: 1, + ), ), + child: HeroIcon(icon, color: Colors.white, size: 25), ), - child: HeroIcon(icon, color: Colors.white, size: 25), - ), - const SizedBox(width: 15), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: AutoSizeText( - request.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Color(0xff204550), - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - if (statusLabel != null) ...[ - const SizedBox(width: 10), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 5, - vertical: 2, - ), - decoration: BoxDecoration( - color: colors[0].withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(5), - ), - child: Text( - statusLabel, - style: TextStyle( - color: colors[0], - fontSize: 12, + const SizedBox(width: 15), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: AutoSizeText( + request.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xff204550), + fontSize: 14, fontWeight: FontWeight.bold, ), ), ), + if (statusLabel != null) ...[ + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: colors[0].withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(5), + ), + child: Text( + statusLabel, + style: TextStyle( + color: colors[0], + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ], - ], - ), - const SizedBox(height: 5), - Text( - "${localizeWithContext.paiementThe} ${DateFormat.yMMMMEEEEd(Localizations.localeOf(context).toString()).format(request.creation)} ${localizeWithContext.paiementAt} ${DateFormat.Hm(Localizations.localeOf(context).toString()).format(request.creation)}", - style: const TextStyle( - color: Color(0xff204550), - fontSize: 12, ), - ), - ], + const SizedBox(height: 5), + Text( + "${localizeWithContext.paiementThe} ${DateFormat.yMMMMEEEEd(Localizations.localeOf(context).toString()).format(request.creation)} ${localizeWithContext.paiementAt} ${DateFormat.Hm(Localizations.localeOf(context).toString()).format(request.creation)}", + style: const TextStyle( + color: Color(0xff204550), + fontSize: 12, + ), + ), + ], + ), ), - ), - const SizedBox(width: 10), - Text( - "${request.status == RequestStatus.accepted ? "- " : ""}${formatter.format(request.total / 100)}", - style: TextStyle( - color: const Color(0xff204550), - fontSize: 18, - fontWeight: FontWeight.bold, - decoration: request.status == RequestStatus.refused || - request.status == RequestStatus.expired - ? TextDecoration.lineThrough - : TextDecoration.none, - decorationColor: const Color(0xff204550).withValues(alpha: 0.8), - decorationThickness: 2.85, + const SizedBox(width: 10), + Text( + "${request.status == RequestStatus.accepted ? "- " : ""}${formatter.format(request.total / 100)}", + style: TextStyle( + color: const Color(0xff204550), + fontSize: 18, + fontWeight: FontWeight.bold, + decoration: + request.status == RequestStatus.refused || + request.status == RequestStatus.expired + ? TextDecoration.lineThrough + : TextDecoration.none, + decorationColor: const Color(0xff204550).withValues(alpha: 0.8), + decorationThickness: 2.85, + ), ), - ), - ], + ], + ), ), - ), ); } } diff --git a/lib/paiement/ui/components/request_detail_modal.dart b/lib/paiement/ui/components/request_detail_modal.dart index 705beb1e5c..da25c18b4a 100644 --- a/lib/paiement/ui/components/request_detail_modal.dart +++ b/lib/paiement/ui/components/request_detail_modal.dart @@ -57,9 +57,7 @@ class RequestDetailModal extends StatelessWidget { decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all( - color: statusColor.withValues(alpha: 0.3), - ), + border: Border.all(color: statusColor.withValues(alpha: 0.3)), ), child: Row( children: [ diff --git a/lib/paiement/ui/pages/request_history_page/request_history_page.dart b/lib/paiement/ui/pages/request_history_page/request_history_page.dart index b946554c41..589e15c07e 100644 --- a/lib/paiement/ui/pages/request_history_page/request_history_page.dart +++ b/lib/paiement/ui/pages/request_history_page/request_history_page.dart @@ -88,9 +88,8 @@ class RequestHistoryPage extends HookConsumerWidget { ), ); } - final sortedRequests = List.from( - requests, - )..sort((a, b) => b.creation.compareTo(a.creation)); + final sortedRequests = List.from(requests) + ..sort((a, b) => b.creation.compareTo(a.creation)); final Map> groupedByDay = {}; final Map stringDate = {}; @@ -128,12 +127,12 @@ class RequestHistoryPage extends HookConsumerWidget { .getRequestHistory(); } : () => showCustomBottomModal( - context: context, - ref: ref, - modal: RequestDetailModal( - request: request, - ), + context: context, + ref: ref, + modal: RequestDetailModal( + request: request, ), + ), ), ], ], From 84dc8bf0f374540fb5ccbff7a4e9b91ad47f2ca5 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Mon, 6 Apr 2026 20:06:53 +0200 Subject: [PATCH 4/4] fix: import --- lib/paiement/ui/pages/main_page/main_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/paiement/ui/pages/main_page/main_page.dart b/lib/paiement/ui/pages/main_page/main_page.dart index 053b60a281..73b720957b 100644 --- a/lib/paiement/ui/pages/main_page/main_page.dart +++ b/lib/paiement/ui/pages/main_page/main_page.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; import 'package:titan/paiement/class/payment_request.dart'; -import 'package:titan/paiement/class/secured_content_data.dart'; import 'package:titan/paiement/providers/has_accepted_tos_provider.dart'; import 'package:titan/paiement/providers/my_wallet_provider.dart'; import 'package:titan/paiement/providers/payment_requests_provider.dart';