From 3823490c034033cc392b8432ea952d32ee0e17b0 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Sat, 14 Feb 2026 21:02:14 +0100 Subject: [PATCH 1/9] feat: adding paymennt delegate modal --- lib/l10n/app_en.arb | 6 + lib/l10n/app_fr.arb | 6 + lib/l10n/app_localizations.dart | 36 +++++ lib/l10n/app_localizations_en.dart | 18 +++ lib/l10n/app_localizations_fr.dart | 18 +++ .../paiment_delegate/account_card.dart | 144 +++++++++++++++++ .../paiment_delegate/add_funds_button.dart | 56 +++++++ .../paiment_delegate/confirm_button.dart | 138 ++++++++++++++++ .../paiment_delegate/countdown_timer.dart | 125 ++++++++++++++ .../paiment_delegate_modal.dart | 152 ++++++++++++++++++ 10 files changed, 699 insertions(+) create mode 100644 lib/paiement/ui/components/paiment_delegate/account_card.dart create mode 100644 lib/paiement/ui/components/paiment_delegate/add_funds_button.dart create mode 100644 lib/paiement/ui/components/paiment_delegate/confirm_button.dart create mode 100644 lib/paiement/ui/components/paiment_delegate/countdown_timer.dart create mode 100644 lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a70c0f87cc..6b8db9dbae 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -906,6 +906,12 @@ "paiementPayWithHA": "Pay with HelloAsso", "paiementPending": "Pending", "paiementPersonalBalance": "Personal balance", + "paiementAddFunds": "Add Funds", + "paiementInsufficientFunds": "Insufficient Funds", + "paiementTimeRemaining": "Time Remaining", + "paiementHurryUp": "Hurry up!", + "paiementCompletePayment": "Complete payment", + "paiementConfirmPayment": "Confirm Payment", "paiementPleaseAcceptPopup": "Please allow popups", "paiementPleaseAcceptTOS": "Please accept the Terms of Service.", "paiementPleaseAddDevice": "Please add this device to pay", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index cb6ced5398..230a465286 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -914,6 +914,12 @@ "paiementPayWithHA": "Payer avec HelloAsso", "paiementPending": "En attente", "paiementPersonalBalance": "Solde personnel", + "paiementAddFunds": "Ajouter des fonds", + "paiementInsufficientFunds": "Fonds insuffisants", + "paiementTimeRemaining": "Temps restant", + "paiementHurryUp": "Dépêchez-vous !", + "paiementCompletePayment": "Finaliser le paiement", + "paiementConfirmPayment": "Confirmer le paiement", "paiementPleaseAcceptPopup": "Veuillez autoriser les popups", "paiementPleaseAcceptTOS": "Veuillez accepter les Conditions Générales d'Utilisation.", "paiementPleaseAddDevice": "Veuillez ajouter cet appareil pour payer", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 760e6cec7b..59cebb6c49 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4928,6 +4928,42 @@ abstract class AppLocalizations { /// **'Solde personnel'** String get paiementPersonalBalance; + /// No description provided for @paiementAddFunds. + /// + /// In fr, this message translates to: + /// **'Ajouter des fonds'** + String get paiementAddFunds; + + /// No description provided for @paiementInsufficientFunds. + /// + /// In fr, this message translates to: + /// **'Fonds insuffisants'** + String get paiementInsufficientFunds; + + /// No description provided for @paiementTimeRemaining. + /// + /// In fr, this message translates to: + /// **'Temps restant'** + String get paiementTimeRemaining; + + /// No description provided for @paiementHurryUp. + /// + /// In fr, this message translates to: + /// **'Dépêchez-vous !'** + String get paiementHurryUp; + + /// No description provided for @paiementCompletePayment. + /// + /// In fr, this message translates to: + /// **'Finaliser le paiement'** + String get paiementCompletePayment; + + /// No description provided for @paiementConfirmPayment. + /// + /// In fr, this message translates to: + /// **'Confirmer le paiement'** + String get paiementConfirmPayment; + /// No description provided for @paiementPleaseAcceptPopup. /// /// In fr, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6657a2dd8b..9f0cf37bb1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2504,6 +2504,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get paiementPersonalBalance => 'Personal balance'; + @override + String get paiementAddFunds => 'Add Funds'; + + @override + String get paiementInsufficientFunds => 'Insufficient Funds'; + + @override + String get paiementTimeRemaining => 'Time Remaining'; + + @override + String get paiementHurryUp => 'Hurry up!'; + + @override + String get paiementCompletePayment => 'Complete payment'; + + @override + String get paiementConfirmPayment => 'Confirm Payment'; + @override String get paiementPleaseAcceptPopup => 'Please allow popups'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 18af78314c..83a4a93871 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2529,6 +2529,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String get paiementPersonalBalance => 'Solde personnel'; + @override + String get paiementAddFunds => 'Ajouter des fonds'; + + @override + String get paiementInsufficientFunds => 'Fonds insuffisants'; + + @override + String get paiementTimeRemaining => 'Temps restant'; + + @override + String get paiementHurryUp => 'Dépêchez-vous !'; + + @override + String get paiementCompletePayment => 'Finaliser le paiement'; + + @override + String get paiementConfirmPayment => 'Confirmer le paiement'; + @override String get paiementPleaseAcceptPopup => 'Veuillez autoriser les popups'; diff --git a/lib/paiement/ui/components/paiment_delegate/account_card.dart b/lib/paiement/ui/components/paiment_delegate/account_card.dart new file mode 100644 index 0000000000..9fe45058e7 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/account_card.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/providers/my_wallet_provider.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/add_funds_button.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/confirm_button.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; + +class AccountCard extends HookConsumerWidget { + final void Function() onConfirm; + final DateTime? itemExpirationDate; + final int itemPrice; + + const AccountCard({ + super.key, + required this.onConfirm, + required this.itemExpirationDate, + required this.itemPrice, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); + final myWallet = ref.watch(myWalletProvider); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); + final localizeWithContext = AppLocalizations.of(context)!; + + // Check if user has sufficient funds + final hasSufficientFunds = myWallet.maybeWhen( + data: (wallet) => wallet.balance >= itemPrice, + orElse: () => false, + ); + + return Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: const [ + Color.fromARGB(255, 9, 103, 103), + Color(0xff017f80), + Color.fromARGB(255, 4, 84, 84), + ], + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + spreadRadius: 2, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 15, + width: MediaQuery.of(context).size.width * 0.8, + ), + Row( + children: [ + Text( + localizeWithContext.paiementPersonalBalance, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + const Spacer(), + ], + ), + const SizedBox(height: 20), + Expanded( + child: Container( + width: double.infinity, + alignment: Alignment.center, + child: AsyncChild( + value: myWallet, + builder: (context, wallet) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + formatter.format(wallet.balance / 100), + style: TextStyle( + color: hasSufficientFunds + ? ColorConstants.background + : ColorConstants.error, + fontSize: 50, + ), + ), + ], + ), + errorBuilder: (error, stackTrace) => Text( + localizeWithContext.paiementGetBalanceError, + style: const TextStyle( + color: ColorConstants.error, + fontSize: 50, + ), + ), + ), + ), + ), + ], + ), + ), + ), + Container( + height: 80, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: const BoxDecoration( + color: ColorConstants.background, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + ), + child: hasSufficientFunds + ? ConfirmButton( + onConfirm: onConfirm, + itemExpirationDate: itemExpirationDate, + ) + : const AddFundsButton(), + ), + ], + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/add_funds_button.dart b/lib/paiement/ui/components/paiment_delegate/add_funds_button.dart new file mode 100644 index 0000000000..ea9cce384f --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/add_funds_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/providers/fund_amount_provider.dart'; +import 'package:titan/paiement/ui/pages/fund_page/fund_page.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; + +class AddFundsButton extends ConsumerWidget { + const AddFundsButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final fundAmountNotifier = ref.watch(fundAmountProvider.notifier); + final localizeWithContext = AppLocalizations.of(context)!; + + void showFundModal() async { + Navigator.of(context).pop(); // Close current modal + await showCustomBottomModal( + context: context, + modal: const FundPage(), + ref: ref, + onCloseCallback: () => fundAmountNotifier.setFundAmount(""), + ); + } + + return GestureDetector( + onTap: showFundModal, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xff017f80), Color.fromARGB(255, 4, 84, 84)], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.add_card, color: Colors.white, size: 20), + const SizedBox(width: 8), + Text( + localizeWithContext.paiementAddFunds, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/confirm_button.dart b/lib/paiement/ui/components/paiment_delegate/confirm_button.dart new file mode 100644 index 0000000000..a6b46c5ac5 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/confirm_button.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; + +class ConfirmButton extends HookWidget { + final VoidCallback onConfirm; + final DateTime? itemExpirationDate; + + const ConfirmButton({ + super.key, + required this.onConfirm, + required this.itemExpirationDate, + }); + + @override + Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + final isDisabled = useState(false); + final totalSeconds = itemExpirationDate + ?.difference(DateTime.now()) + .inSeconds; + + final controller = useAnimationController( + duration: Duration(seconds: totalSeconds ?? 0), + ); + + useEffect(() { + controller.forward(); + void listener(AnimationStatus status) { + if (status == AnimationStatus.completed) { + isDisabled.value = true; + } + } + + controller.addStatusListener(listener); + return () => controller.removeStatusListener(listener); + }, [controller]); + + Widget setAnimation(child) { + if (totalSeconds != null && + (controller.isAnimating || controller.isCompleted)) { + return AnimatedBuilder( + animation: controller, + builder: (context, _) { + return child; + }, + ); + } + return child; + } + + return setAnimation( + GestureDetector( + onTap: isDisabled.value ? onConfirm : null, + child: AnimatedBuilder( + animation: controller, + builder: (context, child) { + return Stack( + children: [ + Center( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: isDisabled.value + ? BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(8), + ) + : BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xff017f80), + Color.fromARGB(255, 4, 84, 84), + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + localizeWithContext.paiementConfirmPayment, + textAlign: TextAlign.center, + style: TextStyle( + color: ColorConstants.background, + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ), + ), + if (!isDisabled.value) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LayoutBuilder( + builder: (context, constraints) { + final shimmerWidth = constraints.maxWidth * 0.9; + + // Start completely off left, end completely off right + final startPosition = -shimmerWidth * 1.5; + final endPosition = constraints.maxWidth; + final totalDistance = endPosition - startPosition; + + return Transform.translate( + offset: Offset( + startPosition + + (controller.value * totalDistance), + 0, + ), + child: Container( + width: shimmerWidth, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withOpacity(0.0), + Colors.white.withOpacity(0.2), + Colors.white.withOpacity(0.0), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + ); + }, + ), + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/countdown_timer.dart b/lib/paiement/ui/components/paiment_delegate/countdown_timer.dart new file mode 100644 index 0000000000..c63ef9f46f --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/countdown_timer.dart @@ -0,0 +1,125 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; + +class CountdownTimer extends HookWidget { + final int totalSeconds; + final VoidCallback? onFinished; + + const CountdownTimer({ + super.key, + required this.totalSeconds, + this.onFinished, + }); + + @override + Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + final controller = useAnimationController( + duration: Duration(seconds: totalSeconds), + ); + + useEffect(() { + controller.forward(); + void listener(AnimationStatus status) { + if (status == AnimationStatus.completed) { + onFinished?.call(); + } + } + + controller.addStatusListener(listener); + return () => controller.removeStatusListener(listener); + }, [controller]); + + final progress = useAnimation(controller); + final remainingSeconds = (totalSeconds * (1 - progress)).round(); + final minutes = (remainingSeconds ~/ 60).toString().padLeft(2, '0'); + final seconds = (remainingSeconds % 60).toString().padLeft(2, '0'); + + final colorScheme = Theme.of(context).colorScheme; + final urgencyRatio = progress; + final progressColor = Color.lerp( + const Color(0xff017f80), + colorScheme.error, + urgencyRatio, + )!; + final backgroundColor = progressColor.withOpacity(0.1); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: progressColor.withOpacity(0.3), width: 2), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 80, + height: 80, + child: CircularProgressIndicator( + value: 1 - progress, + strokeWidth: 6, + backgroundColor: progressColor.withOpacity(0.2), + valueColor: AlwaysStoppedAnimation(progressColor), + strokeCap: StrokeCap.round, + ), + ), + AnimatedScale( + scale: remainingSeconds <= 10 ? 1.1 : 1.0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$minutes:$seconds', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: progressColor, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + localizeWithContext.paiementTimeRemaining, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: progressColor.withOpacity(0.7), + ), + ), + const SizedBox(height: 4), + Text( + remainingSeconds <= 30 + ? localizeWithContext.paiementHurryUp + : localizeWithContext.paiementCompletePayment, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: progressColor, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart new file mode 100644 index 0000000000..8f847a8053 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/account_card.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/countdown_timer.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; + +class PaimentDelegateModal extends HookConsumerWidget { + final String itemTitle; + final String itemDescription; + final ImageProvider? itemImage; + final int itemPrice; + final DateTime? itemExpirationDate; + final VoidCallback onConfirm; + const PaimentDelegateModal({ + super.key, + required this.itemTitle, + required this.itemDescription, + required this.itemImage, + required this.itemPrice, + this.itemExpirationDate, + required this.onConfirm, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); + + final priceFormatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + decimalDigits: 2, + ); + + final secondsLeft = itemExpirationDate + ?.difference(DateTime.now()) + .inSeconds; + + return BottomModalTemplate( + title: itemTitle, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Item Image and Details Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + ), + ), + child: Column( + children: [ + if (itemImage != null) ...[ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image( + image: itemImage!, + width: 120, + height: 120, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 16), + ], + Text( + itemDescription, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.8), + ), + ), + const SizedBox(height: 20), + // Price Tag + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xff017f80), + Color.fromARGB(255, 4, 84, 84), + ], + ), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: const Color(0xff017f80).withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.local_offer, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Text( + priceFormatter.format(itemPrice / 100), + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + + // Countdown Timer + if (secondsLeft != null && secondsLeft > 0) ...[ + const SizedBox(height: 24), + CountdownTimer(totalSeconds: secondsLeft), + ], + + const SizedBox(height: 24), + + // Wallet Balance Card + SizedBox( + height: 250, + width: MediaQuery.of(context).size.width, + child: AccountCard( + onConfirm: onConfirm, + itemExpirationDate: itemExpirationDate, + itemPrice: itemPrice, + ), + ), + + const SizedBox(height: 24), + ], + ), + ), + ); + } +} From 2643ead8e0a17434023a7b8664c30cc0a9a70cde Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Fri, 6 Mar 2026 22:17:17 +0100 Subject: [PATCH 2/9] feat: adding payment modal --- lib/l10n/app_en.arb | 4 + lib/l10n/app_fr.arb | 4 + lib/l10n/app_localizations.dart | 24 ++ lib/l10n/app_localizations_en.dart | 12 + lib/l10n/app_localizations_fr.dart | 12 + .../paiment_delegate/account_card.dart | 144 ------------ .../paiment_delegate/add_funds_button.dart | 56 ----- .../paiment_delegate/confirm_button.dart | 201 ++++++++-------- .../paiment_delegate/feedback_overlay.dart | 79 +++++++ .../paiment_delegate_modal.dart | 214 ++++++++---------- .../paiment_delegate/product_card.dart | 82 +++++++ .../paiment_delegate/wallet_balance_card.dart | 69 ++++++ .../ui/pages/main_page/main_page.dart | 45 ++++ 13 files changed, 528 insertions(+), 418 deletions(-) delete mode 100644 lib/paiement/ui/components/paiment_delegate/account_card.dart delete mode 100644 lib/paiement/ui/components/paiment_delegate/add_funds_button.dart create mode 100644 lib/paiement/ui/components/paiment_delegate/feedback_overlay.dart create mode 100644 lib/paiement/ui/components/paiment_delegate/product_card.dart create mode 100644 lib/paiement/ui/components/paiment_delegate/wallet_balance_card.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6b8db9dbae..2836e01cc7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -958,6 +958,10 @@ "paiementStores": "Stores", "paiementStructureAdmin": "Structure administrator", "paiementSuccededTransaction": "Successful payment", + "paiementConfirmYourPurchase": "Confirm your purchase", + "paiementYourBalance": "Your balance", + "paiementPaymentSuccessful": "Payment successful!", + "paiementPaymentCanceled": "Payment canceled", "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 230a465286..4781162b27 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -966,6 +966,10 @@ "paiementStores": "Magasins", "paiementStructureAdmin": "Administrateur de la structure", "paiementSuccededTransaction": "Paiement réussi", + "paiementConfirmYourPurchase": "Confirmer votre achat", + "paiementYourBalance": "Votre solde", + "paiementPaymentSuccessful": "Paiement réussi !", + "paiementPaymentCanceled": "Paiement annulé", "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 59cebb6c49..def8112230 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5192,6 +5192,30 @@ abstract class AppLocalizations { /// **'Paiement réussi'** String get paiementSuccededTransaction; + /// No description provided for @paiementConfirmYourPurchase. + /// + /// In fr, this message translates to: + /// **'Confirmer votre achat'** + String get paiementConfirmYourPurchase; + + /// No description provided for @paiementYourBalance. + /// + /// In fr, this message translates to: + /// **'Votre solde'** + String get paiementYourBalance; + + /// No description provided for @paiementPaymentSuccessful. + /// + /// In fr, this message translates to: + /// **'Paiement réussi !'** + String get paiementPaymentSuccessful; + + /// No description provided for @paiementPaymentCanceled. + /// + /// In fr, this message translates to: + /// **'Paiement annulé'** + String get paiementPaymentCanceled; + /// 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 9f0cf37bb1..4f4a7dd46a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2642,6 +2642,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get paiementSuccededTransaction => 'Successful payment'; + @override + String get paiementConfirmYourPurchase => 'Confirm your purchase'; + + @override + String get paiementYourBalance => 'Your balance'; + + @override + String get paiementPaymentSuccessful => 'Payment successful!'; + + @override + String get paiementPaymentCanceled => 'Payment canceled'; + @override String get paiementSuccessfullyAddedStore => 'Store successfully added'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 83a4a93871..5ddf3b0e84 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2671,6 +2671,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get paiementSuccededTransaction => 'Paiement réussi'; + @override + String get paiementConfirmYourPurchase => 'Confirmer votre achat'; + + @override + String get paiementYourBalance => 'Votre solde'; + + @override + String get paiementPaymentSuccessful => 'Paiement réussi !'; + + @override + String get paiementPaymentCanceled => 'Paiement annulé'; + @override String get paiementSuccessfullyAddedStore => 'Magasin ajoutée avec succès'; diff --git a/lib/paiement/ui/components/paiment_delegate/account_card.dart b/lib/paiement/ui/components/paiment_delegate/account_card.dart deleted file mode 100644 index 9fe45058e7..0000000000 --- a/lib/paiement/ui/components/paiment_delegate/account_card.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:titan/l10n/app_localizations.dart'; -import 'package:titan/paiement/providers/my_wallet_provider.dart'; -import 'package:titan/paiement/ui/components/paiment_delegate/add_funds_button.dart'; -import 'package:titan/paiement/ui/components/paiment_delegate/confirm_button.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/providers/locale_notifier.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; - -class AccountCard extends HookConsumerWidget { - final void Function() onConfirm; - final DateTime? itemExpirationDate; - final int itemPrice; - - const AccountCard({ - super.key, - required this.onConfirm, - required this.itemExpirationDate, - required this.itemPrice, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final locale = ref.watch(localeProvider); - final myWallet = ref.watch(myWalletProvider); - final formatter = NumberFormat.currency( - locale: locale.toString(), - symbol: "€", - ); - final localizeWithContext = AppLocalizations.of(context)!; - - // Check if user has sufficient funds - final hasSufficientFunds = myWallet.maybeWhen( - data: (wallet) => wallet.balance >= itemPrice, - orElse: () => false, - ); - - return Container( - margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: const [ - Color.fromARGB(255, 9, 103, 103), - Color(0xff017f80), - Color.fromARGB(255, 4, 84, 84), - ], - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.3), - spreadRadius: 2, - blurRadius: 7, - offset: const Offset(0, 3), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 15, - width: MediaQuery.of(context).size.width * 0.8, - ), - Row( - children: [ - Text( - localizeWithContext.paiementPersonalBalance, - textAlign: TextAlign.left, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - ), - ), - const Spacer(), - ], - ), - const SizedBox(height: 20), - Expanded( - child: Container( - width: double.infinity, - alignment: Alignment.center, - child: AsyncChild( - value: myWallet, - builder: (context, wallet) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - formatter.format(wallet.balance / 100), - style: TextStyle( - color: hasSufficientFunds - ? ColorConstants.background - : ColorConstants.error, - fontSize: 50, - ), - ), - ], - ), - errorBuilder: (error, stackTrace) => Text( - localizeWithContext.paiementGetBalanceError, - style: const TextStyle( - color: ColorConstants.error, - fontSize: 50, - ), - ), - ), - ), - ), - ], - ), - ), - ), - Container( - height: 80, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: const BoxDecoration( - color: ColorConstants.background, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(30), - bottomRight: Radius.circular(30), - ), - ), - child: hasSufficientFunds - ? ConfirmButton( - onConfirm: onConfirm, - itemExpirationDate: itemExpirationDate, - ) - : const AddFundsButton(), - ), - ], - ), - ); - } -} diff --git a/lib/paiement/ui/components/paiment_delegate/add_funds_button.dart b/lib/paiement/ui/components/paiment_delegate/add_funds_button.dart deleted file mode 100644 index ea9cce384f..0000000000 --- a/lib/paiement/ui/components/paiment_delegate/add_funds_button.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/l10n/app_localizations.dart'; -import 'package:titan/paiement/providers/fund_amount_provider.dart'; -import 'package:titan/paiement/ui/pages/fund_page/fund_page.dart'; -import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; - -class AddFundsButton extends ConsumerWidget { - const AddFundsButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fundAmountNotifier = ref.watch(fundAmountProvider.notifier); - final localizeWithContext = AppLocalizations.of(context)!; - - void showFundModal() async { - Navigator.of(context).pop(); // Close current modal - await showCustomBottomModal( - context: context, - modal: const FundPage(), - ref: ref, - onCloseCallback: () => fundAmountNotifier.setFundAmount(""), - ); - } - - return GestureDetector( - onTap: showFundModal, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xff017f80), Color.fromARGB(255, 4, 84, 84)], - ), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.add_card, color: Colors.white, size: 20), - const SizedBox(width: 8), - Text( - localizeWithContext.paiementAddFunds, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w900, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/paiement/ui/components/paiment_delegate/confirm_button.dart b/lib/paiement/ui/components/paiment_delegate/confirm_button.dart index a6b46c5ac5..335e5985db 100644 --- a/lib/paiement/ui/components/paiment_delegate/confirm_button.dart +++ b/lib/paiement/ui/components/paiment_delegate/confirm_button.dart @@ -2,137 +2,142 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; + +const _teal = Color(0xff017f80); +const _tealDark = Color.fromARGB(255, 4, 84, 84); class ConfirmButton extends HookWidget { final VoidCallback onConfirm; - final DateTime? itemExpirationDate; + final VoidCallback onCancel; + final int? totalSeconds; const ConfirmButton({ super.key, required this.onConfirm, - required this.itemExpirationDate, + required this.onCancel, + required this.totalSeconds, }); @override Widget build(BuildContext context) { final localizeWithContext = AppLocalizations.of(context)!; - final isDisabled = useState(false); - final totalSeconds = itemExpirationDate - ?.difference(DateTime.now()) - .inSeconds; + final isExpired = useState(false); - final controller = useAnimationController( + // Expiration timer: runs once over totalSeconds + final expirationController = useAnimationController( duration: Duration(seconds: totalSeconds ?? 0), ); + // Shimmer: repeating fast sweep (1.5s per cycle) + final shimmerController = useAnimationController( + duration: const Duration(milliseconds: 1500), + ); + useEffect(() { - controller.forward(); + if (totalSeconds != null && totalSeconds! > 0) { + expirationController.forward(); + } void listener(AnimationStatus status) { if (status == AnimationStatus.completed) { - isDisabled.value = true; + isExpired.value = true; + shimmerController.stop(); } } - controller.addStatusListener(listener); - return () => controller.removeStatusListener(listener); - }, [controller]); + expirationController.addStatusListener(listener); + shimmerController.repeat(); + return () { + expirationController.removeStatusListener(listener); + }; + }, [expirationController, shimmerController]); - Widget setAnimation(child) { - if (totalSeconds != null && - (controller.isAnimating || controller.isCompleted)) { - return AnimatedBuilder( - animation: controller, - builder: (context, _) { - return child; - }, - ); - } - return child; - } - - return setAnimation( - GestureDetector( - onTap: isDisabled.value ? onConfirm : null, - child: AnimatedBuilder( - animation: controller, - builder: (context, child) { - return Stack( - children: [ - Center( - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - decoration: isDisabled.value - ? BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(8), - ) - : BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xff017f80), - Color.fromARGB(255, 4, 84, 84), - ], + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: isExpired.value ? null : onConfirm, + child: AnimatedBuilder( + animation: shimmerController, + builder: (context, child) { + return Stack( + children: [ + Center( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + decoration: isExpired.value + ? BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(8), + ) + : BoxDecoration( + gradient: const LinearGradient( + colors: [_teal, _tealDark], + ), + borderRadius: BorderRadius.circular(8), ), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - localizeWithContext.paiementConfirmPayment, - textAlign: TextAlign.center, - style: TextStyle( - color: ColorConstants.background, - fontSize: 18, - fontWeight: FontWeight.w900, + child: Text( + localizeWithContext.paiementConfirmPayment, + textAlign: TextAlign.center, + style: TextStyle( + color: ColorConstants.background, + fontSize: 18, + fontWeight: FontWeight.w900, + ), ), ), ), - ), - if (!isDisabled.value) - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: LayoutBuilder( - builder: (context, constraints) { - final shimmerWidth = constraints.maxWidth * 0.9; - - // Start completely off left, end completely off right - final startPosition = -shimmerWidth * 1.5; - final endPosition = constraints.maxWidth; - final totalDistance = endPosition - startPosition; + if (!isExpired.value) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LayoutBuilder( + builder: (context, constraints) { + final shimmerWidth = constraints.maxWidth * 0.9; + final startPosition = -shimmerWidth * 1.5; + final endPosition = constraints.maxWidth; + final totalDistance = endPosition - startPosition; - return Transform.translate( - offset: Offset( - startPosition + - (controller.value * totalDistance), - 0, - ), - child: Container( - width: shimmerWidth, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.white.withOpacity(0.0), - Colors.white.withOpacity(0.2), - Colors.white.withOpacity(0.0), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + return Transform.translate( + offset: Offset( + startPosition + + (shimmerController.value * totalDistance), + 0, + ), + child: Container( + width: shimmerWidth, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withOpacity(0.0), + Colors.white.withOpacity(0.2), + Colors.white.withOpacity(0.0), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), ), ), - ), - ); - }, + ); + }, + ), ), ), - ), - ], - ); - }, + ], + ); + }, + ), + ), + const SizedBox(height: 10), + Button.secondary( + text: localizeWithContext.paiementCancel, + onPressed: onCancel, ), - ), + ], ); } } diff --git a/lib/paiement/ui/components/paiment_delegate/feedback_overlay.dart b/lib/paiement/ui/components/paiment_delegate/feedback_overlay.dart new file mode 100644 index 0000000000..6e740cf456 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/feedback_overlay.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; + +const _teal = Color(0xff017f80); + +class FeedbackOverlay extends HookWidget { + final bool isSuccess; + const FeedbackOverlay({super.key, required this.isSuccess}); + + @override + Widget build(BuildContext context) { + final controller = useAnimationController( + duration: const Duration(milliseconds: 700), + ); + + useEffect(() { + controller.forward(); + return null; + }, [controller]); + + final scaleAnim = CurvedAnimation( + parent: controller, + curve: Curves.elasticOut, + ); + final fadeAnim = CurvedAnimation( + parent: controller, + curve: const Interval(0.0, 0.4, curve: Curves.easeOut), + ); + + final color = isSuccess ? _teal : ColorConstants.main; + + return Center( + child: FadeTransition( + opacity: fadeAnim, + child: ScaleTransition( + scale: scaleAnim, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + boxShadow: [ + BoxShadow( + color: color.withAlpha(77), + blurRadius: 20, + spreadRadius: 4, + ), + ], + ), + child: Icon( + isSuccess ? Icons.check_rounded : Icons.close_rounded, + color: Colors.white, + size: 44, + ), + ), + const SizedBox(height: 20), + Text( + isSuccess + ? AppLocalizations.of(context)!.paiementPaymentSuccessful + : AppLocalizations.of(context)!.paiementPaymentCanceled, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: color, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart index 8f847a8053..b2ac026e85 100644 --- a/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart +++ b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:titan/paiement/ui/components/paiment_delegate/account_card.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/confirm_button.dart'; import 'package:titan/paiement/ui/components/paiment_delegate/countdown_timer.dart'; -import 'package:titan/tools/providers/locale_notifier.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/feedback_overlay.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/product_card.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/wallet_balance_card.dart'; import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; -class PaimentDelegateModal extends HookConsumerWidget { +enum _ModalState { idle, loading, success, canceled } + +class PaimentDelegateModal extends HookWidget { final String itemTitle; final String itemDescription; - final ImageProvider? itemImage; final int itemPrice; final DateTime? itemExpirationDate; final VoidCallback onConfirm; @@ -17,134 +20,105 @@ class PaimentDelegateModal extends HookConsumerWidget { super.key, required this.itemTitle, required this.itemDescription, - required this.itemImage, required this.itemPrice, this.itemExpirationDate, required this.onConfirm, }); @override - Widget build(BuildContext context, WidgetRef ref) { - final locale = ref.watch(localeProvider); - - final priceFormatter = NumberFormat.currency( - locale: locale.toString(), - symbol: "€", - decimalDigits: 2, + Widget build(BuildContext context) { + final state = useState(_ModalState.idle); + final isExpired = useState(false); + final idleHeight = useState(null); + final idleKey = useMemoized(() => GlobalKey()); + final expirationDate = useMemoized( + () => DateTime.now().add(const Duration(minutes: 2)), + ); + final secondsLeft = useMemoized( + () => expirationDate.difference(DateTime.now()).inSeconds, ); - final secondsLeft = itemExpirationDate - ?.difference(DateTime.now()) - .inSeconds; + // Capture the idle content height after first layout + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final renderBox = + idleKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null && idleHeight.value == null) { + idleHeight.value = renderBox.size.height; + } + }); + return null; + }, []); + + Future onValidate() async { + if (isExpired.value || state.value != _ModalState.idle) return; + state.value = _ModalState.loading; + onConfirm(); + await Future.delayed(const Duration(milliseconds: 600)); + if (!context.mounted) return; + state.value = _ModalState.success; + await Future.delayed(const Duration(milliseconds: 1500)); + if (context.mounted) Navigator.of(context).pop(); + } + + Future onCancel() async { + if (state.value != _ModalState.idle) return; + state.value = _ModalState.canceled; + await Future.delayed(const Duration(milliseconds: 1500)); + if (context.mounted) Navigator.of(context).pop(); + } + + final showIdle = + state.value == _ModalState.idle || state.value == _ModalState.loading; return BottomModalTemplate( - title: itemTitle, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Item Image and Details Section - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.2), - ), - ), - child: Column( - children: [ - if (itemImage != null) ...[ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image( - image: itemImage!, - width: 120, - height: 120, - fit: BoxFit.cover, + title: AppLocalizations.of(context)!.paiementConfirmYourPurchase, + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + alignment: Alignment.topCenter, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: showIdle + ? SingleChildScrollView( + key: const ValueKey('idle'), + child: Column( + key: idleKey, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ProductCard( + title: itemTitle, + description: itemDescription, + priceInCents: itemPrice, ), - ), - const SizedBox(height: 16), - ], - Text( - itemDescription, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.8), - ), - ), - const SizedBox(height: 20), - // Price Tag - Container( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - Color(0xff017f80), - Color.fromARGB(255, 4, 84, 84), - ], - ), - borderRadius: BorderRadius.circular(30), - boxShadow: [ - BoxShadow( - color: const Color(0xff017f80).withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), + if (secondsLeft > 0) ...[ + const SizedBox(height: 20), + CountdownTimer( + totalSeconds: secondsLeft, + onFinished: () => isExpired.value = true, ), ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.local_offer, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 8), - Text( - priceFormatter.format(itemPrice / 100), - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ], - ), + const SizedBox(height: 20), + const WalletBalanceCard(), + const SizedBox(height: 24), + ConfirmButton( + totalSeconds: secondsLeft, + onConfirm: onValidate, + onCancel: onCancel, + ), + ], ), - ], - ), - ), - - // Countdown Timer - if (secondsLeft != null && secondsLeft > 0) ...[ - const SizedBox(height: 24), - CountdownTimer(totalSeconds: secondsLeft), - ], - - const SizedBox(height: 24), - - // Wallet Balance Card - SizedBox( - height: 250, - width: MediaQuery.of(context).size.width, - child: AccountCard( - onConfirm: onConfirm, - itemExpirationDate: itemExpirationDate, - itemPrice: itemPrice, - ), - ), - - const SizedBox(height: 24), - ], + ) + : SizedBox( + height: idleHeight.value, + child: FeedbackOverlay( + key: ValueKey(state.value), + isSuccess: state.value == _ModalState.success, + ), + ), ), ), ); diff --git a/lib/paiement/ui/components/paiment_delegate/product_card.dart b/lib/paiement/ui/components/paiment_delegate/product_card.dart new file mode 100644 index 0000000000..f627a2d887 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/product_card.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/tools/constants.dart'; + +const _teal = Color(0xff017f80); +const _tealDark = Color.fromARGB(255, 4, 84, 84); + +class ProductCard extends StatelessWidget { + final String title; + final String description; + final int priceInCents; + + const ProductCard({ + super.key, + required this.title, + required this.description, + required this.priceInCents, + }); + + @override + Widget build(BuildContext context) { + final priceFormatter = NumberFormat.currency( + locale: 'fr_FR', + symbol: '€', + decimalDigits: 2, + ); + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: ColorConstants.tertiary, + ), + ), + const SizedBox(height: 6), + Text( + description, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: ColorConstants.tertiary.withAlpha(153), + height: 1.4, + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [_teal, _tealDark]), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: _teal.withAlpha(77), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Text( + priceFormatter.format(priceInCents / 100), + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/wallet_balance_card.dart b/lib/paiement/ui/components/paiment_delegate/wallet_balance_card.dart new file mode 100644 index 0000000000..9aca7709fc --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/wallet_balance_card.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/providers/my_wallet_provider.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +const _teal = Color(0xff017f80); + +class WalletBalanceCard extends ConsumerWidget { + const WalletBalanceCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final wallet = ref.watch(myWalletProvider); + final priceFormatter = NumberFormat.currency( + locale: 'fr_FR', + symbol: '€', + decimalDigits: 2, + ); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: ListItemTemplate( + title: AppLocalizations.of(context)!.paiementYourBalance, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _teal.withAlpha(26), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.account_balance_wallet_rounded, + color: _teal, + size: 20, + ), + ), + trailing: wallet.when( + data: (w) => Text( + priceFormatter.format(w.balance / 100), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _teal, + ), + ), + loading: () => const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + error: (_, __) => const Text( + '—', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _teal, + ), + ), + ), + ), + ); + } +} diff --git a/lib/paiement/ui/pages/main_page/main_page.dart b/lib/paiement/ui/pages/main_page/main_page.dart index 2738ca2131..dae9e76d73 100644 --- a/lib/paiement/ui/pages/main_page/main_page.dart +++ b/lib/paiement/ui/pages/main_page/main_page.dart @@ -11,6 +11,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/ui/components/paiment_delegate/paiment_delegate_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'; @@ -22,6 +23,7 @@ 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}); @@ -176,6 +178,49 @@ class PaymentMainPage extends HookConsumerWidget { ); }, ), + GestureDetector( + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: PaimentDelegateModal( + itemTitle: "Premium Subscription", + itemDescription: + "Monthly access to all premium features\nand exclusive content", + itemPrice: 999, + onConfirm: () { + Navigator.of(context).pop(); + displayToast( + context, + TypeMsg.msg, + AppLocalizations.of( + context, + )!.paiementSuccededTransaction, + ); + }, + ), + ); + }, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + "Test Modal", + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), AnimatedBuilder( animation: controller, builder: (context, child) { From c22f5df8476da9a69aa1998ba3e18d2b56b8bdec Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Tue, 31 Mar 2026 22:59:44 +0200 Subject: [PATCH 3/9] feat: adding payment classes --- lib/paiement/class/payment_request.dart | 106 +++++++++++++++++++++ lib/paiement/class/request_validation.dart | 50 ++++++++++ 2 files changed, 156 insertions(+) create mode 100644 lib/paiement/class/payment_request.dart create mode 100644 lib/paiement/class/request_validation.dart diff --git a/lib/paiement/class/payment_request.dart b/lib/paiement/class/payment_request.dart new file mode 100644 index 0000000000..244e8296cb --- /dev/null +++ b/lib/paiement/class/payment_request.dart @@ -0,0 +1,106 @@ +import 'package:titan/tools/functions.dart'; + +enum RequestStatus { proposed, accepted, refused, expired } + +class PaymentRequest { + final String id; + final String walletId; + final DateTime creation; + final int total; + final String storeId; + final String name; + final String? storeNote; + final String module; + final String objectId; + final RequestStatus status; + final String? transactionId; + + PaymentRequest({ + required this.id, + required this.walletId, + required this.creation, + required this.total, + required this.storeId, + required this.name, + this.storeNote, + required this.module, + required this.objectId, + required this.status, + this.transactionId, + }); + + PaymentRequest.fromJson(Map json) + : id = json['id'], + walletId = json['wallet_id'], + creation = processDateFromAPI(json['creation']), + total = json['total'], + storeId = json['store_id'], + name = json['name'], + storeNote = json['store_note'], + module = json['module'], + objectId = json['object_id'], + status = RequestStatus.values.firstWhere( + (e) => e.toString().split('.').last == json['status'], + ), + transactionId = json['transaction_id']; + + Map toJson() => { + 'id': id, + 'wallet_id': walletId, + 'creation': processDateToAPI(creation), + 'total': total, + 'store_id': storeId, + 'name': name, + 'store_note': storeNote, + 'module': module, + 'object_id': objectId, + 'status': status.toString().split('.').last, + 'transaction_id': transactionId, + }; + + @override + String toString() { + return 'PaymentRequest {id: $id, walletId: $walletId, creation: $creation, total: $total, storeId: $storeId, name: $name, status: $status}'; + } + + PaymentRequest.empty() + : id = '', + walletId = '', + creation = DateTime.now(), + total = 0, + storeId = '', + name = '', + storeNote = null, + module = '', + objectId = '', + status = RequestStatus.proposed, + transactionId = null; + + PaymentRequest copyWith({ + String? id, + String? walletId, + DateTime? creation, + int? total, + String? storeId, + String? name, + String? storeNote, + String? module, + String? objectId, + RequestStatus? status, + String? transactionId, + }) { + return PaymentRequest( + id: id ?? this.id, + walletId: walletId ?? this.walletId, + creation: creation ?? this.creation, + total: total ?? this.total, + storeId: storeId ?? this.storeId, + name: name ?? this.name, + storeNote: storeNote ?? this.storeNote, + module: module ?? this.module, + objectId: objectId ?? this.objectId, + status: status ?? this.status, + transactionId: transactionId ?? this.transactionId, + ); + } +} diff --git a/lib/paiement/class/request_validation.dart b/lib/paiement/class/request_validation.dart new file mode 100644 index 0000000000..5bb27c2cfd --- /dev/null +++ b/lib/paiement/class/request_validation.dart @@ -0,0 +1,50 @@ +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}'; + } +} From 72d5067e65f1c1c9302abfbe9c2cd1dded8bb512 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Tue, 31 Mar 2026 22:59:59 +0200 Subject: [PATCH 4/9] feat(repository): adding payment repo --- .../repositories/requests_repository.dart | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/paiement/repositories/requests_repository.dart diff --git a/lib/paiement/repositories/requests_repository.dart b/lib/paiement/repositories/requests_repository.dart new file mode 100644 index 0000000000..9706d7d578 --- /dev/null +++ b/lib/paiement/repositories/requests_repository.dart @@ -0,0 +1,37 @@ +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/tools/repository/repository.dart'; + +class RequestsRepository extends Repository { + @override + // ignore: overridden_fields + final ext = 'mypayment/'; + + Future> getRequests() async { + return List.from( + (await getList(suffix: 'requests')).map( + (e) => PaymentRequest.fromJson(e), + ), + ); + } + + Future acceptRequest( + String requestId, + RequestValidation validation, + ) async { + await create(validation.toJson(), suffix: 'requests/$requestId/accept'); + return true; + } + + Future refuseRequest(String requestId) async { + await create({}, suffix: 'requests/$requestId/refuse'); + return true; + } +} + +final requestsRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return RequestsRepository()..setToken(token); +}); From e408f251855ad059e344aa236c7398ad5e0b7ddc Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Tue, 31 Mar 2026 23:01:35 +0200 Subject: [PATCH 5/9] feat(provider): adding request provider --- .../providers/payment_requests_provider.dart | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 lib/paiement/providers/payment_requests_provider.dart diff --git a/lib/paiement/providers/payment_requests_provider.dart b/lib/paiement/providers/payment_requests_provider.dart new file mode 100644 index 0000000000..218b3fd966 --- /dev/null +++ b/lib/paiement/providers/payment_requests_provider.dart @@ -0,0 +1,47 @@ +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/repositories/requests_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; + +class PaymentRequestsNotifier extends ListNotifier { + final RequestsRepository requestsRepository; + PaymentRequestsNotifier({required this.requestsRepository}) + : super(const AsyncValue.loading()); + + Future>> getRequests() async { + return await loadList(requestsRepository.getRequests); + } + + Future acceptRequest( + PaymentRequest request, + RequestValidation validation, + ) async { + return await update( + (_) => requestsRepository.acceptRequest(request.id, validation), + (requests, request) => + requests + ..[requests.indexWhere((r) => r.id == request.id)] = request, + request.copyWith(status: RequestStatus.accepted), + ); + } + + Future refuseRequest(PaymentRequest request) async { + return await update( + (_) => requestsRepository.refuseRequest(request.id), + (requests, request) => + requests + ..[requests.indexWhere((r) => r.id == request.id)] = request, + request.copyWith(status: RequestStatus.refused), + ); + } +} + +final paymentRequestsProvider = StateNotifierProvider< + PaymentRequestsNotifier, + AsyncValue> +>((ref) { + final requestsRepository = ref.watch(requestsRepositoryProvider); + return PaymentRequestsNotifier(requestsRepository: requestsRepository) + ..getRequests(); +}); From fee058a4664e24f5aaa5bd0971eb5382c51e45d9 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Tue, 31 Mar 2026 23:01:55 +0200 Subject: [PATCH 6/9] feat: adding api call to the payment modal --- .../paiment_delegate/paiment_delegate_modal.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart index b2ac026e85..c6427d9103 100644 --- a/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart +++ b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart @@ -16,6 +16,7 @@ class PaimentDelegateModal extends HookWidget { final int itemPrice; final DateTime? itemExpirationDate; final VoidCallback onConfirm; + final VoidCallback? onRefuse; const PaimentDelegateModal({ super.key, required this.itemTitle, @@ -23,6 +24,7 @@ class PaimentDelegateModal extends HookWidget { required this.itemPrice, this.itemExpirationDate, required this.onConfirm, + this.onRefuse, }); @override @@ -63,6 +65,11 @@ class PaimentDelegateModal extends HookWidget { Future onCancel() async { if (state.value != _ModalState.idle) return; + if (onRefuse != null) { + state.value = _ModalState.loading; + onRefuse!(); + return; + } state.value = _ModalState.canceled; await Future.delayed(const Duration(milliseconds: 1500)); if (context.mounted) Navigator.of(context).pop(); From 0d37dfddcda94c5187176ec3b43cace6c5f7532e Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Tue, 31 Mar 2026 23:02:16 +0200 Subject: [PATCH 7/9] feat: triggering modal when a request exists --- .../ui/pages/main_page/main_page.dart | 159 +++++++++++++----- 1 file changed, 116 insertions(+), 43 deletions(-) diff --git a/lib/paiement/ui/pages/main_page/main_page.dart b/lib/paiement/ui/pages/main_page/main_page.dart index dae9e76d73..9f488a4cee 100644 --- a/lib/paiement/ui/pages/main_page/main_page.dart +++ b/lib/paiement/ui/pages/main_page/main_page.dart @@ -1,16 +1,22 @@ +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/providers/has_accepted_tos_provider.dart'; import 'package:titan/paiement/providers/my_wallet_provider.dart'; +import 'package:titan/paiement/providers/payment_requests_provider.dart'; import 'package:titan/paiement/providers/tos_provider.dart'; import 'package:titan/paiement/providers/is_payment_admin.dart'; 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/pages/main_page/account_card/account_card.dart'; import 'package:titan/paiement/ui/pages/main_page/tos_dialog.dart'; @@ -50,6 +56,11 @@ class PaymentMainPage extends HookConsumerWidget { final myWalletNotifier = ref.read(myWalletProvider.notifier); final isAdmin = ref.watch(isStructureAdminProvider); final flipped = useState(true); + final paymentRequests = ref.watch(paymentRequestsProvider); + final paymentRequestsNotifier = ref.read( + paymentRequestsProvider.notifier, + ); + final hasShownRequestModal = useState(false); ref.listen(pathForwardingProvider, (previous, next) async { final params = next.queryParameters; @@ -86,6 +97,109 @@ class PaymentMainPage extends HookConsumerWidget { } } + Future showRequestModal(PaymentRequest request) async { + final keyService = KeyService(); + 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 = 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, + ); + 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, + ); + } + }, + ), + ); + } + + useEffect(() { + paymentRequests.whenData((requests) { + final pendingRequests = requests + .where((r) => r.status == RequestStatus.proposed) + .toList(); + if (pendingRequests.isNotEmpty && !hasShownRequestModal.value) { + hasShownRequestModal.value = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + showRequestModal(pendingRequests.first); + }); + } + }); + return null; + }, [paymentRequests]); + tos.maybeWhen( orElse: () {}, error: (e, s) async { @@ -147,6 +261,8 @@ class PaymentMainPage extends HookConsumerWidget { await myHistoryNotifier.getHistory(); await myWalletNotifier.getMyWallet(); await tosNotifier.getTOS(); + hasShownRequestModal.value = false; + await paymentRequestsNotifier.getRequests(); }, child: Column( children: [ @@ -178,49 +294,6 @@ class PaymentMainPage extends HookConsumerWidget { ); }, ), - GestureDetector( - onTap: () async { - await showCustomBottomModal( - context: context, - ref: ref, - modal: PaimentDelegateModal( - itemTitle: "Premium Subscription", - itemDescription: - "Monthly access to all premium features\nand exclusive content", - itemPrice: 999, - onConfirm: () { - Navigator.of(context).pop(); - displayToast( - context, - TypeMsg.msg, - AppLocalizations.of( - context, - )!.paiementSuccededTransaction, - ); - }, - ), - ); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 20), - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - "Test Modal", - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), AnimatedBuilder( animation: controller, builder: (context, child) { From 7a71393ea234c12ee386965929c71f3707017055 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Tue, 31 Mar 2026 23:02:38 +0200 Subject: [PATCH 8/9] feat(trads): adding translation for the modal --- lib/l10n/app_en.arb | 6 ++++++ lib/l10n/app_fr.arb | 6 ++++++ lib/l10n/app_localizations.dart | 30 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_en.dart | 15 +++++++++++++++ lib/l10n/app_localizations_fr.dart | 16 ++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2836e01cc7..08a18c67c0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -962,6 +962,12 @@ "paiementYourBalance": "Your balance", "paiementPaymentSuccessful": "Payment successful!", "paiementPaymentCanceled": "Payment canceled", + "paiementPaymentRequest": "Payment request", + "paiementPaymentRequestAccepted": "Payment request accepted", + "paiementPaymentRequestRefused": "Payment request refused", + "paiementPaymentRequestError": "Error processing payment request", + "paiementAccept": "Accept", + "paiementRefuse": "Refuse", "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 4781162b27..2100ec00ca 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -970,6 +970,12 @@ "paiementYourBalance": "Votre solde", "paiementPaymentSuccessful": "Paiement réussi !", "paiementPaymentCanceled": "Paiement annulé", + "paiementPaymentRequest": "Demande de paiement", + "paiementPaymentRequestAccepted": "Demande de paiement acceptée", + "paiementPaymentRequestRefused": "Demande de paiement refusée", + "paiementPaymentRequestError": "Erreur lors du traitement de la demande", + "paiementAccept": "Accepter", + "paiementRefuse": "Refuser", "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 def8112230..afb31aa700 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5216,6 +5216,36 @@ abstract class AppLocalizations { /// **'Paiement annulé'** String get paiementPaymentCanceled; + /// No description provided for @paiementPaymentRequest. + /// + /// In fr, this message translates to: + /// **'Demande de paiement'** + String get paiementPaymentRequest; + + /// No description provided for @paiementPaymentRequestAccepted. + /// + /// In fr, this message translates to: + /// **'Demande de paiement acceptée'** + String get paiementPaymentRequestAccepted; + + /// No description provided for @paiementPaymentRequestRefused. + /// + /// In fr, this message translates to: + /// **'Demande de paiement refusée'** + String get paiementPaymentRequestRefused; + + /// No description provided for @paiementPaymentRequestError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du traitement de la demande'** + String get paiementPaymentRequestError; + + /// No description provided for @paiementRefuse. + /// + /// In fr, this message translates to: + /// **'Refuser'** + String get paiementRefuse; + /// 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 4f4a7dd46a..601a6c0542 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2654,6 +2654,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get paiementPaymentCanceled => 'Payment canceled'; + @override + String get paiementPaymentRequest => 'Payment request'; + + @override + String get paiementPaymentRequestAccepted => 'Payment request accepted'; + + @override + String get paiementPaymentRequestRefused => 'Payment request refused'; + + @override + String get paiementPaymentRequestError => 'Error processing payment request'; + + @override + String get paiementRefuse => 'Refuse'; + @override String get paiementSuccessfullyAddedStore => 'Store successfully added'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 5ddf3b0e84..54b359d2b0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2683,6 +2683,22 @@ class AppLocalizationsFr extends AppLocalizations { @override String get paiementPaymentCanceled => 'Paiement annulé'; + @override + String get paiementPaymentRequest => 'Demande de paiement'; + + @override + String get paiementPaymentRequestAccepted => 'Demande de paiement acceptée'; + + @override + String get paiementPaymentRequestRefused => 'Demande de paiement refusée'; + + @override + String get paiementPaymentRequestError => + 'Erreur lors du traitement de la demande'; + + @override + String get paiementRefuse => 'Refuser'; + @override String get paiementSuccessfullyAddedStore => 'Magasin ajoutée avec succès'; From f5dbe47a25a36edb55f4837312df90060c6f7638 Mon Sep 17 00:00:00 2001 From: maximeroucher Date: Mon, 6 Apr 2026 11:56:24 +0200 Subject: [PATCH 9/9] fix: format --- lib/paiement/class/request_validation.dart | 5 +--- .../providers/payment_requests_provider.dart | 23 +++++++++---------- .../repositories/requests_repository.dart | 6 ++--- .../ui/pages/main_page/main_page.dart | 12 +++------- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/lib/paiement/class/request_validation.dart b/lib/paiement/class/request_validation.dart index 5bb27c2cfd..646e7ac0cf 100644 --- a/lib/paiement/class/request_validation.dart +++ b/lib/paiement/class/request_validation.dart @@ -38,10 +38,7 @@ class RequestValidation extends RequestValidationData { }); @override - Map toJson() => { - ...super.toJson(), - 'signature': signature, - }; + Map toJson() => {...super.toJson(), 'signature': signature}; @override String toString() { diff --git a/lib/paiement/providers/payment_requests_provider.dart b/lib/paiement/providers/payment_requests_provider.dart index 218b3fd966..c214344187 100644 --- a/lib/paiement/providers/payment_requests_provider.dart +++ b/lib/paiement/providers/payment_requests_provider.dart @@ -20,8 +20,7 @@ class PaymentRequestsNotifier extends ListNotifier { return await update( (_) => requestsRepository.acceptRequest(request.id, validation), (requests, request) => - requests - ..[requests.indexWhere((r) => r.id == request.id)] = request, + requests..[requests.indexWhere((r) => r.id == request.id)] = request, request.copyWith(status: RequestStatus.accepted), ); } @@ -30,18 +29,18 @@ class PaymentRequestsNotifier extends ListNotifier { return await update( (_) => requestsRepository.refuseRequest(request.id), (requests, request) => - requests - ..[requests.indexWhere((r) => r.id == request.id)] = request, + requests..[requests.indexWhere((r) => r.id == request.id)] = request, request.copyWith(status: RequestStatus.refused), ); } } -final paymentRequestsProvider = StateNotifierProvider< - PaymentRequestsNotifier, - AsyncValue> ->((ref) { - final requestsRepository = ref.watch(requestsRepositoryProvider); - return PaymentRequestsNotifier(requestsRepository: requestsRepository) - ..getRequests(); -}); +final paymentRequestsProvider = + StateNotifierProvider< + PaymentRequestsNotifier, + AsyncValue> + >((ref) { + final requestsRepository = ref.watch(requestsRepositoryProvider); + return PaymentRequestsNotifier(requestsRepository: requestsRepository) + ..getRequests(); + }); diff --git a/lib/paiement/repositories/requests_repository.dart b/lib/paiement/repositories/requests_repository.dart index 9706d7d578..812cbcade0 100644 --- a/lib/paiement/repositories/requests_repository.dart +++ b/lib/paiement/repositories/requests_repository.dart @@ -11,9 +11,9 @@ class RequestsRepository extends Repository { Future> getRequests() async { return List.from( - (await getList(suffix: 'requests')).map( - (e) => PaymentRequest.fromJson(e), - ), + (await getList( + suffix: 'requests', + )).map((e) => PaymentRequest.fromJson(e)), ); } diff --git a/lib/paiement/ui/pages/main_page/main_page.dart b/lib/paiement/ui/pages/main_page/main_page.dart index 9f488a4cee..569b07504c 100644 --- a/lib/paiement/ui/pages/main_page/main_page.dart +++ b/lib/paiement/ui/pages/main_page/main_page.dart @@ -57,9 +57,7 @@ class PaymentMainPage extends HookConsumerWidget { final isAdmin = ref.watch(isStructureAdminProvider); final flipped = useState(true); final paymentRequests = ref.watch(paymentRequestsProvider); - final paymentRequestsNotifier = ref.read( - paymentRequestsProvider.notifier, - ); + final paymentRequestsNotifier = ref.read(paymentRequestsProvider.notifier); final hasShownRequestModal = useState(false); ref.listen(pathForwardingProvider, (previous, next) async { @@ -152,9 +150,7 @@ class PaymentMainPage extends HookConsumerWidget { ? AppLocalizations.of( context, )!.paiementPaymentRequestAccepted - : AppLocalizations.of( - context, - )!.paiementPaymentRequestError, + : AppLocalizations.of(context)!.paiementPaymentRequestError, ); if (success) { await myHistoryNotifier.getHistory(); @@ -175,9 +171,7 @@ class PaymentMainPage extends HookConsumerWidget { ? AppLocalizations.of( context, )!.paiementPaymentRequestRefused - : AppLocalizations.of( - context, - )!.paiementPaymentRequestError, + : AppLocalizations.of(context)!.paiementPaymentRequestError, ); } },