From e7c6bae34a3c38932546bbd06129677ed3337b29 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Fri, 27 Feb 2026 17:31:23 +0000 Subject: [PATCH 01/27] Refactor project structure by renaming 'moosyl' to 'moosyl_flutter' in pubspec files, update dependencies, and enhance localization strings. Remove unused fetcher and automatic payment page files to streamline the codebase. --- example/ios/Podfile.lock | 9 +- example/lib/main.dart | 28 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 + example/macos/Podfile | 2 +- example/macos/Podfile.lock | 4 +- .../macos/Runner.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + example/macos/Runner/AppDelegate.swift | 6 +- example/pubspec.lock | 118 +++- example/pubspec.yaml | 2 +- lib/l10n/ar.arb | 30 +- lib/l10n/en.arb | 32 +- lib/l10n/fr.arb | 30 +- lib/l10n/generated/moosyl_localization.dart | 148 +++- .../generated/moosyl_localization_ar.dart | 79 ++- .../generated/moosyl_localization_en.dart | 82 ++- .../generated/moosyl_localization_fr.dart | 87 ++- .../exception_handling/exception_mapper.dart | 4 +- lib/src/helpers/fetcher.dart | 200 ------ lib/src/helpers/validators.dart | 2 +- lib/src/models/payment_method_model.dart | 107 +-- lib/src/pages/automatic_pay_page.dart | 444 ------------ lib/src/pages/available_method_payments.dart | 99 --- lib/src/pages/bankily_view.dart | 230 ++++++ lib/src/pages/home.dart | 12 +- lib/src/pages/masrivi_view.dart | 106 +++ lib/src/pages/moosyl_view.dart | 67 +- lib/src/pages/payment_methods_view.dart | 658 ++++++++++++++++++ lib/src/pages/sedad_view.dart | 177 +++++ .../get_payment_methods_provider.dart | 123 +++- lib/src/providers/pay_provider.dart | 16 +- .../services/get_payment_methods_service.dart | 57 +- .../services/get_payment_request_service.dart | 46 +- lib/src/services/pay_service.dart | 78 +-- lib/src/widgets/buttons.dart | 79 ++- lib/src/widgets/error_widget.dart | 4 +- lib/src/widgets/feedback.dart | 4 +- lib/src/widgets/icons.dart | 17 +- lib/src/widgets/pick_image.dart | 154 ---- lib/src/widgets/text_input.dart | 29 +- pubspec.yaml | 4 +- 41 files changed, 2151 insertions(+), 1232 deletions(-) delete mode 100644 lib/src/helpers/fetcher.dart delete mode 100644 lib/src/pages/automatic_pay_page.dart delete mode 100644 lib/src/pages/available_method_payments.dart create mode 100644 lib/src/pages/bankily_view.dart create mode 100644 lib/src/pages/masrivi_view.dart create mode 100644 lib/src/pages/payment_methods_view.dart create mode 100644 lib/src/pages/sedad_view.dart delete mode 100644 lib/src/widgets/pick_image.dart diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9e90980..bbfb45c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -38,10 +38,14 @@ PODS: - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) - SwiftyGif (5.4.5) + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: @@ -55,14 +59,17 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 diff --git a/example/lib/main.dart b/example/lib/main.dart index 4366505..54fd3ab 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,7 @@ // ignore_for_file: avoid_print import 'package:flutter/material.dart'; -import 'package:moosyl/moosyl.dart'; +import 'package:moosyl_flutter/moosyl.dart'; void main() { runApp(const MyApp()); @@ -48,20 +48,18 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), - body: MoosylView( - publishableApiKey: 'YOUR_PUBLISHABLE_API_KEY', - transactionId: 'TRANSACTION_ID', - organizationLogo: const Text('Moosyl'), - fullPage: false, - onPaymentSuccess: () { - print('Payment was successful!'); - }, - ), + return MoosylView( + publishableApiKey: 'your publishable api key', + transactionId: 'your transaction id', + organizationLogo: const Text('Moosyl'), + isTestingMode: true, + primaryColor: Colors.red, + onBackPress: () => print('Back pressed'), + // amountToPay: 5, + // tax: 5, + onPaymentSuccess: () { + print('Payment was successful!'); + }, ); } } diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..bf2a519 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/example/macos/Podfile b/example/macos/Podfile index c795730..b52666a 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 44d4097..9d359e1 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -9,8 +9,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral SPEC CHECKSUMS: - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 781bc53..a52fd79 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -621,7 +621,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -671,7 +671,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15368ec..ac78810 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index d53ef64..b3c1761 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/example/pubspec.lock b/example/pubspec.lock index 3431711..7e3f72e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -17,6 +17,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + url: "https://pub.dev" + source: hosted + version: "8.12.4" characters: dependency: transitive description: @@ -57,6 +73,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dio: + dependency: transitive + description: + name: dio + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 + url: "https://pub.dev" + source: hosted + version: "5.9.1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: @@ -81,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -201,10 +241,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -222,12 +262,20 @@ packages: source: hosted version: "3.0.0" moosyl: + dependency: transitive + description: + name: moosyl + sha256: d20d5bd50de03cf89c821b63bdc948424794624424ed786e5ab54bf653faaf7e + url: "https://pub.dev" + source: hosted + version: "1.1.1" + moosyl_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.0.10" + version: "1.0.12" nested: dependency: transitive description: @@ -236,6 +284,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + one_of: + dependency: transitive + description: + name: one_of + sha256: "25fe0fcf181e761c6fcd604caf9d5fdf952321be17584ba81c72c06bdaa511f0" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + one_of_serializer: + dependency: transitive + description: + name: one_of_serializer + sha256: "3f3dfb5c1578ba3afef1cb47fcc49e585e797af3f2b6c2cc7ed90aad0c5e7b83" + url: "https://pub.dev" + source: hosted + version: "1.5.0" path: dependency: transitive description: @@ -260,6 +324,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.2" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" sky_engine: dependency: transitive description: flutter @@ -309,10 +381,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" typed_data: dependency: transitive description: @@ -345,6 +417,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.dev" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" + url: "https://pub.dev" + source: hosted + version: "4.10.13" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "108bd85d0ff20bff1e8b52a040f5c19b6b9fc4a78fdf3160534ff5a11a82e267" + url: "https://pub.dev" + source: hosted + version: "3.23.7" win32: dependency: transitive description: @@ -354,5 +458,5 @@ packages: source: hosted version: "5.9.0" sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ad9277b..fe9121b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -30,7 +30,7 @@ environment: dependencies: flutter: sdk: flutter - moosyl: + moosyl_flutter: path: ../ # The following adds the Cupertino Icons font to your application. diff --git a/lib/l10n/ar.arb b/lib/l10n/ar.arb index 1ef963b..bc208f6 100644 --- a/lib/l10n/ar.arb +++ b/lib/l10n/ar.arb @@ -13,13 +13,14 @@ "copyTheCodeBPayAndHeadToBankilyToPayTheAmount": "انسخ الرمز، BPay وتوجه إلى بنكيلي لدفع المبلغ", "afterPayment": "بعد الدفع", "afterMakingThePaymentFillTheFollowingInformation": "بعد إتمام الدفع، املأ المعلومات التالية", - "enterYourBankilyPhoneNumber": "أدخل رقم هاتفك الخاص ببنكيلي", + "enterYourBankilyPhoneNumber": "أدخل رقم هاتفك", "bankilyPhoneNumber": "رقم هاتف بنكيلي", "paymentPassCode": "رمز المرور للدفع", - "paymentPassCodeFromBankily": "رمز المرور للدفع من بنكيلي", - "sendForVerification": "أرسل للتحقق", "codeBPay": "رمز BPay", "amountToPay": "المبلغ الواجب دفعه", + "choosePaymentMethods": "اختر طريقة الدفع", + "tax": "الضريبة", + "pay": "ادفع", "retry": "إعادة المحاولة", "upload": "رفع", "capture": "التقاط", @@ -47,5 +48,26 @@ "phoneNumberRequired": "رقم الهاتف مطلوب", "validMauritanianNumber": "أدخل رقم هاتف موريتاني صالح", "codeRequired": "الرمز مطلوب", - "validDigitCode": "أدخل رمزًا صالحًا مكونًا من 4 أرقام" + "validDigitCode": "أدخل رمزًا صالحًا مكونًا من 4 أرقام", + "totalAmount": "المبلغ الإجمالي", + "paymentSummary": "ملخص الدفع", + "paymentCode": "رمز الدفع", + "ivePaid": "لقد دفعت", + "paymentRequestFullyPaid":"تم دفع طلب الدفع هذا بالكامل.", +"amountToPayShouldMatchPaymentRequest":"يجب أن يتطابق المبلغ المطلوب دفعه مع مبلغ طلب الدفع.", +"usePaymentCodeAboveToComplete": "استخدم رمز الدفع أعلاه لإكمال الدفع في تطبيق سداد، ثم اضغط على الزر أدناه.", + "selected": "محدد", + "tapToUse": "اضغط للاستخدام", + "chooseHowYouWouldLikeToPay": "اختر كيف تريد الدفع", + "useThisCodeToCompletePayment": "استخدم هذا الرمز لإكمال الدفع", + "sedadStep1": "1. افتح تطبيق سداد", + "sedadStep2": "2. انتقل إلى الدفع", + "sedadStep3": "3. الصق أو أدخل الرمز لتأكيد الدفع", + "sedadStep4": "4. ارجع إلى هذه الصفحة وانقر على \"لقد دفعت\"", + "changePaymentMethod": "تغيير طريقة الدفع", + "clickToCopy": "انقر للنسخ", + "bankilyStep1": "1. افتح تطبيق بنكيلي", + "bankilyStep2": "2. انتقل إلى BPay", + "bankilyStep3": "3. أدخل رمز BPay والمبلغ المطلوب دفعه", + "bankilyStep4": "4. ارجع هنا، أدخل رقمك ورمز المرور، ثم انقر على ادفع" } diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index 5c2204d..81f0d57 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -13,13 +13,14 @@ "copyTheCodeBPayAndHeadToBankilyToPayTheAmount": "Copy the code, BPay and head to Bankily to pay the amount", "afterPayment": "After payment", "afterMakingThePaymentFillTheFollowingInformation": "After making the payment, fill the following information", - "enterYourBankilyPhoneNumber": "Enter your Bankily phone number", + "enterYourBankilyPhoneNumber": "Enter phone number", "bankilyPhoneNumber": "Bankily Phone Number", - "paymentPassCode": "Payment Pass Code", - "paymentPassCodeFromBankily": "Payment Pass Code from Bankily", - "sendForVerification": "Send for verification", + "paymentPassCode": "Payment Code", "codeBPay": "Code BPay", "amountToPay": "Amount to pay", + "choosePaymentMethods": "Choose payment method", + "tax": "Tax", + "pay": "Pay", "retry": "Retry", "upload": "Upload", "capture": "Capture", @@ -47,5 +48,26 @@ "phoneNumberRequired": "Phone number is required", "validMauritanianNumber": "Enter a valid Mauritanian phone number", "codeRequired": "Code is required", - "validDigitCode": "Enter a valid 4-digit code" + "validDigitCode": "Enter a valid 4-digit code", + "totalAmount": "Total Amount", + "paymentSummary": "Payment Summary", + "paymentCode": "Payment Code", + "ivePaid": "I've paid", + "usePaymentCodeAboveToComplete": "Use the payment code above to complete your payment in the Sedad app, then tap the button below.", + "paymentRequestFullyPaid": "This payment request has fully paid.", + "amountToPayShouldMatchPaymentRequest": "Amount to pay should match payment request amount.", + "selected": "Selected", + "tapToUse": "Tap to use", + "chooseHowYouWouldLikeToPay": "Choose how you would like to pay", + "useThisCodeToCompletePayment": "Use this code to complete payment", + "sedadStep1": "1. Open the Sedad app", + "sedadStep2": "2. Go to payment", + "sedadStep3": "3. Paste or enter the code to confirm payment", + "sedadStep4": "4. Return to this page and click \"I've paid\"", + "changePaymentMethod": "Change payment method", + "clickToCopy": "Click to copy", + "bankilyStep1": "1. Open the Bankily app", + "bankilyStep2": "2. Go to BPay", + "bankilyStep3": "3. Enter the BPay code and amount to pay", + "bankilyStep4": "4. Return here, enter your phone and passcode, then click Pay" } diff --git a/lib/l10n/fr.arb b/lib/l10n/fr.arb index 85b7473..9a7a851 100644 --- a/lib/l10n/fr.arb +++ b/lib/l10n/fr.arb @@ -12,14 +12,15 @@ "copyTheCodeBPayAndHeadToBankilyToPayTheAmount": "Copiez le code, BPay et dirigez-vous vers Bankily pour payer le montant", "afterPayment": "Après paiement", "afterMakingThePaymentFillTheFollowingInformation": "Après avoir effectué le paiement, remplissez les informations suivantes", - "enterYourBankilyPhoneNumber": "Entrez votre numéro de téléphone Bankily", + "enterYourBankilyPhoneNumber": "Entrez votre numéro", "bankilyPhoneNumber": "Numéro de téléphone Bankily", "paymentPassCode": "Code de passe de paiement", - "paymentPassCodeFromBankily": "Code de passe de paiement de Bankily", - "sendForVerification": "Envoyer pour vérification", "payUsing": "Payer avec {method}", "codeBPay": "Code BPay", "amountToPay": "Montant à payer", + "choosePaymentMethods": "Choisir le mode de paiement", + "tax": "Taxe", + "pay": "Payer", "retry": "Réessayer", "upload": "Télécharger", "capture": "Capturer", @@ -47,5 +48,26 @@ "phoneNumberRequired": "Le numéro de téléphone est obligatoire", "validMauritanianNumber": "Entrez un numéro de téléphone mauritanien valide", "codeRequired": "Le code est obligatoire", - "validDigitCode": "Entrez un code à 4 chiffres valide" + "validDigitCode": "Entrez un code à 4 chiffres valide", + "totalAmount": "Montant total", + "paymentSummary": "Résumé du paiement", + "paymentCode": "Code de paiement", + "ivePaid": "J'ai payé", + "paymentRequestFullyPaid":"Cette demande de paiement est entièrement payée.", +"amountToPayShouldMatchPaymentRequest":"Le montant à payer doit correspondre au montant de la demande de paiement.", +"usePaymentCodeAboveToComplete": "Utilisez le code de paiement ci-dessus pour finaliser votre paiement dans l'application Sedad, puis appuyez sur le bouton ci-dessous.", + "selected": "Sélectionné", + "tapToUse": "Appuyez pour utiliser", + "chooseHowYouWouldLikeToPay": "Choisissez comment vous souhaitez payer", + "useThisCodeToCompletePayment": "Utilisez ce code pour finaliser le paiement", + "sedadStep1": "1. Ouvrez l'application Sedad", + "sedadStep2": "2. Allez dans paiement", + "sedadStep3": "3. Collez ou saisissez le code pour confirmer le paiement", + "sedadStep4": "4. Revenez sur cette page et cliquez sur \"J'ai payé\"", + "changePaymentMethod": "Changer de méthode de paiement", + "clickToCopy": "Cliquez pour copier", + "bankilyStep1": "1. Ouvrez l'application Bankily", + "bankilyStep2": "2. Allez dans BPay", + "bankilyStep3": "3. Saisissez le code BPay et le montant à payer", + "bankilyStep4": "4. Revenez ici, entrez votre numéro et code, puis cliquez sur Payer" } diff --git a/lib/l10n/generated/moosyl_localization.dart b/lib/l10n/generated/moosyl_localization.dart index a47c4fc..b948819 100644 --- a/lib/l10n/generated/moosyl_localization.dart +++ b/lib/l10n/generated/moosyl_localization.dart @@ -181,7 +181,7 @@ abstract class MoosylLocalization { /// No description provided for @enterYourBankilyPhoneNumber. /// /// In en, this message translates to: - /// **'Enter your Bankily phone number'** + /// **'Enter phone number'** String get enterYourBankilyPhoneNumber; /// No description provided for @bankilyPhoneNumber. @@ -193,21 +193,9 @@ abstract class MoosylLocalization { /// No description provided for @paymentPassCode. /// /// In en, this message translates to: - /// **'Payment Pass Code'** + /// **'Payment Code'** String get paymentPassCode; - /// No description provided for @paymentPassCodeFromBankily. - /// - /// In en, this message translates to: - /// **'Payment Pass Code from Bankily'** - String get paymentPassCodeFromBankily; - - /// No description provided for @sendForVerification. - /// - /// In en, this message translates to: - /// **'Send for verification'** - String get sendForVerification; - /// No description provided for @codeBPay. /// /// In en, this message translates to: @@ -220,6 +208,24 @@ abstract class MoosylLocalization { /// **'Amount to pay'** String get amountToPay; + /// No description provided for @choosePaymentMethods. + /// + /// In en, this message translates to: + /// **'Choose payment method'** + String get choosePaymentMethods; + + /// No description provided for @tax. + /// + /// In en, this message translates to: + /// **'Tax'** + String get tax; + + /// No description provided for @pay. + /// + /// In en, this message translates to: + /// **'Pay'** + String get pay; + /// No description provided for @retry. /// /// In en, this message translates to: @@ -388,6 +394,120 @@ abstract class MoosylLocalization { /// In en, this message translates to: /// **'Enter a valid 4-digit code'** String get validDigitCode; + + /// No description provided for @totalAmount. + /// + /// In en, this message translates to: + /// **'Total Amount'** + String get totalAmount; + + /// No description provided for @paymentSummary. + /// + /// In en, this message translates to: + /// **'Payment Summary'** + String get paymentSummary; + + /// No description provided for @paymentCode. + /// + /// In en, this message translates to: + /// **'Payment Code'** + String get paymentCode; + + /// No description provided for @ivePaid. + /// + /// In en, this message translates to: + /// **'I\'ve paid'** + String get ivePaid; + + /// No description provided for @usePaymentCodeAboveToComplete. + /// + /// In en, this message translates to: + /// **'Use the payment code above to complete your payment in the Sedad app, then tap the button below.'** + String get usePaymentCodeAboveToComplete; + + /// No description provided for @paymentRequestFullyPaid. + /// + /// In en, this message translates to: + /// **'This payment request has fully paid.'** + String get paymentRequestFullyPaid; + + /// No description provided for @amountToPayShouldMatchPaymentRequest. + /// + /// In en, this message translates to: + /// **'Amount to pay should match payment request amount.'** + String get amountToPayShouldMatchPaymentRequest; + + /// No description provided for @selected. + /// + /// In en, this message translates to: + /// **'Selected'** + String get selected; + + /// No description provided for @tapToUse. + /// + /// In en, this message translates to: + /// **'Tap to use'** + String get tapToUse; + + /// No description provided for @chooseHowYouWouldLikeToPay. + /// + /// In en, this message translates to: + /// **'Choose how you would like to pay'** + String get chooseHowYouWouldLikeToPay; + + /// No description provided for @useThisCodeToCompletePayment. + /// + /// In en, this message translates to: + /// **'Use this code to complete payment'** + String get useThisCodeToCompletePayment; + + /// No description provided for @sedadStep1. + /// + /// In en, this message translates to: + /// **'1. Open the Sedad app'** + String get sedadStep1; + + /// No description provided for @sedadStep2. + /// + /// In en, this message translates to: + /// **'2. Go to payment'** + String get sedadStep2; + + /// No description provided for @sedadStep3. + /// + /// In en, this message translates to: + /// **'3. Paste or enter the code to confirm payment'** + String get sedadStep3; + + /// No description provided for @sedadStep4. + /// + /// In en, this message translates to: + /// **'4. Return to this page and click \"I\'ve paid\"'** + String get sedadStep4; + + /// No description provided for @changePaymentMethod. + /// + /// In en, this message translates to: + /// **'Change payment method'** + String get changePaymentMethod; + + /// No description provided for @clickToCopy. + /// + /// In en, this message translates to: + /// **'Click to copy'** + String get clickToCopy; + + /// No description provided for @bankilyStep1. + String get bankilyStep1; + + /// No description provided for @bankilyStep2. + String get bankilyStep2; + + /// No description provided for @bankilyStep3. + String get bankilyStep3; + + /// No description provided for @bankilyStep4. + String get bankilyStep4; } class _MoosylLocalizationDelegate diff --git a/lib/l10n/generated/moosyl_localization_ar.dart b/lib/l10n/generated/moosyl_localization_ar.dart index f38091a..00f49bc 100644 --- a/lib/l10n/generated/moosyl_localization_ar.dart +++ b/lib/l10n/generated/moosyl_localization_ar.dart @@ -52,7 +52,7 @@ class MoosylLocalizationAr extends MoosylLocalization { 'بعد إتمام الدفع، املأ المعلومات التالية'; @override - String get enterYourBankilyPhoneNumber => 'أدخل رقم هاتفك الخاص ببنكيلي'; + String get enterYourBankilyPhoneNumber => 'أدخل رقم هاتفك'; @override String get bankilyPhoneNumber => 'رقم هاتف بنكيلي'; @@ -61,16 +61,19 @@ class MoosylLocalizationAr extends MoosylLocalization { String get paymentPassCode => 'رمز المرور للدفع'; @override - String get paymentPassCodeFromBankily => 'رمز المرور للدفع من بنكيلي'; + String get codeBPay => 'رمز BPay'; @override - String get sendForVerification => 'أرسل للتحقق'; + String get amountToPay => 'المبلغ الواجب دفعه'; @override - String get codeBPay => 'رمز BPay'; + String get choosePaymentMethods => 'اختر طريقة الدفع'; @override - String get amountToPay => 'المبلغ الواجب دفعه'; + String get tax => 'الضريبة'; + + @override + String get pay => 'ادفع'; @override String get retry => 'إعادة المحاولة'; @@ -161,4 +164,70 @@ class MoosylLocalizationAr extends MoosylLocalization { @override String get validDigitCode => 'أدخل رمزًا صالحًا مكونًا من 4 أرقام'; + + @override + String get totalAmount => 'المبلغ الإجمالي'; + + @override + String get paymentSummary => 'ملخص الدفع'; + + @override + String get paymentCode => 'رمز الدفع'; + + @override + String get ivePaid => 'لقد دفعت'; + + @override + String get usePaymentCodeAboveToComplete => + 'استخدم رمز الدفع أعلاه لإكمال الدفع في تطبيق سداد، ثم اضغط على الزر أدناه.'; + + @override + String get paymentRequestFullyPaid => 'تم دفع طلب الدفع هذا بالكامل.'; + + @override + String get amountToPayShouldMatchPaymentRequest => + 'يجب أن يتطابق المبلغ المطلوب دفعه مع مبلغ طلب الدفع.'; + + @override + String get selected => 'محدد'; + + @override + String get tapToUse => 'اضغط للاستخدام'; + + @override + String get chooseHowYouWouldLikeToPay => 'اختر كيف تريد الدفع'; + + @override + String get useThisCodeToCompletePayment => 'استخدم هذا الرمز لإكمال الدفع'; + + @override + String get sedadStep1 => '1. افتح تطبيق سداد'; + + @override + String get sedadStep2 => '2. انتقل إلى الدفع'; + + @override + String get sedadStep3 => '3. الصق أو أدخل الرمز لتأكيد الدفع'; + + @override + String get sedadStep4 => '4. ارجع إلى هذه الصفحة وانقر على \"لقد دفعت\"'; + + @override + String get changePaymentMethod => 'تغيير طريقة الدفع'; + + @override + String get clickToCopy => 'انقر للنسخ'; + + @override + String get bankilyStep1 => '1. افتح تطبيق بنكيلي'; + + @override + String get bankilyStep2 => '2. انتقل إلى BPay'; + + @override + String get bankilyStep3 => '3. أدخل رمز BPay والمبلغ المطلوب دفعه'; + + @override + String get bankilyStep4 => + '4. ارجع هنا، أدخل رقمك ورمز المرور، ثم انقر على ادفع'; } diff --git a/lib/l10n/generated/moosyl_localization_en.dart b/lib/l10n/generated/moosyl_localization_en.dart index da83d23..063ed96 100644 --- a/lib/l10n/generated/moosyl_localization_en.dart +++ b/lib/l10n/generated/moosyl_localization_en.dart @@ -52,25 +52,28 @@ class MoosylLocalizationEn extends MoosylLocalization { 'After making the payment, fill the following information'; @override - String get enterYourBankilyPhoneNumber => 'Enter your Bankily phone number'; + String get enterYourBankilyPhoneNumber => 'Enter phone number'; @override String get bankilyPhoneNumber => 'Bankily Phone Number'; @override - String get paymentPassCode => 'Payment Pass Code'; + String get paymentPassCode => 'Payment Code'; @override - String get paymentPassCodeFromBankily => 'Payment Pass Code from Bankily'; + String get codeBPay => 'Code BPay'; @override - String get sendForVerification => 'Send for verification'; + String get amountToPay => 'Amount to pay'; @override - String get codeBPay => 'Code BPay'; + String get choosePaymentMethods => 'Choose payment method'; @override - String get amountToPay => 'Amount to pay'; + String get tax => 'Tax'; + + @override + String get pay => 'Pay'; @override String get retry => 'Retry'; @@ -161,4 +164,71 @@ class MoosylLocalizationEn extends MoosylLocalization { @override String get validDigitCode => 'Enter a valid 4-digit code'; + + @override + String get totalAmount => 'Total Amount'; + + @override + String get paymentSummary => 'Payment Summary'; + + @override + String get paymentCode => 'Payment Code'; + + @override + String get ivePaid => 'I\'ve paid'; + + @override + String get usePaymentCodeAboveToComplete => + 'Use the payment code above to complete your payment in the Sedad app, then tap the button below.'; + + @override + String get paymentRequestFullyPaid => 'This payment request has fully paid.'; + + @override + String get amountToPayShouldMatchPaymentRequest => + 'Amount to pay should match payment request amount.'; + + @override + String get selected => 'Selected'; + + @override + String get tapToUse => 'Tap to use'; + + @override + String get chooseHowYouWouldLikeToPay => 'Choose how you would like to pay'; + + @override + String get useThisCodeToCompletePayment => + 'Use this code to complete payment'; + + @override + String get sedadStep1 => '1. Open the Sedad app'; + + @override + String get sedadStep2 => '2. Go to payment'; + + @override + String get sedadStep3 => '3. Paste or enter the code to confirm payment'; + + @override + String get sedadStep4 => '4. Return to this page and click \"I\'ve paid\"'; + + @override + String get changePaymentMethod => 'Change payment method'; + + @override + String get clickToCopy => 'Click to copy'; + + @override + String get bankilyStep1 => '1. Open the Bankily app'; + + @override + String get bankilyStep2 => '2. Go to BPay'; + + @override + String get bankilyStep3 => '3. Enter the BPay code and amount to pay'; + + @override + String get bankilyStep4 => + '4. Return here, enter your phone and passcode, then click Pay'; } diff --git a/lib/l10n/generated/moosyl_localization_fr.dart b/lib/l10n/generated/moosyl_localization_fr.dart index 8b457c8..8e7d2fe 100644 --- a/lib/l10n/generated/moosyl_localization_fr.dart +++ b/lib/l10n/generated/moosyl_localization_fr.dart @@ -52,8 +52,7 @@ class MoosylLocalizationFr extends MoosylLocalization { 'Après avoir effectué le paiement, remplissez les informations suivantes'; @override - String get enterYourBankilyPhoneNumber => - 'Entrez votre numéro de téléphone Bankily'; + String get enterYourBankilyPhoneNumber => 'Entrez votre numéro'; @override String get bankilyPhoneNumber => 'Numéro de téléphone Bankily'; @@ -62,17 +61,19 @@ class MoosylLocalizationFr extends MoosylLocalization { String get paymentPassCode => 'Code de passe de paiement'; @override - String get paymentPassCodeFromBankily => - 'Code de passe de paiement de Bankily'; + String get codeBPay => 'Code BPay'; @override - String get sendForVerification => 'Envoyer pour vérification'; + String get amountToPay => 'Montant à payer'; @override - String get codeBPay => 'Code BPay'; + String get choosePaymentMethods => 'Choisir le mode de paiement'; @override - String get amountToPay => 'Montant à payer'; + String get tax => 'Taxe'; + + @override + String get pay => 'Payer'; @override String get retry => 'Réessayer'; @@ -165,4 +166,76 @@ class MoosylLocalizationFr extends MoosylLocalization { @override String get validDigitCode => 'Entrez un code à 4 chiffres valide'; + + @override + String get totalAmount => 'Montant total'; + + @override + String get paymentSummary => 'Résumé du paiement'; + + @override + String get paymentCode => 'Code de paiement'; + + @override + String get ivePaid => 'J\'ai payé'; + + @override + String get usePaymentCodeAboveToComplete => + 'Utilisez le code de paiement ci-dessus pour finaliser votre paiement dans l\'application Sedad, puis appuyez sur le bouton ci-dessous.'; + + @override + String get paymentRequestFullyPaid => + 'Cette demande de paiement est entièrement payée.'; + + @override + String get amountToPayShouldMatchPaymentRequest => + 'Le montant à payer doit correspondre au montant de la demande de paiement.'; + + @override + String get selected => 'Sélectionné'; + + @override + String get tapToUse => 'Appuyez pour utiliser'; + + @override + String get chooseHowYouWouldLikeToPay => + 'Choisissez comment vous souhaitez payer'; + + @override + String get useThisCodeToCompletePayment => + 'Utilisez ce code pour finaliser le paiement'; + + @override + String get sedadStep1 => '1. Ouvrez l\'application Sedad'; + + @override + String get sedadStep2 => '2. Allez dans paiement'; + + @override + String get sedadStep3 => + '3. Collez ou saisissez le code pour confirmer le paiement'; + + @override + String get sedadStep4 => + '4. Revenez sur cette page et cliquez sur \"J\'ai payé\"'; + + @override + String get changePaymentMethod => 'Changer de méthode de paiement'; + + @override + String get clickToCopy => 'Cliquez pour copier'; + + @override + String get bankilyStep1 => '1. Ouvrez l\'application Bankily'; + + @override + String get bankilyStep2 => '2. Allez dans BPay'; + + @override + String get bankilyStep3 => + '3. Saisissez le code BPay et le montant à payer'; + + @override + String get bankilyStep4 => + '4. Revenez ici, entrez votre numéro et code, puis cliquez sur Payer'; } diff --git a/lib/src/helpers/exception_handling/exception_mapper.dart b/lib/src/helpers/exception_handling/exception_mapper.dart index 45df62a..e31b1f7 100644 --- a/lib/src/helpers/exception_handling/exception_mapper.dart +++ b/lib/src/helpers/exception_handling/exception_mapper.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; -import 'package:moosyl/src/helpers/exception_handling/exceptions.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/src/helpers/exception_handling/exceptions.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; /// A utility class for mapping exceptions to user-friendly error messages. class ExceptionMapper { diff --git a/lib/src/helpers/fetcher.dart b/lib/src/helpers/fetcher.dart deleted file mode 100644 index 83c18d9..0000000 --- a/lib/src/helpers/fetcher.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'dart:convert'; - -import 'package:file_picker/file_picker.dart'; -import 'package:http/http.dart' as http; - -import 'package:moosyl/src/helpers/exception_handling/exceptions.dart'; - -/// A class responsible for making HTTP requests to the server. -/// It allows for sending GET and POST requests with predefined headers. -class Fetcher { - /// Constructs a [Fetcher] with the provided [publishableApiKey]. - Fetcher(this.publishableApiKey); - - /// The API key used to authenticate the requests. - String publishableApiKey; - - /// Returns the HTTP headers including the `api_key`. - Map get headers { - final headers = { - 'content-type': 'application/json', - 'Authorization': publishableApiKey, - }; - - return headers; - } - - /// Sends a GET request to the given [url] and returns a [FetcherResponse]. - /// - /// The optional [bytes] parameter specifies if the response should return - /// bytes instead of a decoded response body. Throws an exception if the - /// response is unsuccessful. - Future get(String url, {bool bytes = false}) async { - final response = await http - .get(Uri.parse(url), headers: headers) - .timeout(const Duration(minutes: 1)); - - final res = FetcherResponse.fromHttpResponse(response, bytes: bytes); - - if (!res.success) throw res.toException; - - return res; - } - - /// Sends a POST request to the given [url] with an optional [body]. - /// - /// The body should be a [Map] that will be encoded to JSON. Throws an - /// exception if the response is unsuccessful. - Future post(String url, {Map? body}) async { - final response = await http - .post( - Uri.parse(url), - body: json.encode(body), - headers: headers, - ) - .timeout(const Duration(minutes: 1)); - - final res = FetcherResponse.fromHttpResponse(response); - - if (!res.success) throw res.toException; - - return res; - } - - /// Sends a POST request to the given [url] with a [body] and [files]. - - Future multipartPost( - String url, { - required Map body, - List files = const [], - }) async { - // Create a multipart request - final request = http.MultipartRequest('POST', Uri.parse(url)); - - // Add the headers - request.headers.addAll(headers); - - // Attach the body fields (optional) - request.fields.addAll( - body.map((key, value) => MapEntry(key, value.toString())), - ); - - // Attach the files (optional) - for (final file in files) { - if (file.bytes != null) { - request.files.add( - http.MultipartFile.fromBytes( - 'attachments', // make sure this key matches the API's expected field name - file.bytes!, - filename: file.name, - ), - ); - } else if (file.path != null) { - request.files.add( - await http.MultipartFile.fromPath( - 'attachments', // make sure this key matches the API's expected field name - file.path!, - ), - ); - } - } - - // Send the request - final response = await request.send(); - - // Convert the streamed response into a regular response - final responseBody = await http.Response.fromStream(response); - final res = FetcherResponse.fromHttpResponse(responseBody); - - if (!res.success) throw res.toException; - - return res; - } -} - -/// A class that represents the response returned from the [Fetcher]. -/// -/// Contains the [url], [status], and the [data] of the response. -class FetcherResponse { - /// The URL that was used in the request. - final String url; - - /// The HTTP status code returned by the server. - final int status; - - /// The response body data, can be of any type [T]. - final T? data; - - /// A boolean indicating if the response was successful (status code between 200 and 299). - bool get success => status >= 200 && status <= 299; - - /// Converts the raw HTTP response body [String] to a dynamic object. - /// - /// Attempts to parse the body as JSON. If parsing fails, it returns the original body as-is. - static dynamic httpResponseBodyConvertor(String body) { - dynamic result = body; - - try { - result = json.decode(body); - } catch (error) {/* ignored */} - - return result; - } - - /// Constructs a [FetcherResponse] with the given [url], [status], and [data]. - const FetcherResponse({ - required this.url, - required this.status, - required this.data, - }); - - /// Factory constructor that creates a [FetcherResponse] from an [http.Response]. - /// - /// The optional [bytes] parameter specifies if the response should return - /// bytes instead of a decoded response body. - FetcherResponse.fromHttpResponse(http.Response response, {bool bytes = false}) - : url = response.request?.url.toString() ?? '', - status = response.statusCode, - data = bytes - ? response.bodyBytes - : httpResponseBodyConvertor(response.body); - - /// Strips HTML tags and special characters from the given [text]. - /// - /// This is useful for cleaning up error messages returned by the server. - String stripHtmlIfNeeded(String text) { - return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' '); - } - - /// Converts the response to an [AppException]. - /// - /// This method extracts the error message from the server response and returns - /// a properly formatted exception. - AppException get toException { - return AppException( - code: data is String - ? AppExceptionCode(data as String) - : AppExceptionCode.unknown, - message: (data is String) ? data as String : 'An error occurred', - ); - } -} - -/// A static class containing the URLs for various API endpoints. -class Endpoints { - /// The base URL of the API. - static const String baseUrl = 'https://moosyl.moosyl.workers.dev'; - - /// The URL for retrieving payment method configurations. - static String paymentMethods(bool isTestingMode) => - '$baseUrl/configuration?isTestingMode=$isTestingMode'; - - /// Returns the URL for processing payment for the given [id]. - static String get pay => '$baseUrl/payment'; - - /// Returns the URL for processing manual payment. - static String get manualPayment => '$baseUrl/payment/manual'; - - /// Returns the URL for retrieving details of a specific transaction with the given [id]. - static String paymentRequest(String id) => '$baseUrl/payment-request/by-transaction/$id'; -} diff --git a/lib/src/helpers/validators.dart b/lib/src/helpers/validators.dart index 664ab1d..ec43f71 100644 --- a/lib/src/helpers/validators.dart +++ b/lib/src/helpers/validators.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; /// A utility class for validating different input formats. class Validators { diff --git a/lib/src/models/payment_method_model.dart b/lib/src/models/payment_method_model.dart index c60c291..c13499b 100644 --- a/lib/src/models/payment_method_model.dart +++ b/lib/src/models/payment_method_model.dart @@ -1,30 +1,7 @@ import 'package:flutter/widgets.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; -import 'package:moosyl/src/widgets/icons.dart'; - -enum PaymentType { - manual, - auto; - - String get toStr { - return switch (this) { - PaymentType.manual => 'manual', - PaymentType.auto => 'auto' - }; - } - - bool get isManual => this == manual; - bool get isAuto => this == auto; - - static PaymentType fromString(String type) { - return PaymentType.values.firstWhere( - (value) => value.toStr == type, - orElse: () => - throw UnimplementedError('This payment method is not supported'), - ); - } -} +import 'package:moosyl_flutter/src/widgets/icons.dart'; /// Enum representing the different types of payment methods available. /// @@ -106,14 +83,16 @@ enum PaymentMethodTypes { /// Abstract class representing a payment method. /// -/// This class holds the common properties and methods for all payment methods. -abstract class PaymentMethod { +/// Model representing the configuration for the Bankily payment method. +class PaymentMethod { /// The unique identifier for the payment method. final String id; /// The type of the payment method. final PaymentMethodTypes method; - final PaymentType type; + + /// The BPay number associated with the Bankily payment method. + final String bPayNumber; /// Creates an instance of [PaymentMethod]. /// @@ -121,7 +100,7 @@ abstract class PaymentMethod { PaymentMethod({ required this.id, required this.method, - required this.type, + required this.bPayNumber, }); /// Creates an instance of [PaymentMethod] from a map. @@ -130,7 +109,7 @@ abstract class PaymentMethod { PaymentMethod.fromMap(Map map) : id = map['id'], method = PaymentMethodTypes.fromString(map['type']), - type = PaymentType.fromString(map['configurationType']); + bPayNumber = map['config']['code']; /// Creates a [PaymentMethod] instance from the provided type. /// @@ -138,73 +117,7 @@ abstract class PaymentMethod { /// of payment method to create. /// /// Throws an [UnimplementedError] if the payment method type is not supported. - static PaymentMethod fromPaymentMethod(Map map) { - final type = PaymentMethodTypes.fromString(map['type']); - switch (type) { - case PaymentMethodTypes.bankily: - return BankilyConfigModel.fromMap(map); - default: - throw UnimplementedError('This payment method is not supported'); - } - } - static PaymentMethod fromPaymentType(Map map) { - final type = PaymentType.fromString(map['configurationType']); - switch (type) { - case PaymentType.auto: - return fromPaymentMethod(map); - case PaymentType.manual: - return ManualConfigModel.fromMap(map); - } + return PaymentMethod.fromMap(map); } } - -/// Model representing the configuration for the Bankily payment method. -/// -/// Extends [PaymentMethod] to include specific properties for Bankily. -class BankilyConfigModel extends PaymentMethod { - /// The BPay number associated with the Bankily payment method. - final String bPayNumber; - - /// Creates an instance of [BankilyConfigModel]. - /// - /// Requires [id], [method], and [bPayNumber] to initialize the model. - BankilyConfigModel({ - required super.id, - required super.method, - required this.bPayNumber, - required super.type, - }); - - /// Creates an instance of [BankilyConfigModel] from a map. - /// - /// The [map] must contain the key 'bpay_number' to initialize the property. - BankilyConfigModel.fromMap(super.map) - : bPayNumber = map['config']['code'], - super.fromMap(); -} - -/// Model representing the configuration for the Bankily payment method. -/// -/// Extends [PaymentMethod] to include specific properties for Bankily. -class ManualConfigModel extends PaymentMethod { - /// The BPay number associated with the Bankily payment method. - final String merchantCode; - - /// Creates an instance of [ManualConfigModel]. - /// - /// Requires [id], [method], and [bPayNumber] to initialize the model. - ManualConfigModel({ - required super.id, - required super.method, - required this.merchantCode, - required super.type, - }); - - /// Creates an instance of [ManualConfigModel] from a map. - /// - /// The [map] must contain the key 'merchant code' to initialize the property. - ManualConfigModel.fromMap(super.map) - : merchantCode = map['config']['code'], - super.fromMap(); -} diff --git a/lib/src/pages/automatic_pay_page.dart b/lib/src/pages/automatic_pay_page.dart deleted file mode 100644 index 5f82d06..0000000 --- a/lib/src/pages/automatic_pay_page.dart +++ /dev/null @@ -1,444 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:moosyl/src/models/payment_request_model.dart'; -import 'package:moosyl/src/helpers/exception_handling/exception_mapper.dart'; -import 'package:moosyl/src/providers/get_payment_methods_provider.dart'; -import 'package:moosyl/src/widgets/buttons.dart'; -import 'package:provider/provider.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; -import 'package:moosyl/src/helpers/validators.dart'; - -import 'package:moosyl/src/models/payment_method_model.dart'; -import 'package:moosyl/src/providers/pay_provider.dart'; -import 'package:moosyl/src/widgets/container.dart'; -import 'package:moosyl/src/widgets/error_widget.dart'; -import 'package:moosyl/src/widgets/feedback.dart'; -import 'package:moosyl/src/widgets/icons.dart'; -import 'package:moosyl/src/widgets/text_input.dart'; - -/// A widget that displays the mode of payment information. -class BottomSheetButton extends StatelessWidget { - /// Loading state of the button. - final bool loading; - - /// Disabled state of the button. - final bool disabled; - - /// Callback function for the button. - final VoidCallback onTap; - - /// Error message to display. - final String? error; - - /// Flag to indicate whether the button should have a shadow. - final bool withShadow; - - /// A widget that displays the mode of payment information. - - /// Constructor for [BottomSheetButton]. - const BottomSheetButton({ - super.key, - required this.loading, - required this.disabled, - required this.onTap, - required this.withShadow, - this.error, - }); - - @override - Widget build(BuildContext context) { - final deviceBottomPadding = MediaQuery.of(context).padding.bottom; - final localizationHelper = MoosylLocalization.of(context)!; - - return AppContainer( - color: Theme.of(context).colorScheme.onPrimary, - width: double.infinity, - padding: const EdgeInsetsDirectional.symmetric(horizontal: 16).copyWith( - bottom: deviceBottomPadding == 0 ? 20 : deviceBottomPadding, - ), - shadows: [ - if (withShadow) - BoxShadow( - color: Colors.black.withOpacity(0.12), - offset: const Offset(0, -4), - blurRadius: 16, - ) - ], - borderRadius: BorderRadius.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - if (error != null) - Padding( - padding: const EdgeInsetsDirectional.only(top: 16), - child: AppErrorWidget( - message: error, - horizontalAxis: true, - ), - ), - AppButton( - loading: loading, - disabled: disabled, - labelText: loading - ? localizationHelper.sending - : error != null - ? localizationHelper.retry - : localizationHelper.sendForVerification, - background: - error != null ? Theme.of(context).colorScheme.error : null, - onPressed: onTap, - ), - ], - ), - ); - } -} - -/// A widget that represents the payment process. -/// -/// The [AutomaticPayPage] widget allows users to make a payment using the provided -/// [PaymentMethod]. It manages the payment input and validation process. -class AutomaticPayPage extends HookWidget { - /// Creates a [AutomaticPayPage] widget. - /// - /// Requires the [method], [publishableApiKey], [transactionId], and [organizationLogo]. - /// Optionally accepts a callback [onPaymentSuccess] to be called when - /// payment is successful. - - const AutomaticPayPage({ - super.key, - required this.method, - required this.publishableApiKey, - required this.transactionId, - this.organizationLogo, - this.onPaymentSuccess, - this.fullPage = true, - }); - - /// The payment method selected for the payment process. - final PaymentMethod method; - - /// The API key required for payment authentication. - final String publishableApiKey; - - /// The transaction ID associated with the current payment. - final String transactionId; - - /// The logo of the organization processing the payment. - final Widget? organizationLogo; - - /// Callback to be executed upon successful payment. - final FutureOr Function()? onPaymentSuccess; - - /// Builds the [AutomaticPayPage] widget. - final bool fullPage; - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => AutomaticPayProvider( - publishableApiKey: publishableApiKey, - transactionId: transactionId, - method: method, - onPaymentSuccess: onPaymentSuccess, - ), - child: _AutomaticPayBody(organizationLogo, fullPage), - ); - } -} - -class _AutomaticPayBody extends StatelessWidget { - final Widget? organizationLogo; - final bool fullPage; - const _AutomaticPayBody(this.organizationLogo, this.fullPage); - - @override - Widget build(BuildContext context) { - const horizontalPadding = EdgeInsetsDirectional.symmetric(horizontal: 16); - final localizationHelper = MoosylLocalization.of(context)!; - - final getPaymentMethodsProvider = context.read(); - - final provider = context.watch(); - - if (provider.isLoading && provider.paymentRequest == null) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (provider.error != null && provider.paymentRequest == null) { - return AppErrorWidget( - message: ExceptionMapper.getErrorMessage(provider.error, context), - onRetry: provider.getPaymentRequest, - ); - } - - final method = provider.method; - - final paymentButton = BottomSheetButton( - disabled: false, - loading: provider.isLoading, - onTap: () => provider.pay(context), - withShadow: fullPage, - ); - - final children = Form( - key: provider.formKey, - child: ListView( - padding: EdgeInsets.only(bottom: fullPage ? 200 : 20, top: 16), - shrinkWrap: !fullPage, - physics: !fullPage ? const NeverScrollableScrollPhysics() : null, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - localizationHelper.payUsing( - method.method.title(context), - ), - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(fontSize: 20), - ), - if (!fullPage) - AppButton( - minHeight: 0, - background: Theme.of(context).colorScheme.onPrimary, - textColor: Theme.of(context).colorScheme.onSurface, - margin: EdgeInsets.zero, - border: BorderSide( - color: Theme.of(context).colorScheme.onSurface, - ), - labelText: localizationHelper.change, - onPressed: () => - getPaymentMethodsProvider.setPaymentMethod(null), - ), - ], - ), - Text( - localizationHelper - .copyTheCodeBPayAndHeadToBankilyToPayTheAmount, - ), - ], - ), - ), - ModeOfPaymentInfo( - mode: method, - organizationLogo: organizationLogo, - paymentRequest: provider.paymentRequest!, - ), - Divider( - height: 1, - thickness: 4, - color: Theme.of(context).colorScheme.surface, - ), - const SizedBox(height: 16), - InputLabel( - style: Theme.of(context).textTheme.titleLarge!.copyWith( - fontSize: 18, - ), - label: localizationHelper.afterPayment, - child: Text( - localizationHelper - .afterMakingThePaymentFillTheFollowingInformation, - ), - ), - const SizedBox(height: 20), - AppTextInput( - margin: horizontalPadding, - maxLength: 8, - controller: provider.phoneNumberTextController, - validator: (value) => - Validators.validateMauritanianPhoneNumber(value, context), - hint: localizationHelper.enterYourBankilyPhoneNumber, - label: localizationHelper.bankilyPhoneNumber, - ), - AppTextInput( - margin: horizontalPadding, - controller: provider.passCodeTextController, - validator: (value) => Validators.validatePassCode(value, context), - hint: localizationHelper.paymentPassCode, - label: localizationHelper.paymentPassCodeFromBankily, - errorText: provider.error != null - ? ExceptionMapper.getErrorMessage( - provider.error, - context, - ) - : null, - maxLength: 4, - ), - ], - ), - ); - - if (!fullPage) { - return Column( - children: [ - Expanded(child: SingleChildScrollView(child: children)), - paymentButton - ], - ); - } - - return Scaffold( - appBar: fullPage - ? AppBar( - title: Text(method.method.title(context)), - leading: BackButton( - onPressed: () => - getPaymentMethodsProvider.setPaymentMethod(null), - ), - ) - : null, - body: children, - bottomSheet: paymentButton, - ); - } -} - -/// A widget that displays information about the selected payment method. -/// -/// This widget shows details specific to the payment method, such as -/// payment codes and the amount to pay. -class ModeOfPaymentInfo extends StatelessWidget { - /// Creates a [ModeOfPaymentInfo] widget. - const ModeOfPaymentInfo({ - super.key, - required this.mode, - this.organizationLogo, - required this.paymentRequest, - }); - - /// The selected payment method. - final PaymentMethod mode; - - /// The logo of the organization processing the payment. - final Widget? organizationLogo; - - /// - final PaymentRequestModel paymentRequest; - - @override - Widget build(BuildContext context) { - final localizationHelper = MoosylLocalization.of(context)!; - - final isManual = mode is ManualConfigModel; - - final merchantCode = - isManual ? (mode as ManualConfigModel).merchantCode : ''; - - final identifierLabel = isManual && merchantCode.length < 8 - ? localizationHelper.merchantCodeLabel - : localizationHelper.phoneNumberLabel; - - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - children: organizationLogo != null - ? [ - Expanded( - child: organizationLogo!, - ), - const SizedBox(width: 32), - AppIcons.close, - const SizedBox(width: 32), - Expanded( - child: mode.method.icon.apply(size: 80), - ), - ] - : [ - Expanded( - child: mode.method.icon.apply(size: 120), - ), - ], - ), - const SizedBox(height: 12), - if (mode is ManualConfigModel) ...[ - card( - context, - identifierLabel, - (mode as ManualConfigModel).merchantCode, - copyableValue: mode.id, - ), - card( - context, - localizationHelper.amountToPay, - paymentRequest.amount.toStringAsFixed(2), - ), - ] else if (mode is BankilyConfigModel) ...[ - card( - context, - localizationHelper.codeBPay, - (mode as BankilyConfigModel).bPayNumber, - copyableValue: (mode as BankilyConfigModel).bPayNumber, - ), - card( - context, - localizationHelper.amountToPay, - paymentRequest.amount.toStringAsFixed(2), - ), - ] - ], - ), - ); - } - - /// Creates a card displaying a title and description. - /// - /// The [copyableValue] parameter allows the description to be copied - /// when tapped, if provided. - Widget card( - BuildContext context, - String title, - String description, { - String? copyableValue, - }) { - return AppContainer( - color: Theme.of(context).colorScheme.surface, - padding: const EdgeInsetsDirectional.symmetric( - vertical: 16, - horizontal: 12, - ), - borderRadius: BorderRadius.circular(8), - margin: const EdgeInsetsDirectional.only(bottom: 2), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(title), - const Spacer(), - Text.rich( - TextSpan( - text: description, - children: [ - if (copyableValue != null) - WidgetSpan( - child: AppContainer( - margin: const EdgeInsetsDirectional.only( - start: 8, - ), - padding: EdgeInsetsDirectional.zero, - onTap: () => Feedbacks.copy(copyableValue, context), - child: AppIcons.copy.apply(size: 20), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/pages/available_method_payments.dart b/lib/src/pages/available_method_payments.dart deleted file mode 100644 index 7ea2c7f..0000000 --- a/lib/src/pages/available_method_payments.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:moosyl/src/helpers/exception_handling/exception_mapper.dart'; - -import 'package:provider/provider.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; -import 'package:moosyl/src/providers/get_payment_methods_provider.dart'; -import 'package:moosyl/src/widgets/container.dart'; -import 'package:moosyl/src/widgets/error_widget.dart'; -import 'package:moosyl/src/widgets/icons.dart'; - -/// A widget that displays the available payment methods for selection. -/// -/// This widget allows users to choose a payment method from a grid of options. -/// It retrieves the available methods using the [GetPaymentMethodsProvider] -/// and handles loading and error states. -class SelectPaymentMethodPage extends StatelessWidget { - /// Creates an instance of [SelectPaymentMethodPage]. - /// - /// the [fullPage] flag is used to determine whether the widget should be - /// displayed in full page mode or as a part of a larger widget. - const SelectPaymentMethodPage({ - super.key, - required this.fullPage, - }); - - /// A flag to indicate whether the widget is in full page mode. - final bool fullPage; - - @override - Widget build(BuildContext context) { - final provider = context.watch(); - - // Show loading indicator while fetching payment methods. - if (provider.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - // Display an error widget if there was an error fetching payment methods. - if (provider.error != null) { - return AppErrorWidget( - message: ExceptionMapper.getErrorMessage(provider.error, context), - withScaffold: fullPage, - onRetry: provider.getMethods, - ); - } - - final methods = provider.supportedTypes; - - final children = GridView.builder( - padding: const EdgeInsets.all(16), - shrinkWrap: !fullPage, - physics: !fullPage ? const NeverScrollableScrollPhysics() : null, - itemCount: methods.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 1.8, - ), - itemBuilder: (context, index) { - final method = methods.elementAt(index); - - return InkWell( - onTap: () => provider.onTap(method, context), - child: AppContainer( - border: Border.all(width: 1), - padding: const EdgeInsetsDirectional.all(24), - child: provider.customIcons?[method] != null - ? AppIcon(path: provider.customIcons?[method]) - : method.icon, - ), - ); - }, - ); - - final child = Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - MoosylLocalization.of(context)!.paymentMethod, - style: Theme.of(context).textTheme.headlineMedium!.copyWith( - fontSize: 20, - ), - ), - ), - if (fullPage) Expanded(child: children) else children, - ], - ), - ); - - return fullPage ? Scaffold(body: child) : child; - } -} diff --git a/lib/src/pages/bankily_view.dart b/lib/src/pages/bankily_view.dart new file mode 100644 index 0000000..11421b8 --- /dev/null +++ b/lib/src/pages/bankily_view.dart @@ -0,0 +1,230 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/src/helpers/validators.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/providers/pay_provider.dart'; +import 'package:moosyl_flutter/src/widgets/buttons.dart'; +import 'package:moosyl_flutter/src/widgets/container.dart'; +import 'package:moosyl_flutter/src/widgets/feedback.dart'; +import 'package:moosyl_flutter/src/widgets/icons.dart'; +import 'package:moosyl_flutter/src/widgets/text_input.dart'; +import 'package:provider/provider.dart'; + +/// A widget that represents the Bankily payment process. +class BankilyView extends StatelessWidget { + const BankilyView({ + super.key, + required this.method, + required this.publishableApiKey, + required this.transactionId, + this.organizationLogo, + this.onPaymentSuccess, + this.onClose, + required this.primaryColor, + }); + + final PaymentMethod method; + final String publishableApiKey; + final String transactionId; + final Widget? organizationLogo; + final FutureOr Function()? onPaymentSuccess; + final VoidCallback? onClose; + final Color primaryColor; + + @override + Widget build(BuildContext context) { + return _BankilyDialogContent( + primaryColor: primaryColor, + method: method, + onClose: onClose ?? () => Navigator.of(context).pop(), + ); + } +} + +/// Dialog content for Bankily payment. Expects [PayProvider] from context. +class _BankilyDialogContent extends StatelessWidget { + const _BankilyDialogContent({ + required this.primaryColor, + required this.method, + required this.onClose, + }); + + final PaymentMethod method; + final VoidCallback onClose; + final Color primaryColor; + + @override + Widget build(BuildContext context) { + final localizationHelper = MoosylLocalization.of(context)!; + final provider = context.watch(); + final platformIcon = method.method.icon; + + return Dialog( + shadowColor: Theme.of(context).colorScheme.outline, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + insetPadding: const EdgeInsets.symmetric(horizontal: 10), + child: Padding( + padding: const EdgeInsets.all(24), + child: Form( + key: provider.formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + localizationHelper.codeBPay, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + localizationHelper.useThisCodeToCompletePayment, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Platform icon + Center( + child: AppContainer( + width: 200, + border: Border.all(color: Colors.grey.shade300), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: platformIcon.apply(size: 48), + ), + ), + const SizedBox(height: 20), + + // Copyable container with BPay code + GestureDetector( + onTap: () => Feedbacks.copy(method.bPayNumber, context), + child: AppContainer( + color: Colors.grey.shade200, + border: Border.all(color: Colors.grey.shade300), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text( + method.bPayNumber, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + AppIcons.copy.apply(size: 20), + Text( + localizationHelper.clickToCopy, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // Steps + _StepItem(text: localizationHelper.bankilyStep1), + _StepItem(text: localizationHelper.bankilyStep2), + _StepItem(text: localizationHelper.bankilyStep3), + _StepItem(text: localizationHelper.bankilyStep4), + const SizedBox(height: 16), + + // Phone and passcode inputs in same container + AppContainer( + padding: + const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppTextInput( + controller: provider.phoneNumberTextController, + validator: (value) => + Validators.validateMauritanianPhoneNumber( + value, context), + hint: localizationHelper.enterYourBankilyPhoneNumber, + label: localizationHelper.bankilyPhoneNumber, + maxLength: 8, + ), + AppTextInput( + controller: provider.passCodeTextController, + validator: (value) => + Validators.validatePassCode(value, context), + hint: localizationHelper.paymentPassCode, + label: localizationHelper.paymentPassCode, + maxLength: 4, + ), + ], + ), + ), + // Pay button + AppButton( + primaryColor: primaryColor, + minHeight: 40, + style: AppButtonStyle.primary, + labelText: localizationHelper.pay, + loading: provider.isLoading, + onPressed: () => provider.pay(context), + ), + + // Change payment method button + AppButton( + primaryColor: primaryColor, + margin: const EdgeInsets.only(top: 8), + minHeight: 40, + style: AppButtonStyle.outline, + labelText: localizationHelper.changePaymentMethod, + onPressed: onClose, + ), + ], + ), + ), + ), + ), + ); + } +} + +class _StepItem extends StatelessWidget { + final String text; + + const _StepItem({required this.text}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium, + ), + ); + } +} diff --git a/lib/src/pages/home.dart b/lib/src/pages/home.dart index 1ce64db..b1dcd31 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/pages/home.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -import 'package:moosyl/src/models/payment_method_model.dart'; -import 'package:moosyl/src/pages/moosyl_view.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/pages/moosyl_view.dart'; /// [Moosyl] provides a widget that handles the payment process. /// It allows you to customize the payment methods, icons, and success callbacks. @@ -34,6 +34,9 @@ class Moosyl extends HookWidget { /// The payment method selected for the payment process. final bool isTestingMode; + /// When true, shows as full page. When false, shows as bottom sheet. + final bool isFullPage; + /// Displays the [MoosylView] modal sheet to start the payment process. /// /// * [context]: The build context. @@ -50,6 +53,7 @@ class Moosyl extends HookWidget { final FutureOr Function()? onPaymentSuccess, Map? customIcons, bool isTestingMode = false, + bool isFullPage = false, }) { showBarModalBottomSheet( context: context, @@ -60,6 +64,7 @@ class Moosyl extends HookWidget { onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, isTestingMode: isTestingMode, + isFullPage: isFullPage, ), ); } @@ -81,6 +86,7 @@ class Moosyl extends HookWidget { this.inputBuilder, this.onPaymentSuccess, this.isTestingMode = false, + this.isFullPage = false, }); @override @@ -98,6 +104,7 @@ class Moosyl extends HookWidget { onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, isTestingMode: isTestingMode, + isFullPage: isFullPage, ), ); }, @@ -112,6 +119,7 @@ class Moosyl extends HookWidget { onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, isTestingMode: isTestingMode, + isFullPage: isFullPage, ); } } diff --git a/lib/src/pages/masrivi_view.dart b/lib/src/pages/masrivi_view.dart new file mode 100644 index 0000000..12b476b --- /dev/null +++ b/lib/src/pages/masrivi_view.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +/// Base URL for Masrivi payment web view. +const String _masriviPayBaseUrl = 'https://payments.moosyl.com/masrivi/pay'; + +/// A view that displays Masrivi payment in a WebView. +/// +/// Loads the Masrivi pay URL and listens for navigation to a success URL. +/// When the URL contains "/success", [onPaymentSuccess] is invoked. +class MasriviView extends StatelessWidget { + const MasriviView({ + super.key, + required this.publishableApiKey, + required this.transactionId, + required this.configurationId, + this.onPaymentSuccess, + this.onBackPress, + }); + + /// The API key for authenticating the payment. + final String publishableApiKey; + + /// The transaction ID for the payment. + final String transactionId; + + /// The configuration ID (payment method ID) from [PaymentMethod.id]. + final String configurationId; + + /// Callback when payment is successful (URL contains "/success"). + final FutureOr Function()? onPaymentSuccess; + + /// Callback when the back button is pressed. + final VoidCallback? onBackPress; + + /// Builds the Masrivi pay URL with query parameters. + String get _payUrl { + final uri = Uri.parse(_masriviPayBaseUrl).replace( + queryParameters: { + 'apiKey': publishableApiKey, + 'transactionId': transactionId, + 'configurationId': configurationId, + }, + ); + return uri.toString(); + } + + @override + Widget build(BuildContext context) { + return _MasriviWebView( + payUrl: _payUrl, + onPaymentSuccess: onPaymentSuccess, + onBackPress: onBackPress ?? () => Navigator.of(context).pop(), + ); + } +} + +class _MasriviWebView extends StatefulWidget { + const _MasriviWebView({ + required this.payUrl, + this.onPaymentSuccess, + required this.onBackPress, + }); + + final String payUrl; + final FutureOr Function()? onPaymentSuccess; + final VoidCallback onBackPress; + + @override + State<_MasriviWebView> createState() => _MasriviWebViewState(); +} + +class _MasriviWebViewState extends State<_MasriviWebView> { + late final WebViewController _controller; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (NavigationRequest request) { + if (request.url.contains('/success')) { + widget.onPaymentSuccess?.call(); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(widget.payUrl)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: BackButton(onPressed: widget.onBackPress), + ), + body: WebViewWidget(controller: _controller), + ); + } +} diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index ee75488..8924ce4 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -1,17 +1,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/pages/masrivi_view.dart'; +import 'package:moosyl_flutter/src/pages/payment_methods_view.dart'; +import 'package:moosyl_flutter/src/providers/get_payment_methods_provider.dart'; import 'package:provider/provider.dart'; -import 'package:moosyl/src/models/payment_method_model.dart'; -import 'package:moosyl/src/pages/available_method_payments.dart'; -import 'package:moosyl/src/pages/automatic_pay_page.dart'; -import 'package:moosyl/src/providers/get_payment_methods_provider.dart'; /// A widget that provides a payment interface for the Software Pay system. /// /// This widget allows users to select a payment method and proceed with the payment. /// It handles localization and manages the state of the selected payment method. -class MoosylView extends HookWidget { +class MoosylView extends StatelessWidget { /// Creates an instance of [MoosylView]. /// Requires the [publishableApiKey] and [transactionId] for the payment transaction, @@ -25,7 +24,11 @@ class MoosylView extends HookWidget { this.onPaymentSuccess, this.customIcons, this.isTestingMode = false, - this.fullPage = true, + this.primaryColor, + this.onBackPress, + this.amountToPay = 0.0, + this.tax = 0.0, + this.isFullPage = true, }); /// The API key for authenticating the payment transaction. @@ -46,8 +49,20 @@ class MoosylView extends HookWidget { /// A flag to indicate whether the widget is in testing mode. final bool isTestingMode; - /// A flag to indicate whether the widget is in full page mode. - final bool fullPage; + /// Primary color for the payment method selection page (radio, pay button). + final Color? primaryColor; + + /// Callback when the back arrow is pressed on the payment method selection page. + final VoidCallback? onBackPress; + + /// Amount to pay (displayed in summary). Defaults to 0. + final double amountToPay; + + /// Tax amount (displayed in summary). Defaults to 0. + final double tax; + + /// When true, payment method selection shows as full page. When false, shows as bottom sheet. + final bool isFullPage; @override Widget build(BuildContext context) { @@ -57,6 +72,8 @@ class MoosylView extends HookWidget { ChangeNotifierProvider( create: (_) => GetPaymentMethodsProvider( publishableApiKey: publishableApiKey, + transactionId: transactionId, + totalAmount: amountToPay + tax, isTestingMode: isTestingMode, customIcons: customIcons, ), @@ -71,18 +88,28 @@ class MoosylView extends HookWidget { // If no payment method is selected, show the available methods page. if (selectedModeOfPayment == null) { - return SelectPaymentMethodPage(fullPage: fullPage); + return SelectPaymentMethodPage( + primaryColor: primaryColor, + onBackPress: onBackPress, + amountToPay: amountToPay, + tax: tax, + totalAmount: amountToPay + tax, + transactionId: transactionId, + onPaymentSuccess: onPaymentSuccess, + organizationLogo: organizationLogo, + isFullPage: isFullPage, + ); + } else { + return MasriviView( + publishableApiKey: publishableApiKey, + transactionId: transactionId, + configurationId: selectedModeOfPayment.id, + onPaymentSuccess: onPaymentSuccess, + onBackPress: () => context + .read() + .setPaymentMethod(null), + ); } - - // If a payment method is selected, proceed to the payment page. - return AutomaticPayPage( - publishableApiKey: publishableApiKey, - method: selectedModeOfPayment, - transactionId: transactionId, - organizationLogo: organizationLogo, - onPaymentSuccess: onPaymentSuccess, - fullPage: fullPage, - ); }, ), ), diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart new file mode 100644 index 0000000..2bf264c --- /dev/null +++ b/lib/src/pages/payment_methods_view.dart @@ -0,0 +1,658 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/src/helpers/exception_handling/exception_mapper.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/pages/bankily_view.dart'; +import 'package:moosyl_flutter/src/pages/sedad_view.dart'; +import 'package:moosyl_flutter/src/providers/get_payment_methods_provider.dart'; +import 'package:moosyl_flutter/src/providers/pay_provider.dart'; +import 'package:moosyl_flutter/src/widgets/buttons.dart'; +import 'package:moosyl_flutter/src/widgets/container.dart'; +import 'package:moosyl_flutter/src/widgets/error_widget.dart'; +import 'package:moosyl_flutter/src/widgets/icons.dart'; +import 'package:provider/provider.dart'; + +/// A widget that displays the available payment methods for selection. +/// +/// This widget allows users to choose a payment method from a list of options. +/// Each row shows an icon, name, and radio indicator. A pay button at the bottom +/// confirms the selection and proceeds to payment. +class SelectPaymentMethodPage extends StatelessWidget { + /// Creates an instance of [SelectPaymentMethodPage]. + const SelectPaymentMethodPage({ + super.key, + this.primaryColor, + this.onBackPress, + this.amountToPay = 0.0, + this.tax = 0.0, + this.totalAmount = 0.0, + required this.transactionId, + this.onPaymentSuccess, + this.organizationLogo, + required this.isFullPage, + }); + + /// When true, shows as full page. When false, shows as bottom sheet. + final bool isFullPage; + + /// The primary color for the page (e.g. for radio, pay button). + final Color? primaryColor; + + /// Callback when the back arrow is pressed. + final VoidCallback? onBackPress; + + /// The amount to pay (displayed in summary). + final double amountToPay; + + /// The tax amount (displayed in summary). + final double tax; + + /// The total amount including tax (displayed on the pay button). + final double totalAmount; + + /// The transaction ID (displayed in the payment request). + final String transactionId; + + /// Callback when payment succeeds (for Sedad/Bankily dialogs). + final FutureOr Function()? onPaymentSuccess; + + /// Organization logo (for Bankily). + final Widget? organizationLogo; + + @override + Widget build(BuildContext context) { + if (isFullPage) { + return _SelectPaymentMethodContent( + primaryColor: primaryColor, + onBackPress: onBackPress, + amountToPay: amountToPay, + tax: tax, + totalAmount: totalAmount, + transactionId: transactionId, + onPaymentSuccess: onPaymentSuccess, + organizationLogo: organizationLogo, + isFullPage: isFullPage, + ); + } else { + return _SelectPaymentMethodBottomSheetTrigger( + primaryColor: primaryColor, + onBackPress: onBackPress, + amountToPay: amountToPay, + tax: tax, + totalAmount: totalAmount, + transactionId: transactionId, + onPaymentSuccess: onPaymentSuccess, + organizationLogo: organizationLogo, + isFullPage: isFullPage, + ); + } + } +} + +/// Full page or bottom sheet content for payment method selection. +class _SelectPaymentMethodContent extends StatelessWidget { + const _SelectPaymentMethodContent({ + required this.primaryColor, + required this.onBackPress, + required this.amountToPay, + required this.tax, + required this.totalAmount, + required this.transactionId, + required this.onPaymentSuccess, + required this.organizationLogo, + required this.isFullPage, + this.onClose, + }); + + final Color? primaryColor; + final VoidCallback? onBackPress; + final double amountToPay; + final double tax; + final double totalAmount; + final String transactionId; + final FutureOr Function()? onPaymentSuccess; + final Widget? organizationLogo; + final bool isFullPage; + final VoidCallback? onClose; + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + final effectivePrimary = + primaryColor ?? Theme.of(context).colorScheme.primary; + final localizationHelper = MoosylLocalization.of(context)!; + final textTheme = Theme.of(context).textTheme; + + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.error != null) { + return AppErrorWidget( + message: ExceptionMapper.getErrorMessage(provider.error, context), + onRetry: provider.getMethods, + ); + } + + final selectionErrorMessage = provider.selectionError != null + ? (provider.selectionError == 'paymentRequestFullyPaid' + ? localizationHelper.paymentRequestFullyPaid + : provider.selectionError == 'amountToPayShouldMatchPaymentRequest' + ? localizationHelper.amountToPayShouldMatchPaymentRequest + : provider.selectionError) + : null; + + final methods = provider.methods; + final pendingSelection = provider.pendingSelection; + + final bodyContent = Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 8), + AppContainer( + padding: const EdgeInsets.all(16), + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text(localizationHelper.chooseHowYouWouldLikeToPay, + style: textTheme.titleMedium), + const SizedBox(height: 10), + ...methods.asMap().entries.map((e) { + final method = e.value; + return _MethodRow( + method: method, + isSelected: pendingSelection?.id == method.id, + primaryColor: effectivePrimary, + onTap: () => provider.setPendingSelection(method), + ); + }), + ], + ), + ), + ], + ), + ), + ), + SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (selectionErrorMessage != null) + Text(selectionErrorMessage, + style: textTheme.bodyMedium?.copyWith(color: Colors.red)), + const SizedBox(height: 16), + AppContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (amountToPay > 0 && tax > 0) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(localizationHelper.amountToPay, + style: textTheme.bodyLarge), + Text( + '${amountToPay > 0 ? amountToPay.toStringAsFixed(0) : (provider.paymentRequest?.amount.toStringAsFixed(0) ?? '0')} MRU', + style: textTheme.bodyLarge + ?.copyWith(fontWeight: FontWeight.bold)), + ], + ), + if (tax > 0) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(localizationHelper.tax, + style: textTheme.bodyLarge), + Text('${tax.toStringAsFixed(0)} MRU', + style: textTheme.bodyLarge + ?.copyWith(fontWeight: FontWeight.bold)), + ], + ), + Divider(color: Colors.grey.shade300), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(localizationHelper.totalAmount, + style: textTheme.bodyLarge), + Text( + '${totalAmount > 0 ? totalAmount.toStringAsFixed(0) : (provider.paymentRequest?.amount.toStringAsFixed(0) ?? '0')} MRU', + style: textTheme.bodyLarge + ?.copyWith(fontWeight: FontWeight.bold)), + ], + ), + ], + )), + AppButton( + primaryColor: effectivePrimary, + minHeight: 50, + labelText: provider.isValidating + ? localizationHelper.sending + : localizationHelper.pay, + onPressed: pendingSelection == null || provider.isValidating + ? null + : () => _onPayPressed( + context, + provider, + pendingSelection, + effectivePrimary, + ), + ), + ], + ), + ), + ], + ), + ); + + if (isFullPage) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + title: Text(localizationHelper.choosePaymentMethods), + leading: onBackPress != null + ? IconButton( + onPressed: onBackPress, + icon: const Icon(Icons.arrow_back), + color: effectivePrimary, + ) + : null, + ), + body: bodyContent, + ); + } + + return bodyContent; + } + + Future _onPayPressed( + BuildContext context, + GetPaymentMethodsProvider provider, + PaymentMethod pendingSelection, + Color effectivePrimary, + ) async { + final methodToShow = await provider.setPaymentMethodWithValidation( + pendingSelection, + ); + if (!context.mounted) return; + + if (methodToShow == null) { + if (provider.selected != null) { + onClose?.call(); + } + return; + } + + final publishableApiKey = provider.publishableApiKey; + final transactionId = provider.transactionId; + + if (methodToShow.method == PaymentMethodTypes.bankily) { + _showBankilyDialog( + context, + publishableApiKey: publishableApiKey, + transactionId: transactionId, + method: methodToShow, + primaryColor: effectivePrimary, + ); + } else if (methodToShow.method == PaymentMethodTypes.sedad || + methodToShow.method == PaymentMethodTypes.bimBank) { + _showSedadDialog( + context, + publishableApiKey: publishableApiKey, + transactionId: transactionId, + method: methodToShow, + primaryColor: effectivePrimary, + ); + } + } + + void _showSedadDialog( + BuildContext context, { + required String publishableApiKey, + required String transactionId, + required PaymentMethod method, + required Color primaryColor, + }) { + final payProvider = PayProvider( + publishableApiKey: publishableApiKey, + transactionId: transactionId, + method: method, + onPaymentSuccess: () async => await onPaymentSuccess?.call(), + ); + final getPaymentMethodsProvider = context.read(); + + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => ChangeNotifierProvider.value( + value: payProvider, + child: _DialogWithPayProvider( + payProvider: payProvider, + builder: (paymentRequest) => SedadView( + primaryColor: primaryColor, + paymentCodeDisplay: method.bPayNumber, + paymentRequest: paymentRequest, + onClose: () { + Navigator.of(dialogContext).pop(); + getPaymentMethodsProvider.setPaymentMethod(null); + }, + ), + ), + ), + ).then((_) { + getPaymentMethodsProvider.setPaymentMethod(null); + }); + } + + void _showBankilyDialog( + BuildContext context, { + required String publishableApiKey, + required String transactionId, + required PaymentMethod method, + required Color primaryColor, + }) { + final payProvider = PayProvider( + publishableApiKey: publishableApiKey, + transactionId: transactionId, + method: method, + onPaymentSuccess: () async => await onPaymentSuccess?.call(), + ); + final getPaymentMethodsProvider = context.read(); + + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => ChangeNotifierProvider.value( + value: payProvider, + child: _DialogWithPayProvider( + payProvider: payProvider, + builder: (_) => BankilyView( + primaryColor: primaryColor, + method: method, + publishableApiKey: publishableApiKey, + transactionId: transactionId, + organizationLogo: organizationLogo, + onClose: () { + Navigator.of(dialogContext).pop(); + getPaymentMethodsProvider.setPaymentMethod(null); + }, + ), + ), + ), + ).then((_) { + getPaymentMethodsProvider.setPaymentMethod(null); + }); + } +} + +/// Triggers the payment method selection bottom sheet on mount. +class _SelectPaymentMethodBottomSheetTrigger extends StatefulWidget { + const _SelectPaymentMethodBottomSheetTrigger({ + required this.primaryColor, + required this.onBackPress, + required this.amountToPay, + required this.tax, + required this.totalAmount, + required this.transactionId, + required this.onPaymentSuccess, + required this.organizationLogo, + required this.isFullPage, + }); + + final Color? primaryColor; + final VoidCallback? onBackPress; + final double amountToPay; + final double tax; + final double totalAmount; + final String transactionId; + final FutureOr Function()? onPaymentSuccess; + final Widget? organizationLogo; + final bool isFullPage; + @override + State<_SelectPaymentMethodBottomSheetTrigger> createState() => + _SelectPaymentMethodBottomSheetTriggerState(); +} + +class _SelectPaymentMethodBottomSheetTriggerState + extends State<_SelectPaymentMethodBottomSheetTrigger> { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _showBottomSheet()); + } + + void _showBottomSheet() { + if (!mounted) return; + final getPaymentMethodsProvider = context.read(); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (sheetContext) => + ChangeNotifierProvider.value( + value: getPaymentMethodsProvider, + child: DraggableScrollableSheet( + initialChildSize: 0.9, + minChildSize: 0.5, + maxChildSize: 1, + builder: (_, scrollController) => Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + Navigator.of(sheetContext).pop(); + widget.onBackPress?.call(); + }, + ), + ], + ), + Expanded( + child: _SelectPaymentMethodContent( + primaryColor: widget.primaryColor, + onBackPress: () { + Navigator.of(sheetContext).pop(); + widget.onBackPress?.call(); + }, + amountToPay: widget.amountToPay, + tax: widget.tax, + totalAmount: widget.totalAmount, + transactionId: widget.transactionId, + onPaymentSuccess: widget.onPaymentSuccess, + organizationLogo: widget.organizationLogo, + isFullPage: widget.isFullPage, + onClose: () { + Navigator.of(sheetContext).pop(); + widget.onBackPress?.call(); + }, + ), + ), + ], + ), + ), + ), + ), + ).then((_) { + widget.onBackPress?.call(); + }); + } + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} + +/// Shows loading until [PayProvider] has payment request, then builds content. +class _DialogWithPayProvider extends StatelessWidget { + const _DialogWithPayProvider({ + required this.payProvider, + required this.builder, + }); + + final PayProvider payProvider; + final Widget Function(dynamic paymentRequest) builder; + + @override + Widget build(BuildContext context) { + return Builder( + builder: (context) { + final provider = context.watch(); + + if (provider.paymentRequest == null) { + if (provider.isLoading) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text(MoosylLocalization.of(context)?.sending ?? ''), + ], + ), + ), + ); + } + if (provider.error != null) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(provider.error.toString()), + const SizedBox(height: 16), + TextButton( + onPressed: provider.getPaymentRequest, + child: Text( + MoosylLocalization.of(context)?.retry ?? 'Retry'), + ), + ], + ), + ), + ); + } + } + + return provider.paymentRequest != null + ? builder(provider.paymentRequest!) + : const SizedBox.shrink(); + }, + ); + } +} + +class _MethodRow extends StatelessWidget { + const _MethodRow({ + required this.method, + required this.isSelected, + required this.primaryColor, + required this.onTap, + }); + + final PaymentMethod method; + final bool isSelected; + final Color primaryColor; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + final localizationHelper = MoosylLocalization.of(context)!; + final textTheme = Theme.of(context).textTheme; + + return InkWell( + onTap: onTap, + child: AppContainer( + padding: const EdgeInsetsDirectional.all(10), + border: + Border.all(color: isSelected ? primaryColor : Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + child: Row( + children: [ + // Icon - square bordered + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.antiAlias, + child: Center( + child: provider.customIcons?[method.method] != null + ? AppIcon( + path: provider.customIcons?[method.method], + size: 52, + ) + : method.method.icon.apply(size: 40), + ), + ), + const SizedBox(width: 16), + // Name + subtitle + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + method.method.title(context), + style: textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + isSelected + ? localizationHelper.selected + : localizationHelper.tapToUse, + style: textTheme.bodyMedium, + ), + ], + ), + ), + // Radio + Radio( + value: method, + groupValue: provider.pendingSelection, + onChanged: (_) => onTap(), + fillColor: MaterialStateProperty.all(primaryColor), + activeColor: primaryColor, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/pages/sedad_view.dart b/lib/src/pages/sedad_view.dart new file mode 100644 index 0000000..a0d3ccb --- /dev/null +++ b/lib/src/pages/sedad_view.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/src/models/payment_request_model.dart'; +import 'package:moosyl_flutter/src/providers/pay_provider.dart'; +import 'package:moosyl_flutter/src/widgets/buttons.dart'; +import 'package:moosyl_flutter/src/widgets/container.dart'; +import 'package:moosyl_flutter/src/widgets/feedback.dart'; +import 'package:moosyl_flutter/src/widgets/icons.dart'; + +import 'package:provider/provider.dart'; + +/// A dialog view for Sedad/Bim Bank payment flow. +/// +/// Displays payment code with copyable container, steps, and action buttons. +class SedadView extends StatelessWidget { + /// The payment code (merchant code) to display. + final String paymentCodeDisplay; + + /// The payment request containing amount and other details. + final PaymentRequestModel paymentRequest; + + /// Callback when the dialog is closed (e.g. to go back to payment method selection). + final VoidCallback? onClose; + + /// The primary color of the app. + final Color primaryColor; + + const SedadView({ + super.key, + required this.paymentCodeDisplay, + required this.paymentRequest, + this.onClose, + required this.primaryColor, + }); + + @override + Widget build(BuildContext context) { + final localizationHelper = MoosylLocalization.of(context)!; + final payProvider = context.watch(); + final platformIcon = payProvider.method.method.icon; + + return Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.symmetric(horizontal: 10), + shadowColor: Theme.of(context).colorScheme.outline, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Form( + key: payProvider.formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + localizationHelper.paymentCode, + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + localizationHelper.useThisCodeToCompletePayment, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Platform icon + Center( + child: AppContainer( + width: 200, + border: Border.all(color: Colors.grey.shade300), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: platformIcon.apply(size: 48), + ), + ), + const SizedBox(height: 20), + + // Copyable container with payment code only + GestureDetector( + onTap: () => Feedbacks.copy(paymentCodeDisplay, context), + child: AppContainer( + color: Colors.grey.shade200, + border: Border.all(color: Colors.grey.shade300), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text( + payProvider.method.bPayNumber, + style: TextStyle( + fontSize: 24, fontWeight: FontWeight.bold), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + AppIcons.copy.apply(size: 20), + Text(localizationHelper.clickToCopy, + style: Theme.of(context).textTheme.bodyMedium), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // Steps + _StepItem(text: localizationHelper.sedadStep1), + _StepItem(text: localizationHelper.sedadStep2), + _StepItem(text: localizationHelper.sedadStep3), + _StepItem(text: localizationHelper.sedadStep4), + // I've paid button + _IvePaidButton(primaryColor: primaryColor), + + // Change payment method button + AppButton( + primaryColor: primaryColor, + margin: const EdgeInsets.only(top: 0), + minHeight: 40, + style: AppButtonStyle.outline, + labelText: localizationHelper.changePaymentMethod, + onPressed: onClose ?? () => Navigator.of(context).pop(), + ), + ], + ), + ), + ), + ); + } +} + +class _StepItem extends StatelessWidget { + final String text; + + const _StepItem({required this.text}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium, + ), + ); + } +} + +class _IvePaidButton extends StatelessWidget { + final Color primaryColor; + + const _IvePaidButton({required this.primaryColor}); + + @override + Widget build(BuildContext context) { + final localizationHelper = MoosylLocalization.of(context)!; + final payProvider = context.watch(); + + return AppButton( + primaryColor: primaryColor, + minHeight: 40, + style: AppButtonStyle.primary, + labelText: localizationHelper.ivePaid, + loading: payProvider.isLoading, + onPressed: () => payProvider.pay(context), + ); + } +} diff --git a/lib/src/providers/get_payment_methods_provider.dart b/lib/src/providers/get_payment_methods_provider.dart index e2b420b..ba5e181 100644 --- a/lib/src/providers/get_payment_methods_provider.dart +++ b/lib/src/providers/get_payment_methods_provider.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:moosyl/src/helpers/exception_handling/error_handlers.dart'; -import 'package:moosyl/src/models/payment_method_model.dart'; -import 'package:moosyl/src/services/get_payment_methods_service.dart'; +import 'package:moosyl_flutter/src/helpers/exception_handling/error_handlers.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/models/payment_request_model.dart'; +import 'package:moosyl_flutter/src/services/get_payment_methods_service.dart'; +import 'package:moosyl_flutter/src/services/get_payment_request_service.dart'; /// A provider class for managing and retrieving payment methods. /// @@ -11,24 +13,37 @@ class GetPaymentMethodsProvider extends ChangeNotifier { /// The API key used for authentication with the payment methods service. final String publishableApiKey; + /// The transaction ID for payment request validation. + final String transactionId; + + /// The total amount to validate against payment request. + final double totalAmount; + /// A map of custom icons for specific payment method types. final Map? customIcons; - /// A callback function that gets called when a payment method is selected. + /// The payment method selected for the payment process. PaymentMethod? selected; - /// The payment method selected for the payment process. + /// Whether the app is in testing mode. final bool isTestingMode; - /// Constructs a [GetPaymentMethodsProvider]. + /// The payment method selected in the list (radio) before confirming with Pay. + PaymentMethod? pendingSelection; + + /// Error to show on payment method selection when validation fails. + String? selectionError; - /// Initiates fetching payment methods upon creation. + /// Constructs a [GetPaymentMethodsProvider]. GetPaymentMethodsProvider({ required this.publishableApiKey, + required this.transactionId, + required this.totalAmount, required this.isTestingMode, required this.customIcons, }) { getMethods(); + getPaymentRequest(); } /// Holds any error messages that occur during method retrieval. @@ -37,9 +52,71 @@ class GetPaymentMethodsProvider extends ChangeNotifier { /// Indicates whether the provider is currently loading data. bool isLoading = false; + /// Indicates whether validation is in progress (fetching payment request). + bool isValidating = false; + /// List of available payment methods. final List methods = []; + /// The payment request model. + PaymentRequestModel? paymentRequest; + + /// Clears the selection error. + void clearSelectionError() { + if (selectionError != null) { + selectionError = null; + notifyListeners(); + } + } + + /// Validates payment request and sets or returns the payment method. + /// For Sedad/Bankily: returns the method to show dialog (caller shows dialog). + /// For Masrivi etc: calls setPaymentMethod and returns null. + /// On validation error: returns null and sets selectionError. + Future setPaymentMethodWithValidation(PaymentMethod method) async { + selectionError = null; + isValidating = true; + notifyListeners(); + + final result = await ErrorHandlers.catchErrors( + () => GetPaymentRequestService(publishableApiKey).get(transactionId), + showFlashBar: false, + ); + + isValidating = false; + + if (result.isError) { + selectionError = result.error?.toString(); + notifyListeners(); + return null; + } + + final paymentRequest = result.result!; + + if (paymentRequest.amount == 0) { + selectionError = 'paymentRequestFullyPaid'; + notifyListeners(); + return null; + } + + if (totalAmount > 0 && (totalAmount - paymentRequest.amount).abs() > 0.01) { + selectionError = 'amountToPayShouldMatchPaymentRequest'; + notifyListeners(); + return null; + } + + final isDialogMethod = method.method == PaymentMethodTypes.sedad || + method.method == PaymentMethodTypes.bimBank || + method.method == PaymentMethodTypes.bankily; + + if (isDialogMethod) { + return method; + } + + setPaymentMethod(method); + return null; + } + /// Retrieves the list of supported payment method types. List get supportedTypes { return [...methods.map((method) => method.method)]; @@ -59,7 +136,7 @@ class GetPaymentMethodsProvider extends ChangeNotifier { notifyListeners(); final result = await ErrorHandlers.catchErrors( - () => GetPaymentMethodsService(publishableApiKey).get(isTestingMode), + () => GetPaymentMethodsService(publishableApiKey).get(), showFlashBar: false, ); @@ -73,7 +150,6 @@ class GetPaymentMethodsProvider extends ChangeNotifier { // Add the retrieved methods to the methods list. methods.addAll(result.result!); - // Notify listeners of the change in payment methods. notifyListeners(); } @@ -91,9 +167,36 @@ class GetPaymentMethodsProvider extends ChangeNotifier { setPaymentMethod(selected); } - /// Sets the selected payment method. + /// Sets the selected payment method (confirms and proceeds to payment). void setPaymentMethod(PaymentMethod? method) { selected = method; + pendingSelection = method; + notifyListeners(); + } + + /// Sets the pending selection (radio choice before Pay is tapped). + void setPendingSelection(PaymentMethod? method) { + pendingSelection = method; + clearSelectionError(); + notifyListeners(); + } + + /// Confirms the pending selection and proceeds to payment. + void confirmSelection() { + if (pendingSelection != null) { + setPaymentMethod(pendingSelection); + } + } + + /// Updates the payment request details and notifies listeners when the data changes. + void getPaymentRequest() async { + final result = await ErrorHandlers.catchErrors( + () => GetPaymentRequestService(publishableApiKey).get(transactionId), + showFlashBar: false, + ); + + paymentRequest = result.result; + notifyListeners(); } } diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index b7a112d..8d601ac 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -1,16 +1,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:moosyl/src/helpers/exception_handling/error_handlers.dart'; -import 'package:moosyl/src/models/payment_request_model.dart'; -import 'package:moosyl/src/models/payment_method_model.dart'; -import 'package:moosyl/src/services/get_payment_request_service.dart'; -import 'package:moosyl/src/services/pay_service.dart'; +import 'package:moosyl_flutter/src/helpers/exception_handling/error_handlers.dart'; +import 'package:moosyl_flutter/src/models/payment_request_model.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/services/get_payment_request_service.dart'; +import 'package:moosyl_flutter/src/services/pay_service.dart'; /// A provider class for handling payment payment requests. /// /// This class extends [ChangeNotifier] to notify listeners about changes /// in the payment payment request state, including loading status and errors. -class AutomaticPayProvider extends ChangeNotifier { +class PayProvider extends ChangeNotifier { /// The API key used for authentication with the payment services. final String publishableApiKey; @@ -23,10 +23,10 @@ class AutomaticPayProvider extends ChangeNotifier { /// Callback function that gets called on successful payment. final FutureOr Function()? onPaymentSuccess; - /// Constructs a [AutomaticPayProvider]. + /// Constructs a [PayProvider]. /// /// Initiates fetching the payment request details upon creation. - AutomaticPayProvider({ + PayProvider({ required this.publishableApiKey, required this.transactionId, this.onPaymentSuccess, diff --git a/lib/src/services/get_payment_methods_service.dart b/lib/src/services/get_payment_methods_service.dart index 76850a0..d50e15f 100644 --- a/lib/src/services/get_payment_methods_service.dart +++ b/lib/src/services/get_payment_methods_service.dart @@ -1,30 +1,63 @@ -import 'package:moosyl/src/helpers/fetcher.dart'; -import 'package:moosyl/src/models/payment_method_model.dart'; +import 'package:moosyl/moosyl.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; + +/// Base URL for the Moosyl API (matches the original fetcher endpoint). +const String _defaultBaseUrl = 'https://moosyl.moosyl.workers.dev'; /// A service class for fetching available payment methods. /// /// This class provides methods to interact with the backend and retrieve -/// a list of payment methods. +/// a list of payment methods using the Moosyl Dart SDK [ConfigurationApi]. class GetPaymentMethodsService { /// The API key used for authentication with the backend. final String publishableApiKey; + /// Optional base URL override (defaults to [_defaultBaseUrl]). + final String? baseUrlOverride; + /// Constructs a [GetPaymentMethodsService] with the provided [publishableApiKey]. - GetPaymentMethodsService(this.publishableApiKey); + GetPaymentMethodsService( + this.publishableApiKey, { + this.baseUrlOverride, + }); /// Fetches the available payment methods from the backend. /// - /// Makes an API call to retrieve the payment methods. + /// Makes an API call via [ConfigurationApi] to retrieve the payment methods. + /// Uses [isTestingMode] as a query parameter for the configuration endpoint. /// Returns a list of [PaymentMethod] objects. - Future> get(bool isTestingMode) async { - // Fetch the payment methods from the backend. - final methodsResult = await Fetcher(publishableApiKey).get( - Endpoints.paymentMethods(isTestingMode), + Future> get() async { + final client = Moosyl( + basePathOverride: baseUrlOverride ?? _defaultBaseUrl, ); - // Convert the result data to a list of PaymentMethod instances. - return List.from(methodsResult.data["data"]) - .map((e) => PaymentMethod.fromPaymentType(e)) + client.dio.options.headers['Authorization'] = publishableApiKey; + + // Use dio to pass isTestingMode (ConfigurationApi.getConfiguration() + // doesn't support query params, but the backend expects it) + final config = client.getConfigurationApi(); + final response = await config.getConfiguration(); + + final data = response.data; + if (data == null) { + return []; + } + + return data.data + .map((e) => PaymentMethod.fromPaymentType(_toMap(e))) .toList(); } + + /// Converts [ConfigurationListDataInner] to [Map] for [PaymentMethod.fromPaymentType]. + static Map _toMap(ConfigurationListDataInner inner) { + final config = inner.config; + final code = config != null && config.isMap + ? (config.asMap['code']?.toString() ?? '') + : ''; + return { + 'id': inner.id, + 'type': inner.type, + 'config': {'code': code}, + }; + } } diff --git a/lib/src/services/get_payment_request_service.dart b/lib/src/services/get_payment_request_service.dart index 4e388ee..401aac0 100644 --- a/lib/src/services/get_payment_request_service.dart +++ b/lib/src/services/get_payment_request_service.dart @@ -1,28 +1,52 @@ -import 'package:moosyl/src/helpers/fetcher.dart'; -import 'package:moosyl/src/models/payment_request_model.dart'; +import 'package:moosyl/moosyl.dart'; +import 'package:moosyl_flutter/src/models/payment_request_model.dart'; + +/// Base URL for the Moosyl API. +const String _defaultBaseUrl = 'https://moosyl.moosyl.workers.dev'; /// A service class for fetching Payment Request details. /// /// This class provides methods to interact with the backend and retrieve -/// Payment Request information based on the Payment Request ID. +/// Payment Request information using the Moosyl SDK [PaymentRequestApi]. class GetPaymentRequestService { /// The API key used for authentication with the backend. final String authorizations; + /// Optional base URL override. + final String? baseUrlOverride; + /// Constructs a [GetPaymentRequestService] with the provided [authorizations]. - GetPaymentRequestService(this.authorizations); + GetPaymentRequestService( + this.authorizations, { + this.baseUrlOverride, + }); /// Fetches Payment Request details for the specified [transactionId]. /// - /// Makes an API call to retrieve the Payment Request information. - /// Returns an [PaymentRequestModel] object containing the Payment Request details. + /// Makes an API call via [PaymentRequestApi] to retrieve the Payment Request. + /// Returns a [PaymentRequestModel] object containing the Payment Request details. Future get(String transactionId) async { - // Fetch the Payment Request details from the backend. - final methodsResult = await Fetcher(authorizations).get( - Endpoints.paymentRequest(transactionId), + final client = Moosyl( + basePathOverride: baseUrlOverride ?? _defaultBaseUrl, ); - // Convert the result data to an PaymentRequestModel instance. - return PaymentRequestModel.fromMap(methodsResult.data['data']); + client.dio.options.headers['Authorization'] = authorizations; + + final paymentRequestApi = client.getPaymentRequestApi(); + final response = + await paymentRequestApi.getPaymentRequestByTransactionByTransactionId( + transactionId: transactionId, + ); + + final data = response.data?.data; + if (data == null) { + throw StateError('Payment request not found'); + } + + return PaymentRequestModel( + id: data.id, + phoneNumber: data.phoneNumber, + amount: data.amount.toDouble(), + ); } } diff --git a/lib/src/services/pay_service.dart b/lib/src/services/pay_service.dart index a3a5f6f..8619771 100644 --- a/lib/src/services/pay_service.dart +++ b/lib/src/services/pay_service.dart @@ -1,21 +1,26 @@ -import 'dart:convert'; +import 'package:moosyl/moosyl.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:mime/mime.dart'; -import 'package:moosyl/src/helpers/fetcher.dart'; +/// Base URL for the Moosyl API. +const String _defaultBaseUrl = 'https://moosyl.moosyl.workers.dev'; /// A service class for processing payments. /// /// This class provides methods to interact with the backend and -/// execute payment transactions. +/// execute payment transactions using the Moosyl SDK [PaymentApi]. class PayService { /// The API key used for authentication with the backend. final String publishableApiKey; + /// Optional base URL override. + final String? baseUrlOverride; + /// Constructs a [PayService] with the provided [publishableApiKey]. - PayService(this.publishableApiKey); + PayService( + this.publishableApiKey, { + this.baseUrlOverride, + }); - /// Processes a payment transaction. + /// Processes a payment transaction via [PaymentApi]. /// /// This method makes an API call to process a payment using the specified /// parameters. It requires the Transaction ID, phone number, passcode, @@ -25,53 +30,26 @@ class PayService { required String phoneNumber, required String passCode, required String paymentMethodId, - }) { - // Make a POST request to the payment methods endpoint with the payment details. + }) async { + final client = _createClient(); + final paymentApi = client.getPaymentApi(); - return Fetcher(publishableApiKey).post( - Endpoints.pay, - body: { - 'transactionId': transactionId, - 'phoneNumber': phoneNumber, - 'passCode': passCode, - 'configurationId': paymentMethodId, - }, + final paymentCreate = PaymentCreate( + (b) => b + ..configurationId = paymentMethodId + ..transactionId = transactionId + ..phoneNumber = phoneNumber + ..passCode = passCode, ); - } - /// Manually processes a payment by making a POST request to the payment methods endpoint. - /// - /// This method sends the payment details including the transaction ID, payment method ID, - /// and a screenshot of the payment to the server for processing. - /// - /// Parameters: - /// - `transactionId` (required): The unique identifier for the transaction. - /// - `paymentMethodId` (required): The identifier for the selected payment method. - /// - `selectedImage` (required): A screenshot of the payment as a `PlatformFile`. - /// - /// Returns: - /// - A `Future` that completes when the payment has been processed. + await paymentApi.postPayment(paymentCreate: paymentCreate); + } - Future manualPay({ - required String transactionId, - required String paymentMethodId, - required PlatformFile selectedImage, - }) async { - // Make a POST request to the payment methods endpoint with the payment details. - Fetcher(publishableApiKey).post( - Endpoints.manualPayment, - body: { - 'transactionId': transactionId, - 'configurationId': paymentMethodId, - 'attachments': [ - { - "name": selectedImage.name, - "type": lookupMimeType(selectedImage.name), - "size": selectedImage.size, - "data": base64Encode(await selectedImage.xFile.readAsBytes()), - } - ] - }, + Moosyl _createClient() { + final client = Moosyl( + basePathOverride: baseUrlOverride ?? _defaultBaseUrl, ); + client.dio.options.headers['Authorization'] = publishableApiKey; + return client; } } diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index c1cda0f..c627a46 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -1,6 +1,18 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +/// Visual style variants for [AppButton]. +enum AppButtonStyle { + /// Filled button with primary background. + primary, + + /// Outlined button with transparent background and border. + outline, + + /// Disabled/grayed-out appearance. + disabled, +} + class AppButton extends StatelessWidget { final String labelText; final Widget? leading; @@ -8,6 +20,7 @@ class AppButton extends StatelessWidget { final EdgeInsetsGeometry margin; final bool disabled, loading; + final AppButtonStyle style; final BorderRadius? borderRadius; final Color? background; final Color? textColor; @@ -15,6 +28,7 @@ class AppButton extends StatelessWidget { final double? minHeight; final EdgeInsetsGeometry? padding; final BorderSide border; + final Color? primaryColor; const AppButton({ super.key, @@ -24,6 +38,7 @@ class AppButton extends StatelessWidget { this.margin = const EdgeInsets.only(top: 16), this.disabled = false, this.loading = false, + this.style = AppButtonStyle.primary, this.borderRadius, this.background, this.textColor, @@ -31,27 +46,51 @@ class AppButton extends StatelessWidget { this.padding, this.minHeight = 60, this.border = BorderSide.none, + this.primaryColor, }); + bool get _isDisabled => + disabled || loading || style == AppButtonStyle.disabled; + @override Widget build(BuildContext context) { - final background = loading || disabled - ? Theme.of(context).colorScheme.surface - : (this.background ?? Theme.of(context).colorScheme.primary); + final colorScheme = Theme.of(context).colorScheme; + final primary = colorScheme.primary; + final onPrimary = colorScheme.onPrimary; + final surface = colorScheme.surface; + final onSurface = colorScheme.onSurface; + final outlineColor = colorScheme.outline; + + Color bg; + Color fg; + BorderSide side; - final foregroundColor = loading || disabled - ? Theme.of(context).colorScheme.onSurface - : textColor ?? Theme.of(context).colorScheme.onPrimary; + if (_isDisabled) { + bg = surface; + fg = onSurface.withOpacity(0.6); + side = BorderSide(color: outlineColor.withOpacity(0.5)); + } else if (style == AppButtonStyle.outline) { + bg = Colors.transparent; + fg = this.textColor ?? primaryColor ?? primary; + side = border == BorderSide.none + ? BorderSide(color: primaryColor ?? primary) + : border; + } else { + // primary + bg = this.background ?? primaryColor ?? primary; + fg = textColor ?? onPrimary; + side = border; + } final buttonStyle = ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: borderRadius ?? BorderRadius.circular(8), - side: border, + side: side, ), - backgroundColor: background, - foregroundColor: foregroundColor, - textStyle: Theme.of(context).textTheme.titleSmall!.copyWith( - fontSize: 14, + backgroundColor: bg, + foregroundColor: fg, + textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, ), elevation: 0, minimumSize: const Size(0, 40), @@ -60,21 +99,29 @@ class AppButton extends StatelessWidget { final button = loading || leading != null ? ElevatedButton.icon( - onPressed: disabled || loading ? () {} : onPressed ?? () {}, + onPressed: _isDisabled ? () {} : onPressed ?? () {}, icon: loading ? SizedBox( width: 24, height: 24, - child: CircularProgressIndicator(color: foregroundColor), + child: CircularProgressIndicator(color: fg), ) : leading, style: buttonStyle, - label: Text(labelText), + label: Text(labelText, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: fg, + )), ) : ElevatedButton( - onPressed: disabled || loading ? () {} : onPressed ?? () {}, + onPressed: _isDisabled ? () {} : onPressed ?? () {}, style: buttonStyle, - child: Text(labelText), + child: Text(labelText, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: fg, + )), ); return Padding( diff --git a/lib/src/widgets/error_widget.dart b/lib/src/widgets/error_widget.dart index 90f0a7f..3ca92d9 100644 --- a/lib/src/widgets/error_widget.dart +++ b/lib/src/widgets/error_widget.dart @@ -1,8 +1,8 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; -import 'package:moosyl/src/widgets/icons.dart'; +import 'package:moosyl_flutter/src/widgets/icons.dart'; class AppErrorWidget extends StatelessWidget { final String? message; diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index a4a37c2..ab21f27 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -1,9 +1,9 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; -import 'package:moosyl/src/widgets/icons.dart'; +import 'package:moosyl_flutter/src/widgets/icons.dart'; class Feedbacks { static void flushBar({ diff --git a/lib/src/widgets/icons.dart b/lib/src/widgets/icons.dart index fa6ad37..903a2ff 100644 --- a/lib/src/widgets/icons.dart +++ b/lib/src/widgets/icons.dart @@ -1,7 +1,7 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:moosyl/src/widgets/container.dart'; +import 'package:moosyl_flutter/src/widgets/container.dart'; class AppIcon extends StatelessWidget { final String? path; @@ -96,37 +96,38 @@ class AppIcon extends StatelessWidget { class AppIcons { //payment methods static const bankily = AppIcon( - path: 'packages/moosyl/assets/icons/bankily.png', + path: 'packages/moosyl_flutter/assets/icons/bankily.png', useOriginalColor: true, ); //masrivi static const masrivi = AppIcon( - path: 'packages/moosyl/assets/icons/masrivi.png', + path: 'packages/moosyl_flutter/assets/icons/masrivi.png', useOriginalColor: true, ); //sedad static const sedad = AppIcon( - path: 'packages/moosyl/assets/icons/sedad.png', + path: 'packages/moosyl_flutter/assets/icons/sedad.png', useOriginalColor: true, ); //bimBank static const bimBank = AppIcon( - path: 'packages/moosyl/assets/icons/bim_bank.png', + path: 'packages/moosyl_flutter/assets/icons/bim_bank.png', useOriginalColor: true, ); //amanty static const amanty = AppIcon( - path: 'packages/moosyl/assets/icons/amanty.png', + path: 'packages/moosyl_flutter/assets/icons/amanty.png', useOriginalColor: true, ); //bCIpay static const bCIpay = AppIcon( - path: 'packages/moosyl/assets/icons/bci_pay.png', + path: 'packages/moosyl_flutter/assets/icons/bci_pay.png', useOriginalColor: true, ); //error - static const error = AppIcon(path: 'packages/moosyl/assets/icons/error.png'); + static const error = + AppIcon(path: 'packages/moosyl_flutter/assets/icons/error.png'); //closeError ,doneError static const done = AppIcon(iconData: Icons.done); diff --git a/lib/src/widgets/pick_image.dart b/lib/src/widgets/pick_image.dart deleted file mode 100644 index 4c8ce09..0000000 --- a/lib/src/widgets/pick_image.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:moosyl/l10n/generated/moosyl_localization.dart'; -import 'package:moosyl/src/helpers/exception_handling/error_handlers.dart'; -import 'package:moosyl/src/widgets/buttons.dart'; -import 'package:moosyl/src/widgets/container.dart'; -import 'package:moosyl/src/widgets/icons.dart'; - -class PickImageCard extends StatefulWidget { - final String? label; - final void Function(PlatformFile?) onChanged; - - const PickImageCard({super.key, this.label, required this.onChanged}); - - @override - State createState() => _PickImageCardState(); -} - -class _PickImageCardState extends State { - PlatformFile? selectedFile; - - Future _pickImage({ - required bool isCamera, - required BuildContext context, - }) async { - final response = await ErrorHandlers.catchErrors( - () => FilePicker.platform.pickFiles( - allowMultiple: false, - type: FileType.image, - ), - ); - - if (response.isError) return; - - selectedFile = response.result!.files.first; - widget.onChanged(selectedFile!); - } - - @override - Widget build(BuildContext context) { - const size = 300.0; - final textTheme = Theme.of(context).textTheme; - // final style = ElevatedButton.styleFrom( - // minimumSize: const Size(200, 60), // Fixed size (width, height) - // ); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.label != null) ...[ - Text( - widget.label!, - style: textTheme.headlineMedium, - ), - const SizedBox(height: 16), - ], - if (selectedFile == null) - AppContainer( - padding: const EdgeInsets.all(16), - color: Theme.of(context).colorScheme.surface, - child: Row( - children: [ - Expanded( - child: AppButton( - minHeight: 40, - textColor: Theme.of(context).colorScheme.onSurface, - leading: AppIcons.cloud, - background: Theme.of(context).colorScheme.onPrimary, - labelText: MoosylLocalization.of(context)!.upload, - onPressed: () => _pickImage( - isCamera: false, - context: context, - ), - )), - const SizedBox(width: 16), - Expanded( - child: AppButton( - minHeight: 40, - textColor: Theme.of(context).colorScheme.onSurface, - leading: AppIcons.pic, - background: Theme.of(context).colorScheme.onPrimary, - labelText: MoosylLocalization.of(context)!.capture, - onPressed: () => _pickImage( - isCamera: true, - context: context, - ), - ), - ) - ], - ), - ), - if (selectedFile != null) - Container( - padding: const EdgeInsets.only(top: 16), - height: size, - child: Stack( - children: [ - Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: SizedBox( - width: 215, - child: AppPlatformFileView(file: selectedFile!), - ), - )), - Center( - child: IconButton( - icon: AppIcons.delete.apply( - size: 44, - color: Theme.of(context).colorScheme.surface, - ), - onPressed: () { - selectedFile = null; - widget.onChanged(selectedFile); - }, - ), - ), - ], - ), - ), - ], - ), - ); - } -} - -class AppPlatformFileView extends StatelessWidget { - final PlatformFile file; - - const AppPlatformFileView({super.key, required this.file}); - - @override - Widget build(BuildContext context) { - if (file.bytes != null) { - return Image.memory( - file.bytes!, - fit: BoxFit.fill, - width: double.infinity, - height: double.infinity, - ); - } else if (file.path != null) { - return Image.asset( - file.path!, - fit: BoxFit.fill, - width: double.infinity, - height: double.infinity, - ); - } else { - return const Center(child: Text('No image available')); - } - } -} diff --git a/lib/src/widgets/text_input.dart b/lib/src/widgets/text_input.dart index df379dc..5b4858c 100644 --- a/lib/src/widgets/text_input.dart +++ b/lib/src/widgets/text_input.dart @@ -1,6 +1,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:moosyl/src/widgets/icons.dart'; +import 'package:moosyl_flutter/src/widgets/icons.dart'; class AppTextInput extends StatelessWidget { final String label; @@ -94,23 +94,18 @@ class InputLabel extends StatelessWidget { @override Widget build(BuildContext context) { - const horizontalPadding = EdgeInsetsDirectional.symmetric(horizontal: 16); - - return Padding( - padding: horizontalPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - label, - style: style, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + label, + style: style, ), - child, - ], - ), + ), + child, + ], ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 1229edb..f0f25d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: moosyl +name: moosyl_flutter description: "The Moosyl Flutter SDK is a powerful tool for integrating payment solutions with Mauritania's popular banking apps, such as Bankily, Sedad, and Masrivi" homepage: https://github.com/SoftwareSavants/moosyl_flutter @@ -22,6 +22,8 @@ dependencies: modal_bottom_sheet: ^3.0.0 file_picker: ^8.1.2 mime: ^2.0.0 + moosyl: ^1.1.1 + webview_flutter: ^4.10.0 dev_dependencies: flutter_test: From 5e73417f640f12fb1ff313aac999e82040de91c8 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Fri, 27 Feb 2026 17:51:04 +0000 Subject: [PATCH 02/27] Refactor README and codebase by renaming 'moosyl' to 'moosyl_flutter', removing 'isTestingMode' references across multiple files, and updating import statements accordingly. --- README.md | 4 ++-- example/lib/main.dart | 1 - lib/src/pages/home.dart | 10 ---------- lib/src/pages/moosyl_view.dart | 5 ----- lib/src/providers/get_payment_methods_provider.dart | 7 ++----- lib/src/services/get_payment_methods_service.dart | 3 --- 6 files changed, 4 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index be45909..2b4f67f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Add the **Moosyl** package to your `pubspec.yaml`: ```yaml dependencies: - moosyl: ^latest_version + moosyl_flutter: ^latest_version ``` Fetch the package: @@ -36,7 +36,7 @@ flutter pub get ### Import the Package ```dart -import 'package:moosyl/moosyl.dart'; +import 'package:moosyl_flutter/moosyl.dart'; ``` --- diff --git a/example/lib/main.dart b/example/lib/main.dart index 54fd3ab..0a1ab15 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -52,7 +52,6 @@ class _MyHomePageState extends State { publishableApiKey: 'your publishable api key', transactionId: 'your transaction id', organizationLogo: const Text('Moosyl'), - isTestingMode: true, primaryColor: Colors.red, onBackPress: () => print('Back pressed'), // amountToPay: 5, diff --git a/lib/src/pages/home.dart b/lib/src/pages/home.dart index b1dcd31..be7d06f 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/pages/home.dart @@ -29,11 +29,6 @@ class Moosyl extends HookWidget { /// Optional callback to be triggered upon successful payment. final FutureOr Function()? onPaymentSuccess; - /// manual payment - - /// The payment method selected for the payment process. - final bool isTestingMode; - /// When true, shows as full page. When false, shows as bottom sheet. final bool isFullPage; @@ -52,7 +47,6 @@ class Moosyl extends HookWidget { Widget? organizationLogo, final FutureOr Function()? onPaymentSuccess, Map? customIcons, - bool isTestingMode = false, bool isFullPage = false, }) { showBarModalBottomSheet( @@ -63,7 +57,6 @@ class Moosyl extends HookWidget { organizationLogo: organizationLogo, onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, - isTestingMode: isTestingMode, isFullPage: isFullPage, ), ); @@ -85,7 +78,6 @@ class Moosyl extends HookWidget { this.customIcons, this.inputBuilder, this.onPaymentSuccess, - this.isTestingMode = false, this.isFullPage = false, }); @@ -103,7 +95,6 @@ class Moosyl extends HookWidget { organizationLogo: organizationLogo, onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, - isTestingMode: isTestingMode, isFullPage: isFullPage, ), ); @@ -118,7 +109,6 @@ class Moosyl extends HookWidget { organizationLogo: organizationLogo, onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, - isTestingMode: isTestingMode, isFullPage: isFullPage, ); } diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index 8924ce4..ca5cb7c 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -23,7 +23,6 @@ class MoosylView extends StatelessWidget { this.organizationLogo, this.onPaymentSuccess, this.customIcons, - this.isTestingMode = false, this.primaryColor, this.onBackPress, this.amountToPay = 0.0, @@ -46,9 +45,6 @@ class MoosylView extends StatelessWidget { /// Optional custom icons for different payment methods. final Map? customIcons; - /// A flag to indicate whether the widget is in testing mode. - final bool isTestingMode; - /// Primary color for the payment method selection page (radio, pay button). final Color? primaryColor; @@ -74,7 +70,6 @@ class MoosylView extends StatelessWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, totalAmount: amountToPay + tax, - isTestingMode: isTestingMode, customIcons: customIcons, ), ), diff --git a/lib/src/providers/get_payment_methods_provider.dart b/lib/src/providers/get_payment_methods_provider.dart index ba5e181..a0ff4d3 100644 --- a/lib/src/providers/get_payment_methods_provider.dart +++ b/lib/src/providers/get_payment_methods_provider.dart @@ -25,9 +25,6 @@ class GetPaymentMethodsProvider extends ChangeNotifier { /// The payment method selected for the payment process. PaymentMethod? selected; - /// Whether the app is in testing mode. - final bool isTestingMode; - /// The payment method selected in the list (radio) before confirming with Pay. PaymentMethod? pendingSelection; @@ -39,7 +36,6 @@ class GetPaymentMethodsProvider extends ChangeNotifier { required this.publishableApiKey, required this.transactionId, required this.totalAmount, - required this.isTestingMode, required this.customIcons, }) { getMethods(); @@ -73,7 +69,8 @@ class GetPaymentMethodsProvider extends ChangeNotifier { /// For Sedad/Bankily: returns the method to show dialog (caller shows dialog). /// For Masrivi etc: calls setPaymentMethod and returns null. /// On validation error: returns null and sets selectionError. - Future setPaymentMethodWithValidation(PaymentMethod method) async { + Future setPaymentMethodWithValidation( + PaymentMethod method) async { selectionError = null; isValidating = true; notifyListeners(); diff --git a/lib/src/services/get_payment_methods_service.dart b/lib/src/services/get_payment_methods_service.dart index d50e15f..bb471a7 100644 --- a/lib/src/services/get_payment_methods_service.dart +++ b/lib/src/services/get_payment_methods_service.dart @@ -24,7 +24,6 @@ class GetPaymentMethodsService { /// Fetches the available payment methods from the backend. /// /// Makes an API call via [ConfigurationApi] to retrieve the payment methods. - /// Uses [isTestingMode] as a query parameter for the configuration endpoint. /// Returns a list of [PaymentMethod] objects. Future> get() async { final client = Moosyl( @@ -33,8 +32,6 @@ class GetPaymentMethodsService { client.dio.options.headers['Authorization'] = publishableApiKey; - // Use dio to pass isTestingMode (ConfigurationApi.getConfiguration() - // doesn't support query params, but the backend expects it) final config = client.getConfigurationApi(); final response = await config.getConfiguration(); From d236effd2840e58ef2cb31e3fd459f67a95c760b Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Fri, 27 Feb 2026 17:57:05 +0000 Subject: [PATCH 03/27] Refactor payment components by removing organization logo references from multiple files and updating the README to reflect changes in the MoosylView configuration. --- README.md | 3 ++- example/lib/main.dart | 1 - lib/src/pages/bankily_view.dart | 2 -- lib/src/pages/home.dart | 10 ---------- lib/src/pages/moosyl_view.dart | 7 +------ lib/src/pages/payment_methods_view.dart | 12 ------------ 6 files changed, 3 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 2b4f67f..76f2ac6 100644 --- a/README.md +++ b/README.md @@ -119,10 +119,11 @@ class PaymentScreen extends StatelessWidget { return MoosylView( publishableApiKey: 'YOUR_PUBLISHABLE_API_KEY', transactionId: 'TRANSACTION_ID', // Retrieved from your backend - organizationLogo: const Text('Your Logo Here'), + primaryColor: Colors.red, onPaymentSuccess: () { print('Payment was successful!'); }, + onBackPress: () => print('Back pressed'), ); } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 0a1ab15..bf60a4f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -51,7 +51,6 @@ class _MyHomePageState extends State { return MoosylView( publishableApiKey: 'your publishable api key', transactionId: 'your transaction id', - organizationLogo: const Text('Moosyl'), primaryColor: Colors.red, onBackPress: () => print('Back pressed'), // amountToPay: 5, diff --git a/lib/src/pages/bankily_view.dart b/lib/src/pages/bankily_view.dart index 11421b8..0a1333a 100644 --- a/lib/src/pages/bankily_view.dart +++ b/lib/src/pages/bankily_view.dart @@ -19,7 +19,6 @@ class BankilyView extends StatelessWidget { required this.method, required this.publishableApiKey, required this.transactionId, - this.organizationLogo, this.onPaymentSuccess, this.onClose, required this.primaryColor, @@ -28,7 +27,6 @@ class BankilyView extends StatelessWidget { final PaymentMethod method; final String publishableApiKey; final String transactionId; - final Widget? organizationLogo; final FutureOr Function()? onPaymentSuccess; final VoidCallback? onClose; final Color primaryColor; diff --git a/lib/src/pages/home.dart b/lib/src/pages/home.dart index be7d06f..ff41923 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/pages/home.dart @@ -15,9 +15,6 @@ class Moosyl extends HookWidget { /// The transaction ID associated with the current payment session. final String transactionId; - /// The logo of the organization processing the payment. - final Widget? organizationLogo; - /// Optional function to build a custom input widget for the payment process. /// [open] is the callback to open the payment sheet. final Widget Function(VoidCallback open)? inputBuilder; @@ -37,14 +34,12 @@ class Moosyl extends HookWidget { /// * [context]: The build context. /// * [publishableApiKey]: The API key to authenticate the payment. /// * [transactionId]: The transaction ID for the current session. - /// * [organizationLogo]: The logo widget of the organization. /// * [customIcons]: Map for custom icons for payment methods. /// * [onPaymentSuccess]: Callback for when payment is successful. static void show( BuildContext context, { required String publishableApiKey, required String transactionId, - Widget? organizationLogo, final FutureOr Function()? onPaymentSuccess, Map? customIcons, bool isFullPage = false, @@ -54,7 +49,6 @@ class Moosyl extends HookWidget { builder: (context) => MoosylView( publishableApiKey: publishableApiKey, transactionId: transactionId, - organizationLogo: organizationLogo, onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, isFullPage: isFullPage, @@ -66,7 +60,6 @@ class Moosyl extends HookWidget { /// /// * [publishableApiKey]: The API key for payment authentication. /// * [transactionId]: The transaction ID for the current payment session. - /// * [organizationLogo]: The logo of the organization handling the payment. /// * [customIcons]: Optional custom icons for specific payment methods. /// * [inputBuilder]: A function to build a custom input widget. /// * [onPaymentSuccess]: Callback when the payment is successful. @@ -74,7 +67,6 @@ class Moosyl extends HookWidget { super.key, required this.publishableApiKey, required this.transactionId, - this.organizationLogo, this.customIcons, this.inputBuilder, this.onPaymentSuccess, @@ -92,7 +84,6 @@ class Moosyl extends HookWidget { builder: (context) => MoosylView( publishableApiKey: publishableApiKey, transactionId: transactionId, - organizationLogo: organizationLogo, onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, isFullPage: isFullPage, @@ -106,7 +97,6 @@ class Moosyl extends HookWidget { return MoosylView( publishableApiKey: publishableApiKey, transactionId: transactionId, - organizationLogo: organizationLogo, onPaymentSuccess: onPaymentSuccess, customIcons: customIcons, isFullPage: isFullPage, diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index ca5cb7c..cb4ccbf 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -14,13 +14,12 @@ class MoosylView extends StatelessWidget { /// Creates an instance of [MoosylView]. /// Requires the [publishableApiKey] and [transactionId] for the payment transaction, - /// an [organizationLogo] to display, and optional handlers for custom payment methods, + /// and optional handlers for custom payment methods, /// success callbacks, and custom icons. const MoosylView({ super.key, required this.publishableApiKey, required this.transactionId, - this.organizationLogo, this.onPaymentSuccess, this.customIcons, this.primaryColor, @@ -36,9 +35,6 @@ class MoosylView extends StatelessWidget { /// The transaction ID for the specific payment transaction. final String transactionId; - /// A widget representing the logo of the organization. - final Widget? organizationLogo; - /// Optional callback that is invoked when the payment is successful. final FutureOr Function()? onPaymentSuccess; @@ -91,7 +87,6 @@ class MoosylView extends StatelessWidget { totalAmount: amountToPay + tax, transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, - organizationLogo: organizationLogo, isFullPage: isFullPage, ); } else { diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 2bf264c..70f9cf7 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -30,7 +30,6 @@ class SelectPaymentMethodPage extends StatelessWidget { this.totalAmount = 0.0, required this.transactionId, this.onPaymentSuccess, - this.organizationLogo, required this.isFullPage, }); @@ -58,9 +57,6 @@ class SelectPaymentMethodPage extends StatelessWidget { /// Callback when payment succeeds (for Sedad/Bankily dialogs). final FutureOr Function()? onPaymentSuccess; - /// Organization logo (for Bankily). - final Widget? organizationLogo; - @override Widget build(BuildContext context) { if (isFullPage) { @@ -72,7 +68,6 @@ class SelectPaymentMethodPage extends StatelessWidget { totalAmount: totalAmount, transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, - organizationLogo: organizationLogo, isFullPage: isFullPage, ); } else { @@ -84,7 +79,6 @@ class SelectPaymentMethodPage extends StatelessWidget { totalAmount: totalAmount, transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, - organizationLogo: organizationLogo, isFullPage: isFullPage, ); } @@ -101,7 +95,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { required this.totalAmount, required this.transactionId, required this.onPaymentSuccess, - required this.organizationLogo, required this.isFullPage, this.onClose, }); @@ -113,7 +106,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { final double totalAmount; final String transactionId; final FutureOr Function()? onPaymentSuccess; - final Widget? organizationLogo; final bool isFullPage; final VoidCallback? onClose; @@ -394,7 +386,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { method: method, publishableApiKey: publishableApiKey, transactionId: transactionId, - organizationLogo: organizationLogo, onClose: () { Navigator.of(dialogContext).pop(); getPaymentMethodsProvider.setPaymentMethod(null); @@ -418,7 +409,6 @@ class _SelectPaymentMethodBottomSheetTrigger extends StatefulWidget { required this.totalAmount, required this.transactionId, required this.onPaymentSuccess, - required this.organizationLogo, required this.isFullPage, }); @@ -429,7 +419,6 @@ class _SelectPaymentMethodBottomSheetTrigger extends StatefulWidget { final double totalAmount; final String transactionId; final FutureOr Function()? onPaymentSuccess; - final Widget? organizationLogo; final bool isFullPage; @override State<_SelectPaymentMethodBottomSheetTrigger> createState() => @@ -490,7 +479,6 @@ class _SelectPaymentMethodBottomSheetTriggerState totalAmount: widget.totalAmount, transactionId: widget.transactionId, onPaymentSuccess: widget.onPaymentSuccess, - organizationLogo: widget.organizationLogo, isFullPage: widget.isFullPage, onClose: () { Navigator.of(sheetContext).pop(); From 29ceb3a34075b8839b19d54af1aae2e4186207b5 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Fri, 27 Feb 2026 18:02:02 +0000 Subject: [PATCH 04/27] Update README to clarify package name from 'Moosyl' to 'Moosyl Flutter' for better accuracy in installation instructions. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76f2ac6..b36516c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The **Moosyl Flutter SDK** is a powerful tool for integrating payment solutions ### Installation -Add the **Moosyl** package to your `pubspec.yaml`: +Add the **Moosyl Flutter** package to your `pubspec.yaml`: ```yaml dependencies: From c560171177c1e66dacb40cdf4f089e11dcf964b6 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Fri, 27 Feb 2026 18:06:05 +0000 Subject: [PATCH 05/27] Add `onBackPress` event handler to README for MoosylView configuration --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b36516c..d8d0cdc 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ This example showcases the essential **MoosylView** widget, where you can config - **`publishableApiKey`**: Your Moosyl publishable API Key. - **`transactionId`**: The unique transaction identifier returned by your backend. - **`onPaymentSuccess`**: Handle successful payment events. +- **`onBackPress`**: Handle back press events. For detailed API documentation, visit the [Moosyl API Documentation](https://pub.dev/documentation/moosyl/latest/moosyl/moosyl-library.html). From 1a9e1f00179c57fc638e306f3ca0aa0c96537900 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Fri, 27 Feb 2026 18:32:09 +0000 Subject: [PATCH 06/27] Remove outdated transactionId reference from README to streamline payment request instructions. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index d8d0cdc..9ab6115 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,6 @@ async function createPaymentRequest() { createPaymentRequest(); ``` -Once the payment request is created, the backend will return details including the **transactionId**, which you will pass to the Flutter app for further processing. - --- ### Step 2: Display the Payment View From 6ee2603efc1b4126f7acbbefc5453560dd74d422 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Fri, 27 Feb 2026 23:20:42 +0000 Subject: [PATCH 07/27] Update dependencies and localization strings, refactor payment method models, and streamline payment request handling. Bump 'moosyl' version to 1.1.2 and remove unused payment request model. --- example/pubspec.lock | 4 +- lib/l10n/generated/moosyl_localization.dart | 12 ++++++ .../generated/moosyl_localization_fr.dart | 3 +- lib/src/models/payment_method_model.dart | 41 ------------------- lib/src/models/payment_request_model.dart | 33 --------------- lib/src/pages/bankily_view.dart | 12 +++--- lib/src/pages/payment_methods_view.dart | 35 ++++++++++------ lib/src/pages/sedad_view.dart | 11 +++-- .../get_payment_methods_provider.dart | 36 +++++++++------- lib/src/providers/pay_provider.dart | 7 ++-- .../services/get_payment_methods_service.dart | 22 ++-------- .../services/get_payment_request_service.dart | 13 ++---- lib/src/services/pay_service.dart | 2 +- pubspec.yaml | 2 +- 14 files changed, 84 insertions(+), 149 deletions(-) delete mode 100644 lib/src/models/payment_request_model.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index 7e3f72e..7152e05 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -265,10 +265,10 @@ packages: dependency: transitive description: name: moosyl - sha256: d20d5bd50de03cf89c821b63bdc948424794624424ed786e5ab54bf653faaf7e + sha256: "2caac058159e4399650d42ce76d5d1c589435e3e90de07aa59e9f739699005f4" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" moosyl_flutter: dependency: "direct main" description: diff --git a/lib/l10n/generated/moosyl_localization.dart b/lib/l10n/generated/moosyl_localization.dart index b948819..d0ee3af 100644 --- a/lib/l10n/generated/moosyl_localization.dart +++ b/lib/l10n/generated/moosyl_localization.dart @@ -498,15 +498,27 @@ abstract class MoosylLocalization { String get clickToCopy; /// No description provided for @bankilyStep1. + /// + /// In en, this message translates to: + /// **'1. Open the Bankily app'** String get bankilyStep1; /// No description provided for @bankilyStep2. + /// + /// In en, this message translates to: + /// **'2. Go to BPay'** String get bankilyStep2; /// No description provided for @bankilyStep3. + /// + /// In en, this message translates to: + /// **'3. Enter the BPay code and amount to pay'** String get bankilyStep3; /// No description provided for @bankilyStep4. + /// + /// In en, this message translates to: + /// **'4. Return here, enter your phone and passcode, then click Pay'** String get bankilyStep4; } diff --git a/lib/l10n/generated/moosyl_localization_fr.dart b/lib/l10n/generated/moosyl_localization_fr.dart index 8e7d2fe..bf055e9 100644 --- a/lib/l10n/generated/moosyl_localization_fr.dart +++ b/lib/l10n/generated/moosyl_localization_fr.dart @@ -232,8 +232,7 @@ class MoosylLocalizationFr extends MoosylLocalization { String get bankilyStep2 => '2. Allez dans BPay'; @override - String get bankilyStep3 => - '3. Saisissez le code BPay et le montant à payer'; + String get bankilyStep3 => '3. Saisissez le code BPay et le montant à payer'; @override String get bankilyStep4 => diff --git a/lib/src/models/payment_method_model.dart b/lib/src/models/payment_method_model.dart index c13499b..be9ce9e 100644 --- a/lib/src/models/payment_method_model.dart +++ b/lib/src/models/payment_method_model.dart @@ -80,44 +80,3 @@ enum PaymentMethodTypes { ); } } - -/// Abstract class representing a payment method. -/// -/// Model representing the configuration for the Bankily payment method. -class PaymentMethod { - /// The unique identifier for the payment method. - final String id; - - /// The type of the payment method. - final PaymentMethodTypes method; - - /// The BPay number associated with the Bankily payment method. - final String bPayNumber; - - /// Creates an instance of [PaymentMethod]. - /// - /// Requires [id] and [method] to initialize the model. - PaymentMethod({ - required this.id, - required this.method, - required this.bPayNumber, - }); - - /// Creates an instance of [PaymentMethod] from a map. - /// - /// The [map] must contain the keys 'id' and 'method' to initialize the properties. - PaymentMethod.fromMap(Map map) - : id = map['id'], - method = PaymentMethodTypes.fromString(map['type']), - bPayNumber = map['config']['code']; - - /// Creates a [PaymentMethod] instance from the provided type. - /// - /// The [map] must contain the key 'method' to determine the specific type - /// of payment method to create. - /// - /// Throws an [UnimplementedError] if the payment method type is not supported. - static PaymentMethod fromPaymentType(Map map) { - return PaymentMethod.fromMap(map); - } -} diff --git a/lib/src/models/payment_request_model.dart b/lib/src/models/payment_request_model.dart deleted file mode 100644 index 09eed10..0000000 --- a/lib/src/models/payment_request_model.dart +++ /dev/null @@ -1,33 +0,0 @@ -/// A model representing an Payment Request in the payment system. -/// -/// This model holds information about a specific Payment Request, including -/// an identifier, the phone number associated with the Payment Request, -/// and the amount involved in the Payment Request. -class PaymentRequestModel { - /// The unique identifier for the Payment Request. - final String id; - - /// The phone number associated with the Payment Request. - final String? phoneNumber; - - /// The amount involved in the Payment Request. - final double amount; - - /// Creates an instance of [PaymentRequestModel]. - /// - /// Requires [id], [phoneNumber], and [amount] to initialize the model. - PaymentRequestModel({ - required this.id, - this.phoneNumber, - required this.amount, - }); - - /// Creates an instance of [PaymentRequestModel] from a map. - /// - /// The [map] must contain the keys 'id', 'phone_number', and 'amount' - /// to successfully initialize the properties of the model. - PaymentRequestModel.fromMap(Map map) - : id = map['id'], - phoneNumber = map['phoneNumber'], - amount = map['amount'].toDouble(); -} diff --git a/lib/src/pages/bankily_view.dart b/lib/src/pages/bankily_view.dart index 0a1333a..f597c13 100644 --- a/lib/src/pages/bankily_view.dart +++ b/lib/src/pages/bankily_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; import 'package:moosyl_flutter/src/helpers/validators.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; @@ -24,7 +25,7 @@ class BankilyView extends StatelessWidget { required this.primaryColor, }); - final PaymentMethod method; + final ConfigurationListDataInner method; final String publishableApiKey; final String transactionId; final FutureOr Function()? onPaymentSuccess; @@ -49,7 +50,7 @@ class _BankilyDialogContent extends StatelessWidget { required this.onClose, }); - final PaymentMethod method; + final ConfigurationListDataInner method; final VoidCallback onClose; final Color primaryColor; @@ -57,7 +58,7 @@ class _BankilyDialogContent extends StatelessWidget { Widget build(BuildContext context) { final localizationHelper = MoosylLocalization.of(context)!; final provider = context.watch(); - final platformIcon = method.method.icon; + final platformIcon = PaymentMethodTypes.fromString(method.type).icon; return Dialog( shadowColor: Theme.of(context).colorScheme.outline, @@ -107,7 +108,8 @@ class _BankilyDialogContent extends StatelessWidget { // Copyable container with BPay code GestureDetector( - onTap: () => Feedbacks.copy(method.bPayNumber, context), + onTap: () => Feedbacks.copy( + method.config?.asMap['code']?.toString() ?? '', context), child: AppContainer( color: Colors.grey.shade200, border: Border.all(color: Colors.grey.shade300), @@ -121,7 +123,7 @@ class _BankilyDialogContent extends StatelessWidget { spacing: 8, children: [ Text( - method.bPayNumber, + method.config?.asMap['code']?.toString() ?? '', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 70f9cf7..22adc02 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; import 'package:moosyl_flutter/src/helpers/exception_handling/exception_mapper.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; @@ -283,7 +284,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { Future _onPayPressed( BuildContext context, GetPaymentMethodsProvider provider, - PaymentMethod pendingSelection, + ConfigurationListDataInner pendingSelection, Color effectivePrimary, ) async { final methodToShow = await provider.setPaymentMethodWithValidation( @@ -301,7 +302,8 @@ class _SelectPaymentMethodContent extends StatelessWidget { final publishableApiKey = provider.publishableApiKey; final transactionId = provider.transactionId; - if (methodToShow.method == PaymentMethodTypes.bankily) { + if (PaymentMethodTypes.fromString(methodToShow.type) == + PaymentMethodTypes.bankily) { _showBankilyDialog( context, publishableApiKey: publishableApiKey, @@ -309,8 +311,10 @@ class _SelectPaymentMethodContent extends StatelessWidget { method: methodToShow, primaryColor: effectivePrimary, ); - } else if (methodToShow.method == PaymentMethodTypes.sedad || - methodToShow.method == PaymentMethodTypes.bimBank) { + } else if (PaymentMethodTypes.fromString(methodToShow.type) == + PaymentMethodTypes.sedad || + PaymentMethodTypes.fromString(methodToShow.type) == + PaymentMethodTypes.bimBank) { _showSedadDialog( context, publishableApiKey: publishableApiKey, @@ -325,7 +329,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { BuildContext context, { required String publishableApiKey, required String transactionId, - required PaymentMethod method, + required ConfigurationListDataInner method, required Color primaryColor, }) { final payProvider = PayProvider( @@ -345,7 +349,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { payProvider: payProvider, builder: (paymentRequest) => SedadView( primaryColor: primaryColor, - paymentCodeDisplay: method.bPayNumber, + paymentCodeDisplay: method.config?.asMap['code']?.toString() ?? '', paymentRequest: paymentRequest, onClose: () { Navigator.of(dialogContext).pop(); @@ -363,7 +367,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { BuildContext context, { required String publishableApiKey, required String transactionId, - required PaymentMethod method, + required ConfigurationListDataInner method, required Color primaryColor, }) { final payProvider = PayProvider( @@ -571,7 +575,7 @@ class _MethodRow extends StatelessWidget { required this.onTap, }); - final PaymentMethod method; + final ConfigurationListDataInner method; final bool isSelected; final Color primaryColor; final VoidCallback onTap; @@ -601,12 +605,17 @@ class _MethodRow extends StatelessWidget { ), clipBehavior: Clip.antiAlias, child: Center( - child: provider.customIcons?[method.method] != null + child: provider.customIcons?[ + PaymentMethodTypes.fromString(method.type)] != + null ? AppIcon( - path: provider.customIcons?[method.method], + path: provider.customIcons?[ + PaymentMethodTypes.fromString(method.type)], size: 52, ) - : method.method.icon.apply(size: 40), + : PaymentMethodTypes.fromString(method.type) + .icon + .apply(size: 40), ), ), const SizedBox(width: 16), @@ -617,7 +626,7 @@ class _MethodRow extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - method.method.title(context), + PaymentMethodTypes.fromString(method.type).title(context), style: textTheme.titleMedium, ), const SizedBox(height: 4), @@ -631,7 +640,7 @@ class _MethodRow extends StatelessWidget { ), ), // Radio - Radio( + Radio( value: method, groupValue: provider.pendingSelection, onChanged: (_) => onTap(), diff --git a/lib/src/pages/sedad_view.dart b/lib/src/pages/sedad_view.dart index a0d3ccb..52c6288 100644 --- a/lib/src/pages/sedad_view.dart +++ b/lib/src/pages/sedad_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; -import 'package:moosyl_flutter/src/models/payment_request_model.dart'; +import 'package:moosyl_flutter/src/models/payment_method_model.dart'; import 'package:moosyl_flutter/src/providers/pay_provider.dart'; import 'package:moosyl_flutter/src/widgets/buttons.dart'; import 'package:moosyl_flutter/src/widgets/container.dart'; @@ -17,7 +18,7 @@ class SedadView extends StatelessWidget { final String paymentCodeDisplay; /// The payment request containing amount and other details. - final PaymentRequestModel paymentRequest; + final PaymentRequestGetData paymentRequest; /// Callback when the dialog is closed (e.g. to go back to payment method selection). final VoidCallback? onClose; @@ -37,7 +38,8 @@ class SedadView extends StatelessWidget { Widget build(BuildContext context) { final localizationHelper = MoosylLocalization.of(context)!; final payProvider = context.watch(); - final platformIcon = payProvider.method.method.icon; + final platformIcon = + PaymentMethodTypes.fromString(payProvider.method.type).icon; return Dialog( backgroundColor: Colors.white, @@ -93,7 +95,8 @@ class SedadView extends StatelessWidget { spacing: 8, children: [ Text( - payProvider.method.bPayNumber, + payProvider.method.config?.asMap['code']?.toString() ?? + '', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold), ), diff --git a/lib/src/providers/get_payment_methods_provider.dart b/lib/src/providers/get_payment_methods_provider.dart index a0ff4d3..e49d2a7 100644 --- a/lib/src/providers/get_payment_methods_provider.dart +++ b/lib/src/providers/get_payment_methods_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/src/helpers/exception_handling/error_handlers.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; -import 'package:moosyl_flutter/src/models/payment_request_model.dart'; import 'package:moosyl_flutter/src/services/get_payment_methods_service.dart'; import 'package:moosyl_flutter/src/services/get_payment_request_service.dart'; @@ -23,10 +23,10 @@ class GetPaymentMethodsProvider extends ChangeNotifier { final Map? customIcons; /// The payment method selected for the payment process. - PaymentMethod? selected; + ConfigurationListDataInner? selected; /// The payment method selected in the list (radio) before confirming with Pay. - PaymentMethod? pendingSelection; + ConfigurationListDataInner? pendingSelection; /// Error to show on payment method selection when validation fails. String? selectionError; @@ -52,10 +52,10 @@ class GetPaymentMethodsProvider extends ChangeNotifier { bool isValidating = false; /// List of available payment methods. - final List methods = []; + final List methods = []; /// The payment request model. - PaymentRequestModel? paymentRequest; + PaymentRequestGetData? paymentRequest; /// Clears the selection error. void clearSelectionError() { @@ -69,8 +69,8 @@ class GetPaymentMethodsProvider extends ChangeNotifier { /// For Sedad/Bankily: returns the method to show dialog (caller shows dialog). /// For Masrivi etc: calls setPaymentMethod and returns null. /// On validation error: returns null and sets selectionError. - Future setPaymentMethodWithValidation( - PaymentMethod method) async { + Future setPaymentMethodWithValidation( + ConfigurationListDataInner method) async { selectionError = null; isValidating = true; notifyListeners(); @@ -102,9 +102,12 @@ class GetPaymentMethodsProvider extends ChangeNotifier { return null; } - final isDialogMethod = method.method == PaymentMethodTypes.sedad || - method.method == PaymentMethodTypes.bimBank || - method.method == PaymentMethodTypes.bankily; + final isDialogMethod = PaymentMethodTypes.fromString(method.type) == + PaymentMethodTypes.sedad || + PaymentMethodTypes.fromString(method.type) == + PaymentMethodTypes.bimBank || + PaymentMethodTypes.fromString(method.type) == + PaymentMethodTypes.bankily; if (isDialogMethod) { return method; @@ -116,12 +119,14 @@ class GetPaymentMethodsProvider extends ChangeNotifier { /// Retrieves the list of supported payment method types. List get supportedTypes { - return [...methods.map((method) => method.method)]; + return [ + ...methods.map((method) => PaymentMethodTypes.fromString(method.type)) + ]; } /// Retrieves the list of valid payment method types, including custom handlers. List get validMethods => - [...methods.map((e) => e.method)]; + [...methods.map((e) => PaymentMethodTypes.fromString(e.type))]; /// Asynchronously fetches available payment methods from the service. /// @@ -159,20 +164,21 @@ class GetPaymentMethodsProvider extends ChangeNotifier { void onTap(PaymentMethodTypes type, BuildContext context) async { // Find and select the payment method from the list. - final selected = methods.firstWhere((element) => element.method == type); + final selected = methods.firstWhere( + (element) => PaymentMethodTypes.fromString(element.type) == type); setPaymentMethod(selected); } /// Sets the selected payment method (confirms and proceeds to payment). - void setPaymentMethod(PaymentMethod? method) { + void setPaymentMethod(ConfigurationListDataInner? method) { selected = method; pendingSelection = method; notifyListeners(); } /// Sets the pending selection (radio choice before Pay is tapped). - void setPendingSelection(PaymentMethod? method) { + void setPendingSelection(ConfigurationListDataInner? method) { pendingSelection = method; clearSelectionError(); notifyListeners(); diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index 8d601ac..758b533 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/src/helpers/exception_handling/error_handlers.dart'; -import 'package:moosyl_flutter/src/models/payment_request_model.dart'; -import 'package:moosyl_flutter/src/models/payment_method_model.dart'; import 'package:moosyl_flutter/src/services/get_payment_request_service.dart'; import 'package:moosyl_flutter/src/services/pay_service.dart'; @@ -18,7 +17,7 @@ class PayProvider extends ChangeNotifier { final String transactionId; /// The payment method selected for the payment process. - final PaymentMethod method; + final ConfigurationListDataInner method; /// Callback function that gets called on successful payment. final FutureOr Function()? onPaymentSuccess; @@ -45,7 +44,7 @@ class PayProvider extends ChangeNotifier { final formKey = GlobalKey(); /// Holds the payment request details. - PaymentRequestModel? paymentRequest; + PaymentRequestGetData? paymentRequest; /// Holds any error messages that occur during payment processing. Object? error; diff --git a/lib/src/services/get_payment_methods_service.dart b/lib/src/services/get_payment_methods_service.dart index bb471a7..60d1b93 100644 --- a/lib/src/services/get_payment_methods_service.dart +++ b/lib/src/services/get_payment_methods_service.dart @@ -1,5 +1,4 @@ import 'package:moosyl/moosyl.dart'; -import 'package:moosyl_flutter/src/models/payment_method_model.dart'; /// Base URL for the Moosyl API (matches the original fetcher endpoint). const String _defaultBaseUrl = 'https://moosyl.moosyl.workers.dev'; @@ -25,12 +24,12 @@ class GetPaymentMethodsService { /// /// Makes an API call via [ConfigurationApi] to retrieve the payment methods. /// Returns a list of [PaymentMethod] objects. - Future> get() async { + Future> get() async { final client = Moosyl( basePathOverride: baseUrlOverride ?? _defaultBaseUrl, ); - client.dio.options.headers['Authorization'] = publishableApiKey; + client.setApiKey('ApiKey', publishableApiKey); final config = client.getConfigurationApi(); final response = await config.getConfiguration(); @@ -40,21 +39,6 @@ class GetPaymentMethodsService { return []; } - return data.data - .map((e) => PaymentMethod.fromPaymentType(_toMap(e))) - .toList(); - } - - /// Converts [ConfigurationListDataInner] to [Map] for [PaymentMethod.fromPaymentType]. - static Map _toMap(ConfigurationListDataInner inner) { - final config = inner.config; - final code = config != null && config.isMap - ? (config.asMap['code']?.toString() ?? '') - : ''; - return { - 'id': inner.id, - 'type': inner.type, - 'config': {'code': code}, - }; + return data.data.toList(); } } diff --git a/lib/src/services/get_payment_request_service.dart b/lib/src/services/get_payment_request_service.dart index 401aac0..f496f71 100644 --- a/lib/src/services/get_payment_request_service.dart +++ b/lib/src/services/get_payment_request_service.dart @@ -1,5 +1,4 @@ import 'package:moosyl/moosyl.dart'; -import 'package:moosyl_flutter/src/models/payment_request_model.dart'; /// Base URL for the Moosyl API. const String _defaultBaseUrl = 'https://moosyl.moosyl.workers.dev'; @@ -24,13 +23,13 @@ class GetPaymentRequestService { /// Fetches Payment Request details for the specified [transactionId]. /// /// Makes an API call via [PaymentRequestApi] to retrieve the Payment Request. - /// Returns a [PaymentRequestModel] object containing the Payment Request details. - Future get(String transactionId) async { + /// Returns a [PaymentRequestGetData] object containing the Payment Request details. + Future get(String transactionId) async { final client = Moosyl( basePathOverride: baseUrlOverride ?? _defaultBaseUrl, ); - client.dio.options.headers['Authorization'] = authorizations; + client.setApiKey('ApiKey', authorizations); final paymentRequestApi = client.getPaymentRequestApi(); final response = @@ -43,10 +42,6 @@ class GetPaymentRequestService { throw StateError('Payment request not found'); } - return PaymentRequestModel( - id: data.id, - phoneNumber: data.phoneNumber, - amount: data.amount.toDouble(), - ); + return data; } } diff --git a/lib/src/services/pay_service.dart b/lib/src/services/pay_service.dart index 8619771..057b80c 100644 --- a/lib/src/services/pay_service.dart +++ b/lib/src/services/pay_service.dart @@ -49,7 +49,7 @@ class PayService { final client = Moosyl( basePathOverride: baseUrlOverride ?? _defaultBaseUrl, ); - client.dio.options.headers['Authorization'] = publishableApiKey; + client.setApiKey('ApiKey', publishableApiKey); return client; } } diff --git a/pubspec.yaml b/pubspec.yaml index f0f25d7..8f61362 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: modal_bottom_sheet: ^3.0.0 file_picker: ^8.1.2 mime: ^2.0.0 - moosyl: ^1.1.1 + moosyl: ^1.1.2 webview_flutter: ^4.10.0 dev_dependencies: From 725dbac6362d0dbb1b11842e9faf40ac8f6b3b44 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Sat, 28 Feb 2026 01:26:58 +0000 Subject: [PATCH 08/27] Enhance payment flow by adding optional payment code display in Bankily and Sedad views, and refactor payment method handling to support loading states and error feedback. Update PayProvider to manage payment code retrieval more efficiently. --- lib/src/pages/bankily_view.dart | 15 +++++-- lib/src/pages/payment_methods_view.dart | 37 +++++++++++++++-- lib/src/pages/sedad_view.dart | 14 +++++-- lib/src/providers/pay_provider.dart | 55 +++++++++++++++++++++---- lib/src/services/pay_service.dart | 26 +++++++++++- 5 files changed, 127 insertions(+), 20 deletions(-) diff --git a/lib/src/pages/bankily_view.dart b/lib/src/pages/bankily_view.dart index f597c13..da44035 100644 --- a/lib/src/pages/bankily_view.dart +++ b/lib/src/pages/bankily_view.dart @@ -23,6 +23,7 @@ class BankilyView extends StatelessWidget { this.onPaymentSuccess, this.onClose, required this.primaryColor, + required this.paymentCodeDisplay, }); final ConfigurationListDataInner method; @@ -31,6 +32,7 @@ class BankilyView extends StatelessWidget { final FutureOr Function()? onPaymentSuccess; final VoidCallback? onClose; final Color primaryColor; + final String? paymentCodeDisplay; @override Widget build(BuildContext context) { @@ -38,6 +40,7 @@ class BankilyView extends StatelessWidget { primaryColor: primaryColor, method: method, onClose: onClose ?? () => Navigator.of(context).pop(), + paymentCodeDisplay: paymentCodeDisplay, ); } } @@ -48,12 +51,13 @@ class _BankilyDialogContent extends StatelessWidget { required this.primaryColor, required this.method, required this.onClose, + required this.paymentCodeDisplay, }); final ConfigurationListDataInner method; final VoidCallback onClose; final Color primaryColor; - + final String? paymentCodeDisplay; @override Widget build(BuildContext context) { final localizationHelper = MoosylLocalization.of(context)!; @@ -109,7 +113,10 @@ class _BankilyDialogContent extends StatelessWidget { // Copyable container with BPay code GestureDetector( onTap: () => Feedbacks.copy( - method.config?.asMap['code']?.toString() ?? '', context), + paymentCodeDisplay ?? + method.config?.asMap['code']?.toString() ?? + '', + context), child: AppContainer( color: Colors.grey.shade200, border: Border.all(color: Colors.grey.shade300), @@ -123,7 +130,9 @@ class _BankilyDialogContent extends StatelessWidget { spacing: 8, children: [ Text( - method.config?.asMap['code']?.toString() ?? '', + paymentCodeDisplay ?? + method.config?.asMap['code']?.toString() ?? + '', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 22adc02..c1d8578 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -12,6 +12,7 @@ import 'package:moosyl_flutter/src/providers/pay_provider.dart'; import 'package:moosyl_flutter/src/widgets/buttons.dart'; import 'package:moosyl_flutter/src/widgets/container.dart'; import 'package:moosyl_flutter/src/widgets/error_widget.dart'; +import 'package:moosyl_flutter/src/widgets/feedback.dart'; import 'package:moosyl_flutter/src/widgets/icons.dart'; import 'package:provider/provider.dart'; @@ -315,7 +316,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { PaymentMethodTypes.sedad || PaymentMethodTypes.fromString(methodToShow.type) == PaymentMethodTypes.bimBank) { - _showSedadDialog( + await _showSedadDialog( context, publishableApiKey: publishableApiKey, transactionId: transactionId, @@ -325,13 +326,13 @@ class _SelectPaymentMethodContent extends StatelessWidget { } } - void _showSedadDialog( + Future _showSedadDialog( BuildContext context, { required String publishableApiKey, required String transactionId, required ConfigurationListDataInner method, required Color primaryColor, - }) { + }) async { final payProvider = PayProvider( publishableApiKey: publishableApiKey, transactionId: transactionId, @@ -340,6 +341,33 @@ class _SelectPaymentMethodContent extends StatelessWidget { ); final getPaymentMethodsProvider = context.read(); + // Show loading while fetching payment code. + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => const Center( + child: CircularProgressIndicator(), + ), + ); + + // Call pay() to get the payment code before showing the Sedad dialog. + final paymentCode = await payProvider.getPaymentCodeForSedad(); + + if (context.mounted) { + Navigator.of(context).pop(); // Dismiss loading dialog + } + if (!context.mounted) return; + if (paymentCode == null || paymentCode.isEmpty) { + if (context.mounted && payProvider.error != null) { + Feedbacks.flushBar( + context: context, + message: ExceptionMapper.getErrorMessage(payProvider.error, context), + error: true, + ); + } + return; + } + showDialog( context: context, barrierDismissible: false, @@ -349,7 +377,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { payProvider: payProvider, builder: (paymentRequest) => SedadView( primaryColor: primaryColor, - paymentCodeDisplay: method.config?.asMap['code']?.toString() ?? '', + paymentCodeDisplay: paymentCode, paymentRequest: paymentRequest, onClose: () { Navigator.of(dialogContext).pop(); @@ -390,6 +418,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { method: method, publishableApiKey: publishableApiKey, transactionId: transactionId, + paymentCodeDisplay: payProvider.paymentCode, onClose: () { Navigator.of(dialogContext).pop(); getPaymentMethodsProvider.setPaymentMethod(null); diff --git a/lib/src/pages/sedad_view.dart b/lib/src/pages/sedad_view.dart index 52c6288..3360ea0 100644 --- a/lib/src/pages/sedad_view.dart +++ b/lib/src/pages/sedad_view.dart @@ -15,7 +15,7 @@ import 'package:provider/provider.dart'; /// Displays payment code with copyable container, steps, and action buttons. class SedadView extends StatelessWidget { /// The payment code (merchant code) to display. - final String paymentCodeDisplay; + final String? paymentCodeDisplay; /// The payment request containing amount and other details. final PaymentRequestGetData paymentRequest; @@ -83,7 +83,11 @@ class SedadView extends StatelessWidget { // Copyable container with payment code only GestureDetector( - onTap: () => Feedbacks.copy(paymentCodeDisplay, context), + onTap: () => Feedbacks.copy( + paymentCodeDisplay ?? + payProvider.method.config?.asMap['code']?.toString() ?? + '', + context), child: AppContainer( color: Colors.grey.shade200, border: Border.all(color: Colors.grey.shade300), @@ -95,7 +99,9 @@ class SedadView extends StatelessWidget { spacing: 8, children: [ Text( - payProvider.method.config?.asMap['code']?.toString() ?? + paymentCodeDisplay ?? + payProvider.method.config?.asMap['code'] + ?.toString() ?? '', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold), @@ -174,7 +180,7 @@ class _IvePaidButton extends StatelessWidget { style: AppButtonStyle.primary, labelText: localizationHelper.ivePaid, loading: payProvider.isLoading, - onPressed: () => payProvider.pay(context), + onPressed: () => payProvider.handleSedadPay(), ); } } diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index 758b533..91d2dea 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -55,11 +55,23 @@ class PayProvider extends ChangeNotifier { /// The payment service used for processing payments. final PayService service; + /// Cached future for getPaymentRequest to avoid duplicate fetches. + Future? _getPaymentRequestFuture; + + /// + String? paymentCode; + /// Asynchronously fetches payment request details from the service. /// /// Updates the loading state and handles any errors that occur during /// the fetching process. Notifies listeners when the data changes. - void getPaymentRequest() async { + Future getPaymentRequest() async { + if (_getPaymentRequestFuture != null) return _getPaymentRequestFuture!; + _getPaymentRequestFuture = _getPaymentRequestImpl(); + return _getPaymentRequestFuture!; + } + + Future _getPaymentRequestImpl() async { error = null; isLoading = true; notifyListeners(); @@ -95,9 +107,7 @@ class PayProvider extends ChangeNotifier { /// /// Validates the form, sets the loading state, and calls the payment service. /// If payment is successful, it invokes the [onPaymentSuccess] callback. - void pay(BuildContext context) async { - if (!formKey.currentState!.validate()) return; // Ensure the form is valid. - + Future pay([BuildContext? context]) async { error = null; isLoading = true; notifyListeners(); @@ -117,11 +127,42 @@ class PayProvider extends ChangeNotifier { error = result.error; return notifyListeners(); } + paymentCode = + result.result?.metadata?.asMap['paymentCode'].toString() ?? ''; + notifyListeners(); + } + + /// Initiates Sedad/Bim Bank payment to get the payment code. + /// Call this when user presses Pay on the selection screen (before showing the dialog). + /// Waits for payment request, then calls pay with phone from request and empty passcode. + Future getPaymentCodeForSedad() async { + await getPaymentRequest(); + passCodeTextController.clear(); + await pay(); + return paymentCode; + } - if (context.mounted) { - Navigator.pop(context); + /// Handles the payment for the payment request. + void handleSedadPay() async { + error = null; + isLoading = true; + notifyListeners(); + + final result = await ErrorHandlers.catchErrors( + () => service.getPayment(transactionId: transactionId), + ); + + if (result.isError) { + error = result.error; + return notifyListeners(); } + isLoading = false; - await onPaymentSuccess?.call(); + if (result.result!.status == 'completed') { + onPaymentSuccess?.call(); + } else { + error = 'Payment not completed'; + return notifyListeners(); + } } } diff --git a/lib/src/services/pay_service.dart b/lib/src/services/pay_service.dart index 057b80c..1b5646c 100644 --- a/lib/src/services/pay_service.dart +++ b/lib/src/services/pay_service.dart @@ -25,7 +25,7 @@ class PayService { /// This method makes an API call to process a payment using the specified /// parameters. It requires the Transaction ID, phone number, passcode, /// and payment method ID. - Future pay({ + Future pay({ required String transactionId, required String phoneNumber, required String passCode, @@ -42,7 +42,12 @@ class PayService { ..passCode = passCode, ); - await paymentApi.postPayment(paymentCreate: paymentCreate); + final response = await paymentApi.postPayment(paymentCreate: paymentCreate); + final data = response.data; + if (data == null) { + throw StateError('payment not found'); + } + return data; } Moosyl _createClient() { @@ -52,4 +57,21 @@ class PayService { client.setApiKey('ApiKey', publishableApiKey); return client; } + + /// Fetches a payment by its ID. + /// + /// Makes an API call via [PaymentApi] to retrieve the payment. + /// Returns a [PaymentGet] object containing the payment details. + Future getPayment({ + required String transactionId, + }) async { + final client = _createClient(); + final paymentApi = client.getPaymentApi(); + final response = await paymentApi.getPaymentById(id: transactionId); + final data = response.data; + if (data == null) { + throw StateError('payment request not found'); + } + return data.data; + } } From af63ff116a4a02727865f3921e4083a03839993f Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Sat, 28 Feb 2026 03:18:35 +0000 Subject: [PATCH 09/27] Refactor payment flow to include optional callback before payment success, enhancing dialog management. Update payment handling in PayProvider to trigger the new callback for Bankily provider. --- lib/src/pages/masrivi_view.dart | 1 + lib/src/pages/payment_methods_view.dart | 34 +++++++++++++++---------- lib/src/providers/pay_provider.dart | 13 +++++++++- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/lib/src/pages/masrivi_view.dart b/lib/src/pages/masrivi_view.dart index 12b476b..d74f3bb 100644 --- a/lib/src/pages/masrivi_view.dart +++ b/lib/src/pages/masrivi_view.dart @@ -84,6 +84,7 @@ class _MasriviWebViewState extends State<_MasriviWebView> { NavigationDelegate( onNavigationRequest: (NavigationRequest request) { if (request.url.contains('/success')) { + widget.onBackPress(); widget.onPaymentSuccess?.call(); return NavigationDecision.prevent; } diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index c1d8578..091c663 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -371,21 +371,27 @@ class _SelectPaymentMethodContent extends StatelessWidget { showDialog( context: context, barrierDismissible: false, - builder: (dialogContext) => ChangeNotifierProvider.value( - value: payProvider, - child: _DialogWithPayProvider( - payProvider: payProvider, - builder: (paymentRequest) => SedadView( - primaryColor: primaryColor, - paymentCodeDisplay: paymentCode, - paymentRequest: paymentRequest, - onClose: () { - Navigator.of(dialogContext).pop(); - getPaymentMethodsProvider.setPaymentMethod(null); - }, + builder: (dialogContext) { + payProvider.onBeforePaymentSuccess = () { + Navigator.of(dialogContext).pop(); + getPaymentMethodsProvider.setPaymentMethod(null); + }; + return ChangeNotifierProvider.value( + value: payProvider, + child: _DialogWithPayProvider( + payProvider: payProvider, + builder: (paymentRequest) => SedadView( + primaryColor: primaryColor, + paymentCodeDisplay: paymentCode, + paymentRequest: paymentRequest, + onClose: () { + Navigator.of(dialogContext).pop(); + getPaymentMethodsProvider.setPaymentMethod(null); + }, + ), ), - ), - ), + ); + }, ).then((_) { getPaymentMethodsProvider.setPaymentMethod(null); }); diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index 91d2dea..cde725a 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -22,6 +22,10 @@ class PayProvider extends ChangeNotifier { /// Callback function that gets called on successful payment. final FutureOr Function()? onPaymentSuccess; + /// Optional callback called before [onPaymentSuccess] when payment completes. + /// Use this to close the dialog before invoking the success callback. + VoidCallback? onBeforePaymentSuccess; + /// Constructs a [PayProvider]. /// /// Initiates fetching the payment request details upon creation. @@ -130,6 +134,10 @@ class PayProvider extends ChangeNotifier { paymentCode = result.result?.metadata?.asMap['paymentCode'].toString() ?? ''; notifyListeners(); + if (result.result?.metadata?.asMap['provider'] == 'bankily') { + onBeforePaymentSuccess?.call(); + onPaymentSuccess?.call(); + } } /// Initiates Sedad/Bim Bank payment to get the payment code. @@ -157,12 +165,15 @@ class PayProvider extends ChangeNotifier { return notifyListeners(); } isLoading = false; + print('result.result!.status: ${result.result}'); - if (result.result!.status == 'completed') { + if (result.result!.status.name == 'completed') { + onBeforePaymentSuccess?.call(); onPaymentSuccess?.call(); } else { error = 'Payment not completed'; return notifyListeners(); } + notifyListeners(); } } From cc099727dca9b398375633b6ba290298f9c9b71d Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Sat, 28 Feb 2026 03:24:37 +0000 Subject: [PATCH 10/27] Refactor payment dialog to utilize a callback for handling actions before payment success, improving state management. Update PayProvider to trigger this callback upon successful payment completion. --- lib/src/pages/payment_methods_view.dart | 38 ++++++++++++++----------- lib/src/providers/pay_provider.dart | 1 - 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 091c663..5a51653 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -415,23 +415,29 @@ class _SelectPaymentMethodContent extends StatelessWidget { showDialog( context: context, barrierDismissible: false, - builder: (dialogContext) => ChangeNotifierProvider.value( - value: payProvider, - child: _DialogWithPayProvider( - payProvider: payProvider, - builder: (_) => BankilyView( - primaryColor: primaryColor, - method: method, - publishableApiKey: publishableApiKey, - transactionId: transactionId, - paymentCodeDisplay: payProvider.paymentCode, - onClose: () { - Navigator.of(dialogContext).pop(); - getPaymentMethodsProvider.setPaymentMethod(null); - }, - ), + builder: (dialogContext) { + payProvider.onBeforePaymentSuccess = () { + Navigator.of(dialogContext).pop(); + getPaymentMethodsProvider.setPaymentMethod(null); + }; + return ChangeNotifierProvider.value( + value: payProvider, + child: _DialogWithPayProvider( + payProvider: payProvider, + builder: (_) => BankilyView( + primaryColor: primaryColor, + method: method, + publishableApiKey: publishableApiKey, + transactionId: transactionId, + paymentCodeDisplay: payProvider.paymentCode, + onClose: () { + Navigator.of(dialogContext).pop(); + getPaymentMethodsProvider.setPaymentMethod(null); + }, + ), ), - ), + ); + }, ).then((_) { getPaymentMethodsProvider.setPaymentMethod(null); }); diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index cde725a..6bbb89f 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -165,7 +165,6 @@ class PayProvider extends ChangeNotifier { return notifyListeners(); } isLoading = false; - print('result.result!.status: ${result.result}'); if (result.result!.status.name == 'completed') { onBeforePaymentSuccess?.call(); From 9a77149e09cd66acce226243d92e81d555dd16d6 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 09:27:54 +0000 Subject: [PATCH 11/27] Remove customIcons parameter from Moosyl and MoosylView classes, simplifying the payment method configuration. Update related components to reflect this change and enhance code clarity. --- lib/src/pages/home.dart | 3 --- lib/src/pages/moosyl_view.dart | 9 +-------- lib/src/pages/payment_methods_view.dart | 18 +++++------------- .../get_payment_methods_provider.dart | 4 ---- 4 files changed, 6 insertions(+), 28 deletions(-) diff --git a/lib/src/pages/home.dart b/lib/src/pages/home.dart index ff41923..790d2ec 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/pages/home.dart @@ -50,7 +50,6 @@ class Moosyl extends HookWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, - customIcons: customIcons, isFullPage: isFullPage, ), ); @@ -85,7 +84,6 @@ class Moosyl extends HookWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, - customIcons: customIcons, isFullPage: isFullPage, ), ); @@ -98,7 +96,6 @@ class Moosyl extends HookWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, - customIcons: customIcons, isFullPage: isFullPage, ); } diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index cb4ccbf..00be9aa 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:moosyl_flutter/src/models/payment_method_model.dart'; import 'package:moosyl_flutter/src/pages/masrivi_view.dart'; import 'package:moosyl_flutter/src/pages/payment_methods_view.dart'; import 'package:moosyl_flutter/src/providers/get_payment_methods_provider.dart'; @@ -14,14 +13,12 @@ class MoosylView extends StatelessWidget { /// Creates an instance of [MoosylView]. /// Requires the [publishableApiKey] and [transactionId] for the payment transaction, - /// and optional handlers for custom payment methods, - /// success callbacks, and custom icons. + /// and optional handlers for success callbacks. const MoosylView({ super.key, required this.publishableApiKey, required this.transactionId, this.onPaymentSuccess, - this.customIcons, this.primaryColor, this.onBackPress, this.amountToPay = 0.0, @@ -38,9 +35,6 @@ class MoosylView extends StatelessWidget { /// Optional callback that is invoked when the payment is successful. final FutureOr Function()? onPaymentSuccess; - /// Optional custom icons for different payment methods. - final Map? customIcons; - /// Primary color for the payment method selection page (radio, pay button). final Color? primaryColor; @@ -66,7 +60,6 @@ class MoosylView extends StatelessWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, totalAmount: amountToPay + tax, - customIcons: customIcons, ), ), ], diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 5a51653..36d5fe6 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -435,8 +435,8 @@ class _SelectPaymentMethodContent extends StatelessWidget { getPaymentMethodsProvider.setPaymentMethod(null); }, ), - ), - ); + ), + ); }, ).then((_) { getPaymentMethodsProvider.setPaymentMethod(null); @@ -646,17 +646,9 @@ class _MethodRow extends StatelessWidget { ), clipBehavior: Clip.antiAlias, child: Center( - child: provider.customIcons?[ - PaymentMethodTypes.fromString(method.type)] != - null - ? AppIcon( - path: provider.customIcons?[ - PaymentMethodTypes.fromString(method.type)], - size: 52, - ) - : PaymentMethodTypes.fromString(method.type) - .icon - .apply(size: 40), + child: PaymentMethodTypes.fromString(method.type) + .icon + .apply(size: 40), ), ), const SizedBox(width: 16), diff --git a/lib/src/providers/get_payment_methods_provider.dart b/lib/src/providers/get_payment_methods_provider.dart index e49d2a7..f4b0aab 100644 --- a/lib/src/providers/get_payment_methods_provider.dart +++ b/lib/src/providers/get_payment_methods_provider.dart @@ -19,9 +19,6 @@ class GetPaymentMethodsProvider extends ChangeNotifier { /// The total amount to validate against payment request. final double totalAmount; - /// A map of custom icons for specific payment method types. - final Map? customIcons; - /// The payment method selected for the payment process. ConfigurationListDataInner? selected; @@ -36,7 +33,6 @@ class GetPaymentMethodsProvider extends ChangeNotifier { required this.publishableApiKey, required this.transactionId, required this.totalAmount, - required this.customIcons, }) { getMethods(); getPaymentRequest(); From 2337677a14a5c5115a6ee1d89aac384a006deb95 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 09:52:43 +0000 Subject: [PATCH 12/27] Refactor payment components to remove primaryColor parameter from multiple classes, simplifying the code and utilizing the theme's primary color instead. Update related files to ensure consistent styling across payment dialogs and buttons. --- README.md | 1 - lib/src/pages/bankily_view.dart | 8 +------- lib/src/pages/moosyl_view.dart | 5 ----- lib/src/pages/payment_methods_view.dart | 25 +++---------------------- lib/src/pages/sedad_view.dart | 7 +------ lib/src/widgets/buttons.dart | 10 +++------- 6 files changed, 8 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 9ab6115..6e3864a 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,6 @@ class PaymentScreen extends StatelessWidget { return MoosylView( publishableApiKey: 'YOUR_PUBLISHABLE_API_KEY', transactionId: 'TRANSACTION_ID', // Retrieved from your backend - primaryColor: Colors.red, onPaymentSuccess: () { print('Payment was successful!'); }, diff --git a/lib/src/pages/bankily_view.dart b/lib/src/pages/bankily_view.dart index da44035..8016313 100644 --- a/lib/src/pages/bankily_view.dart +++ b/lib/src/pages/bankily_view.dart @@ -22,7 +22,6 @@ class BankilyView extends StatelessWidget { required this.transactionId, this.onPaymentSuccess, this.onClose, - required this.primaryColor, required this.paymentCodeDisplay, }); @@ -31,13 +30,11 @@ class BankilyView extends StatelessWidget { final String transactionId; final FutureOr Function()? onPaymentSuccess; final VoidCallback? onClose; - final Color primaryColor; final String? paymentCodeDisplay; @override Widget build(BuildContext context) { return _BankilyDialogContent( - primaryColor: primaryColor, method: method, onClose: onClose ?? () => Navigator.of(context).pop(), paymentCodeDisplay: paymentCodeDisplay, @@ -48,7 +45,6 @@ class BankilyView extends StatelessWidget { /// Dialog content for Bankily payment. Expects [PayProvider] from context. class _BankilyDialogContent extends StatelessWidget { const _BankilyDialogContent({ - required this.primaryColor, required this.method, required this.onClose, required this.paymentCodeDisplay, @@ -56,13 +52,13 @@ class _BankilyDialogContent extends StatelessWidget { final ConfigurationListDataInner method; final VoidCallback onClose; - final Color primaryColor; final String? paymentCodeDisplay; @override Widget build(BuildContext context) { final localizationHelper = MoosylLocalization.of(context)!; final provider = context.watch(); final platformIcon = PaymentMethodTypes.fromString(method.type).icon; + final primaryColor = Theme.of(context).colorScheme.primary; return Dialog( shadowColor: Theme.of(context).colorScheme.outline, @@ -195,7 +191,6 @@ class _BankilyDialogContent extends StatelessWidget { ), // Pay button AppButton( - primaryColor: primaryColor, minHeight: 40, style: AppButtonStyle.primary, labelText: localizationHelper.pay, @@ -205,7 +200,6 @@ class _BankilyDialogContent extends StatelessWidget { // Change payment method button AppButton( - primaryColor: primaryColor, margin: const EdgeInsets.only(top: 8), minHeight: 40, style: AppButtonStyle.outline, diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index 00be9aa..25d20ab 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -19,7 +19,6 @@ class MoosylView extends StatelessWidget { required this.publishableApiKey, required this.transactionId, this.onPaymentSuccess, - this.primaryColor, this.onBackPress, this.amountToPay = 0.0, this.tax = 0.0, @@ -35,9 +34,6 @@ class MoosylView extends StatelessWidget { /// Optional callback that is invoked when the payment is successful. final FutureOr Function()? onPaymentSuccess; - /// Primary color for the payment method selection page (radio, pay button). - final Color? primaryColor; - /// Callback when the back arrow is pressed on the payment method selection page. final VoidCallback? onBackPress; @@ -73,7 +69,6 @@ class MoosylView extends StatelessWidget { // If no payment method is selected, show the available methods page. if (selectedModeOfPayment == null) { return SelectPaymentMethodPage( - primaryColor: primaryColor, onBackPress: onBackPress, amountToPay: amountToPay, tax: tax, diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 36d5fe6..0336531 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -13,7 +13,6 @@ import 'package:moosyl_flutter/src/widgets/buttons.dart'; import 'package:moosyl_flutter/src/widgets/container.dart'; import 'package:moosyl_flutter/src/widgets/error_widget.dart'; import 'package:moosyl_flutter/src/widgets/feedback.dart'; -import 'package:moosyl_flutter/src/widgets/icons.dart'; import 'package:provider/provider.dart'; /// A widget that displays the available payment methods for selection. @@ -25,7 +24,6 @@ class SelectPaymentMethodPage extends StatelessWidget { /// Creates an instance of [SelectPaymentMethodPage]. const SelectPaymentMethodPage({ super.key, - this.primaryColor, this.onBackPress, this.amountToPay = 0.0, this.tax = 0.0, @@ -38,9 +36,6 @@ class SelectPaymentMethodPage extends StatelessWidget { /// When true, shows as full page. When false, shows as bottom sheet. final bool isFullPage; - /// The primary color for the page (e.g. for radio, pay button). - final Color? primaryColor; - /// Callback when the back arrow is pressed. final VoidCallback? onBackPress; @@ -63,7 +58,6 @@ class SelectPaymentMethodPage extends StatelessWidget { Widget build(BuildContext context) { if (isFullPage) { return _SelectPaymentMethodContent( - primaryColor: primaryColor, onBackPress: onBackPress, amountToPay: amountToPay, tax: tax, @@ -74,7 +68,6 @@ class SelectPaymentMethodPage extends StatelessWidget { ); } else { return _SelectPaymentMethodBottomSheetTrigger( - primaryColor: primaryColor, onBackPress: onBackPress, amountToPay: amountToPay, tax: tax, @@ -90,7 +83,6 @@ class SelectPaymentMethodPage extends StatelessWidget { /// Full page or bottom sheet content for payment method selection. class _SelectPaymentMethodContent extends StatelessWidget { const _SelectPaymentMethodContent({ - required this.primaryColor, required this.onBackPress, required this.amountToPay, required this.tax, @@ -101,7 +93,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { this.onClose, }); - final Color? primaryColor; final VoidCallback? onBackPress; final double amountToPay; final double tax; @@ -114,8 +105,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { @override Widget build(BuildContext context) { final provider = context.watch(); - final effectivePrimary = - primaryColor ?? Theme.of(context).colorScheme.primary; + final effectivePrimary = Theme.of(context).colorScheme.primary; final localizationHelper = MoosylLocalization.of(context)!; final textTheme = Theme.of(context).textTheme; @@ -170,7 +160,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { return _MethodRow( method: method, isSelected: pendingSelection?.id == method.id, - primaryColor: effectivePrimary, onTap: () => provider.setPendingSelection(method), ); }), @@ -240,7 +229,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { ], )), AppButton( - primaryColor: effectivePrimary, minHeight: 50, labelText: provider.isValidating ? localizationHelper.sending @@ -310,7 +298,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, method: methodToShow, - primaryColor: effectivePrimary, ); } else if (PaymentMethodTypes.fromString(methodToShow.type) == PaymentMethodTypes.sedad || @@ -381,7 +368,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { child: _DialogWithPayProvider( payProvider: payProvider, builder: (paymentRequest) => SedadView( - primaryColor: primaryColor, paymentCodeDisplay: paymentCode, paymentRequest: paymentRequest, onClose: () { @@ -402,7 +388,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { required String publishableApiKey, required String transactionId, required ConfigurationListDataInner method, - required Color primaryColor, }) { final payProvider = PayProvider( publishableApiKey: publishableApiKey, @@ -425,7 +410,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { child: _DialogWithPayProvider( payProvider: payProvider, builder: (_) => BankilyView( - primaryColor: primaryColor, method: method, publishableApiKey: publishableApiKey, transactionId: transactionId, @@ -447,7 +431,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { /// Triggers the payment method selection bottom sheet on mount. class _SelectPaymentMethodBottomSheetTrigger extends StatefulWidget { const _SelectPaymentMethodBottomSheetTrigger({ - required this.primaryColor, required this.onBackPress, required this.amountToPay, required this.tax, @@ -457,7 +440,6 @@ class _SelectPaymentMethodBottomSheetTrigger extends StatefulWidget { required this.isFullPage, }); - final Color? primaryColor; final VoidCallback? onBackPress; final double amountToPay; final double tax; @@ -514,7 +496,6 @@ class _SelectPaymentMethodBottomSheetTriggerState ), Expanded( child: _SelectPaymentMethodContent( - primaryColor: widget.primaryColor, onBackPress: () { Navigator.of(sheetContext).pop(); widget.onBackPress?.call(); @@ -612,13 +593,12 @@ class _MethodRow extends StatelessWidget { const _MethodRow({ required this.method, required this.isSelected, - required this.primaryColor, required this.onTap, }); final ConfigurationListDataInner method; final bool isSelected; - final Color primaryColor; + final VoidCallback onTap; @override @@ -626,6 +606,7 @@ class _MethodRow extends StatelessWidget { final provider = context.watch(); final localizationHelper = MoosylLocalization.of(context)!; final textTheme = Theme.of(context).textTheme; + final primaryColor = Theme.of(context).colorScheme.primary; return InkWell( onTap: onTap, diff --git a/lib/src/pages/sedad_view.dart b/lib/src/pages/sedad_view.dart index 3360ea0..875a111 100644 --- a/lib/src/pages/sedad_view.dart +++ b/lib/src/pages/sedad_view.dart @@ -23,15 +23,11 @@ class SedadView extends StatelessWidget { /// Callback when the dialog is closed (e.g. to go back to payment method selection). final VoidCallback? onClose; - /// The primary color of the app. - final Color primaryColor; - const SedadView({ super.key, required this.paymentCodeDisplay, required this.paymentRequest, this.onClose, - required this.primaryColor, }); @override @@ -40,6 +36,7 @@ class SedadView extends StatelessWidget { final payProvider = context.watch(); final platformIcon = PaymentMethodTypes.fromString(payProvider.method.type).icon; + final primaryColor = Theme.of(context).colorScheme.primary; return Dialog( backgroundColor: Colors.white, @@ -132,7 +129,6 @@ class SedadView extends StatelessWidget { // Change payment method button AppButton( - primaryColor: primaryColor, margin: const EdgeInsets.only(top: 0), minHeight: 40, style: AppButtonStyle.outline, @@ -175,7 +171,6 @@ class _IvePaidButton extends StatelessWidget { final payProvider = context.watch(); return AppButton( - primaryColor: primaryColor, minHeight: 40, style: AppButtonStyle.primary, labelText: localizationHelper.ivePaid, diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index c627a46..c4652a4 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -28,7 +28,6 @@ class AppButton extends StatelessWidget { final double? minHeight; final EdgeInsetsGeometry? padding; final BorderSide border; - final Color? primaryColor; const AppButton({ super.key, @@ -46,7 +45,6 @@ class AppButton extends StatelessWidget { this.padding, this.minHeight = 60, this.border = BorderSide.none, - this.primaryColor, }); bool get _isDisabled => @@ -71,13 +69,11 @@ class AppButton extends StatelessWidget { side = BorderSide(color: outlineColor.withOpacity(0.5)); } else if (style == AppButtonStyle.outline) { bg = Colors.transparent; - fg = this.textColor ?? primaryColor ?? primary; - side = border == BorderSide.none - ? BorderSide(color: primaryColor ?? primary) - : border; + fg = this.textColor ?? primary; + side = border == BorderSide.none ? BorderSide(color: primary) : border; } else { // primary - bg = this.background ?? primaryColor ?? primary; + bg = this.background ?? primary; fg = textColor ?? onPrimary; side = border; } From 60bb8cec4a5b82ec152224f3f6e029d6cbdb5062 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 10:24:51 +0000 Subject: [PATCH 13/27] Add localization for payment completion errors in Arabic, English, and French; update related localization files and models to handle new error messages. --- lib/l10n/ar.arb | 3 +- lib/l10n/en.arb | 3 +- lib/l10n/fr.arb | 3 +- lib/l10n/generated/moosyl_localization.dart | 6 ++++ .../generated/moosyl_localization_ar.dart | 3 ++ .../generated/moosyl_localization_en.dart | 3 ++ .../generated/moosyl_localization_fr.dart | 3 ++ lib/src/models/selection_error.dart | 31 +++++++++++++++++++ lib/src/pages/payment_methods_view.dart | 8 ++--- lib/src/pages/sedad_view.dart | 13 ++++++++ lib/src/providers/pay_provider.dart | 2 +- 11 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 lib/src/models/selection_error.dart diff --git a/lib/l10n/ar.arb b/lib/l10n/ar.arb index bc208f6..be0cbb3 100644 --- a/lib/l10n/ar.arb +++ b/lib/l10n/ar.arb @@ -69,5 +69,6 @@ "bankilyStep1": "1. افتح تطبيق بنكيلي", "bankilyStep2": "2. انتقل إلى BPay", "bankilyStep3": "3. أدخل رمز BPay والمبلغ المطلوب دفعه", - "bankilyStep4": "4. ارجع هنا، أدخل رقمك ورمز المرور، ثم انقر على ادفع" + "bankilyStep4": "4. ارجع هنا، أدخل رقمك ورمز المرور، ثم انقر على ادفع", + "paymentNotCompleted": "لم يتم إكمال الدفع" } diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index 81f0d57..647dee7 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -69,5 +69,6 @@ "bankilyStep1": "1. Open the Bankily app", "bankilyStep2": "2. Go to BPay", "bankilyStep3": "3. Enter the BPay code and amount to pay", - "bankilyStep4": "4. Return here, enter your phone and passcode, then click Pay" + "bankilyStep4": "4. Return here, enter your phone and passcode, then click Pay", + "paymentNotCompleted": "Payment not completed" } diff --git a/lib/l10n/fr.arb b/lib/l10n/fr.arb index 9a7a851..8a95a55 100644 --- a/lib/l10n/fr.arb +++ b/lib/l10n/fr.arb @@ -69,5 +69,6 @@ "bankilyStep1": "1. Ouvrez l'application Bankily", "bankilyStep2": "2. Allez dans BPay", "bankilyStep3": "3. Saisissez le code BPay et le montant à payer", - "bankilyStep4": "4. Revenez ici, entrez votre numéro et code, puis cliquez sur Payer" + "bankilyStep4": "4. Revenez ici, entrez votre numéro et code, puis cliquez sur Payer", + "paymentNotCompleted": "Paiement non terminé" } diff --git a/lib/l10n/generated/moosyl_localization.dart b/lib/l10n/generated/moosyl_localization.dart index d0ee3af..bbe363a 100644 --- a/lib/l10n/generated/moosyl_localization.dart +++ b/lib/l10n/generated/moosyl_localization.dart @@ -520,6 +520,12 @@ abstract class MoosylLocalization { /// In en, this message translates to: /// **'4. Return here, enter your phone and passcode, then click Pay'** String get bankilyStep4; + + /// No description provided for @paymentNotCompleted. + /// + /// In en, this message translates to: + /// **'Payment not completed'** + String get paymentNotCompleted; } class _MoosylLocalizationDelegate diff --git a/lib/l10n/generated/moosyl_localization_ar.dart b/lib/l10n/generated/moosyl_localization_ar.dart index 00f49bc..e13632b 100644 --- a/lib/l10n/generated/moosyl_localization_ar.dart +++ b/lib/l10n/generated/moosyl_localization_ar.dart @@ -230,4 +230,7 @@ class MoosylLocalizationAr extends MoosylLocalization { @override String get bankilyStep4 => '4. ارجع هنا، أدخل رقمك ورمز المرور، ثم انقر على ادفع'; + + @override + String get paymentNotCompleted => 'لم يتم إكمال الدفع'; } diff --git a/lib/l10n/generated/moosyl_localization_en.dart b/lib/l10n/generated/moosyl_localization_en.dart index 063ed96..8a39e66 100644 --- a/lib/l10n/generated/moosyl_localization_en.dart +++ b/lib/l10n/generated/moosyl_localization_en.dart @@ -231,4 +231,7 @@ class MoosylLocalizationEn extends MoosylLocalization { @override String get bankilyStep4 => '4. Return here, enter your phone and passcode, then click Pay'; + + @override + String get paymentNotCompleted => 'Payment not completed'; } diff --git a/lib/l10n/generated/moosyl_localization_fr.dart b/lib/l10n/generated/moosyl_localization_fr.dart index bf055e9..4ba8c56 100644 --- a/lib/l10n/generated/moosyl_localization_fr.dart +++ b/lib/l10n/generated/moosyl_localization_fr.dart @@ -237,4 +237,7 @@ class MoosylLocalizationFr extends MoosylLocalization { @override String get bankilyStep4 => '4. Revenez ici, entrez votre numéro et code, puis cliquez sur Payer'; + + @override + String get paymentNotCompleted => 'Paiement non terminé'; } diff --git a/lib/src/models/selection_error.dart b/lib/src/models/selection_error.dart new file mode 100644 index 0000000..0af74e7 --- /dev/null +++ b/lib/src/models/selection_error.dart @@ -0,0 +1,31 @@ +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; + +/// Enum representing validation errors when selecting a payment method. +enum SelectionErrorType { + /// The payment request has already been fully paid. + paymentRequestFullyPaid, + + /// The amount to pay does not match the payment request amount. + amountToPayShouldMatchPaymentRequest; + + /// Creates a [SelectionErrorType] from its string representation. + static SelectionErrorType fromStr(String value) { + return switch (value) { + 'paymentRequestFullyPaid' => SelectionErrorType.paymentRequestFullyPaid, + 'amountToPayShouldMatchPaymentRequest' => + SelectionErrorType.amountToPayShouldMatchPaymentRequest, + _ => throw Exception('Invalid selection error type: $value'), + }; + } +} + +extension SelectionErrorTypeExtension on SelectionErrorType { + String message(MoosylLocalization l10n) { + switch (this) { + case SelectionErrorType.paymentRequestFullyPaid: + return l10n.paymentRequestFullyPaid; + case SelectionErrorType.amountToPayShouldMatchPaymentRequest: + return l10n.amountToPayShouldMatchPaymentRequest; + } + } +} diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 0336531..0160138 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -5,6 +5,7 @@ import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; import 'package:moosyl_flutter/src/helpers/exception_handling/exception_mapper.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/models/selection_error.dart'; import 'package:moosyl_flutter/src/pages/bankily_view.dart'; import 'package:moosyl_flutter/src/pages/sedad_view.dart'; import 'package:moosyl_flutter/src/providers/get_payment_methods_provider.dart'; @@ -121,11 +122,8 @@ class _SelectPaymentMethodContent extends StatelessWidget { } final selectionErrorMessage = provider.selectionError != null - ? (provider.selectionError == 'paymentRequestFullyPaid' - ? localizationHelper.paymentRequestFullyPaid - : provider.selectionError == 'amountToPayShouldMatchPaymentRequest' - ? localizationHelper.amountToPayShouldMatchPaymentRequest - : provider.selectionError) + ? SelectionErrorType.fromStr(provider.selectionError!) + .message(localizationHelper) : null; final methods = provider.methods; diff --git a/lib/src/pages/sedad_view.dart b/lib/src/pages/sedad_view.dart index 875a111..7d76cd7 100644 --- a/lib/src/pages/sedad_view.dart +++ b/lib/src/pages/sedad_view.dart @@ -38,6 +38,12 @@ class SedadView extends StatelessWidget { PaymentMethodTypes.fromString(payProvider.method.type).icon; final primaryColor = Theme.of(context).colorScheme.primary; + final errorMessage = payProvider.error != null + ? payProvider.error.toString() == 'PaymentNotCompleted' + ? localizationHelper.paymentNotCompleted + : payProvider.error.toString() + : null; + return Dialog( backgroundColor: Colors.white, insetPadding: const EdgeInsets.symmetric(horizontal: 10), @@ -124,6 +130,13 @@ class SedadView extends StatelessWidget { _StepItem(text: localizationHelper.sedadStep2), _StepItem(text: localizationHelper.sedadStep3), _StepItem(text: localizationHelper.sedadStep4), + + if (payProvider.error != null) + Text(errorMessage!, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Colors.red)), // I've paid button _IvePaidButton(primaryColor: primaryColor), diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index 6bbb89f..ab1bf6d 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -170,7 +170,7 @@ class PayProvider extends ChangeNotifier { onBeforePaymentSuccess?.call(); onPaymentSuccess?.call(); } else { - error = 'Payment not completed'; + error = 'PaymentNotCompleted'; return notifyListeners(); } notifyListeners(); From a0bd1ef3d042edae05d6f94316ce6f0d6f6cacb0 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 10:32:08 +0000 Subject: [PATCH 14/27] Enhance selection error handling by adding a new error type for incomplete payments and updating the error message retrieval in SedadView. This improves localization support for payment errors. --- lib/src/models/selection_error.dart | 8 +++++++- lib/src/pages/sedad_view.dart | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/src/models/selection_error.dart b/lib/src/models/selection_error.dart index 0af74e7..2100939 100644 --- a/lib/src/models/selection_error.dart +++ b/lib/src/models/selection_error.dart @@ -6,7 +6,10 @@ enum SelectionErrorType { paymentRequestFullyPaid, /// The amount to pay does not match the payment request amount. - amountToPayShouldMatchPaymentRequest; + amountToPayShouldMatchPaymentRequest, + + /// The payment request has not been completed. + paymentNotCompleted; /// Creates a [SelectionErrorType] from its string representation. static SelectionErrorType fromStr(String value) { @@ -14,6 +17,7 @@ enum SelectionErrorType { 'paymentRequestFullyPaid' => SelectionErrorType.paymentRequestFullyPaid, 'amountToPayShouldMatchPaymentRequest' => SelectionErrorType.amountToPayShouldMatchPaymentRequest, + 'paymentNotCompleted' => SelectionErrorType.paymentNotCompleted, _ => throw Exception('Invalid selection error type: $value'), }; } @@ -26,6 +30,8 @@ extension SelectionErrorTypeExtension on SelectionErrorType { return l10n.paymentRequestFullyPaid; case SelectionErrorType.amountToPayShouldMatchPaymentRequest: return l10n.amountToPayShouldMatchPaymentRequest; + case SelectionErrorType.paymentNotCompleted: + return l10n.paymentNotCompleted; } } } diff --git a/lib/src/pages/sedad_view.dart b/lib/src/pages/sedad_view.dart index 7d76cd7..23be590 100644 --- a/lib/src/pages/sedad_view.dart +++ b/lib/src/pages/sedad_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/models/selection_error.dart'; import 'package:moosyl_flutter/src/providers/pay_provider.dart'; import 'package:moosyl_flutter/src/widgets/buttons.dart'; import 'package:moosyl_flutter/src/widgets/container.dart'; @@ -39,9 +40,8 @@ class SedadView extends StatelessWidget { final primaryColor = Theme.of(context).colorScheme.primary; final errorMessage = payProvider.error != null - ? payProvider.error.toString() == 'PaymentNotCompleted' - ? localizationHelper.paymentNotCompleted - : payProvider.error.toString() + ? SelectionErrorType.fromStr(payProvider.error.toString()) + .message(localizationHelper) : null; return Dialog( From 3857c23c9207964f865f8161fc9decd2aa301d89 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 10:41:53 +0000 Subject: [PATCH 15/27] Implement error handling for incomplete payments in PayProvider and display error messages in Bankily dialog. This enhances user feedback during payment processes and improves localization support for error messages. --- lib/src/pages/bankily_view.dart | 12 +++++++++++- lib/src/providers/pay_provider.dart | 9 +++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/src/pages/bankily_view.dart b/lib/src/pages/bankily_view.dart index 8016313..54f476f 100644 --- a/lib/src/pages/bankily_view.dart +++ b/lib/src/pages/bankily_view.dart @@ -5,6 +5,7 @@ import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; import 'package:moosyl_flutter/src/helpers/validators.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/models/selection_error.dart'; import 'package:moosyl_flutter/src/providers/pay_provider.dart'; import 'package:moosyl_flutter/src/widgets/buttons.dart'; import 'package:moosyl_flutter/src/widgets/container.dart'; @@ -58,7 +59,10 @@ class _BankilyDialogContent extends StatelessWidget { final localizationHelper = MoosylLocalization.of(context)!; final provider = context.watch(); final platformIcon = PaymentMethodTypes.fromString(method.type).icon; - final primaryColor = Theme.of(context).colorScheme.primary; + final errorMessage = provider.error != null + ? SelectionErrorType.fromStr(provider.error.toString()) + .message(localizationHelper) + : null; return Dialog( shadowColor: Theme.of(context).colorScheme.outline, @@ -158,6 +162,12 @@ class _BankilyDialogContent extends StatelessWidget { _StepItem(text: localizationHelper.bankilyStep3), _StepItem(text: localizationHelper.bankilyStep4), const SizedBox(height: 16), + if (provider.error != null) + Text(errorMessage!, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Colors.red)), // Phone and passcode inputs in same container AppContainer( diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index ab1bf6d..8d9d15e 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -135,8 +135,13 @@ class PayProvider extends ChangeNotifier { result.result?.metadata?.asMap['paymentCode'].toString() ?? ''; notifyListeners(); if (result.result?.metadata?.asMap['provider'] == 'bankily') { - onBeforePaymentSuccess?.call(); - onPaymentSuccess?.call(); + if (result.result!.status == 'completed') { + onBeforePaymentSuccess?.call(); + onPaymentSuccess?.call(); + } else { + error = 'PaymentNotCompleted'; + return notifyListeners(); + } } } From 9483f94b3d1914d5eeedcffddfe6bac048936de6 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 12:18:20 +0000 Subject: [PATCH 16/27] Integrate payment success dialog and model into the payment flow. Update localization for success messages in Arabic, English, and French. Refactor payment handling to utilize the new PaymentSuccess model, enhancing user feedback upon successful transactions. --- example/lib/main.dart | 57 +++++++-- example/lib/payment_success_dialog.dart | 109 ++++++++++++++++++ lib/l10n/ar.arb | 5 +- lib/l10n/en.arb | 5 +- lib/l10n/fr.arb | 5 +- lib/l10n/generated/moosyl_localization.dart | 18 +++ .../generated/moosyl_localization_ar.dart | 9 ++ .../generated/moosyl_localization_en.dart | 9 ++ .../generated/moosyl_localization_fr.dart | 9 ++ lib/moosyl.dart | 1 + lib/src/models/payment_success.dart | 50 ++++++++ lib/src/pages/home.dart | 7 +- lib/src/pages/masrivi_view.dart | 31 ++++- lib/src/pages/moosyl_view.dart | 5 +- lib/src/pages/payment_methods_view.dart | 13 ++- lib/src/providers/pay_provider.dart | 15 ++- 16 files changed, 317 insertions(+), 31 deletions(-) create mode 100644 example/lib/payment_success_dialog.dart create mode 100644 lib/src/models/payment_success.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index bf60a4f..eee292a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:moosyl_flutter/moosyl.dart'; +import 'payment_success_dialog.dart'; + void main() { runApp(const MyApp()); } @@ -10,7 +12,7 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); static const colorScheme = ColorScheme.light( - primary: Color(0xFF4445F4), + primary: Color.fromARGB(255, 244, 68, 147), onPrimary: Color(0xFFFFFFFF), surface: Color(0xFFF0F0F0), onSurface: Color(0xFF000000), @@ -46,18 +48,51 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { + void _openPaymentFlow() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MoosylView( + publishableApiKey: 'your publishable api key', + transactionId: 'your transaction id', + onBackPress: () => Navigator.of(context).pop(), + onPaymentSuccess: (payment) async { + if (context.mounted) { + await showPaymentSuccessDialog(context, payment: payment); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + print( + 'Payment was successful! id=${payment.id} amount=${payment.amount} status=${payment.status}'); + }, + ), + ), + ); + } + @override Widget build(BuildContext context) { - return MoosylView( - publishableApiKey: 'your publishable api key', - transactionId: 'your transaction id', - primaryColor: Colors.red, - onBackPress: () => print('Back pressed'), - // amountToPay: 5, - // tax: 5, - onPaymentSuccess: () { - print('Payment was successful!'); - }, + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + ), + ), + onPressed: _openPaymentFlow, + child: const Text('Test payment flow'), + ), + ), ); } } diff --git a/example/lib/payment_success_dialog.dart b/example/lib/payment_success_dialog.dart new file mode 100644 index 0000000..f687cd6 --- /dev/null +++ b/example/lib/payment_success_dialog.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; +import 'package:moosyl_flutter/moosyl.dart' show PaymentSuccess; + +/// Shows a dialog with a green check icon, payment success message, and summary. +/// Returns a [Future] that completes when the dialog is dismissed. +Future showPaymentSuccessDialog( + BuildContext context, { + required PaymentSuccess payment, +}) async { + if (!context.mounted) return; + final l10n = MoosylLocalization.of(context)!; + await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + contentPadding: const EdgeInsets.all(24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle, + color: Colors.green, + size: 64, + ), + const SizedBox(height: 16), + Text( + l10n.paymentSuccessful, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _SummaryRow( + label: l10n.paymentId, + value: payment.id, + ), + const SizedBox(height: 8), + _SummaryRow( + label: l10n.amountToPay, + value: '${payment.amount} MRU', + ), + const SizedBox(height: 8), + _SummaryRow( + label: l10n.status, + value: payment.status, + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: Text(MaterialLocalizations.of(ctx).okButtonLabel), + ), + ], + ), + ); +} + +class _SummaryRow extends StatelessWidget { + const _SummaryRow({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 3, + child: Text( + value, + textAlign: TextAlign.right, + softWrap: true, + overflow: TextOverflow.visible, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ); + } +} diff --git a/lib/l10n/ar.arb b/lib/l10n/ar.arb index be0cbb3..e803fa3 100644 --- a/lib/l10n/ar.arb +++ b/lib/l10n/ar.arb @@ -70,5 +70,8 @@ "bankilyStep2": "2. انتقل إلى BPay", "bankilyStep3": "3. أدخل رمز BPay والمبلغ المطلوب دفعه", "bankilyStep4": "4. ارجع هنا، أدخل رقمك ورمز المرور، ثم انقر على ادفع", - "paymentNotCompleted": "لم يتم إكمال الدفع" + "paymentNotCompleted": "لم يتم إكمال الدفع", + "paymentSuccessful": "تم الدفع بنجاح", + "paymentId": "معرف الدفع", + "status": "الحالة" } diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index 647dee7..23a3b64 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -70,5 +70,8 @@ "bankilyStep2": "2. Go to BPay", "bankilyStep3": "3. Enter the BPay code and amount to pay", "bankilyStep4": "4. Return here, enter your phone and passcode, then click Pay", - "paymentNotCompleted": "Payment not completed" + "paymentNotCompleted": "Payment not completed", + "paymentSuccessful": "Payment successful", + "paymentId": "Payment ID", + "status": "Status" } diff --git a/lib/l10n/fr.arb b/lib/l10n/fr.arb index 8a95a55..0734f16 100644 --- a/lib/l10n/fr.arb +++ b/lib/l10n/fr.arb @@ -70,5 +70,8 @@ "bankilyStep2": "2. Allez dans BPay", "bankilyStep3": "3. Saisissez le code BPay et le montant à payer", "bankilyStep4": "4. Revenez ici, entrez votre numéro et code, puis cliquez sur Payer", - "paymentNotCompleted": "Paiement non terminé" + "paymentNotCompleted": "Paiement non terminé", + "paymentSuccessful": "Paiement réussi", + "paymentId": "ID de paiement", + "status": "Statut" } diff --git a/lib/l10n/generated/moosyl_localization.dart b/lib/l10n/generated/moosyl_localization.dart index bbe363a..0098401 100644 --- a/lib/l10n/generated/moosyl_localization.dart +++ b/lib/l10n/generated/moosyl_localization.dart @@ -526,6 +526,24 @@ abstract class MoosylLocalization { /// In en, this message translates to: /// **'Payment not completed'** String get paymentNotCompleted; + + /// No description provided for @paymentSuccessful. + /// + /// In en, this message translates to: + /// **'Payment successful'** + String get paymentSuccessful; + + /// No description provided for @paymentId. + /// + /// In en, this message translates to: + /// **'Payment ID'** + String get paymentId; + + /// No description provided for @status. + /// + /// In en, this message translates to: + /// **'Status'** + String get status; } class _MoosylLocalizationDelegate diff --git a/lib/l10n/generated/moosyl_localization_ar.dart b/lib/l10n/generated/moosyl_localization_ar.dart index e13632b..7657ba2 100644 --- a/lib/l10n/generated/moosyl_localization_ar.dart +++ b/lib/l10n/generated/moosyl_localization_ar.dart @@ -233,4 +233,13 @@ class MoosylLocalizationAr extends MoosylLocalization { @override String get paymentNotCompleted => 'لم يتم إكمال الدفع'; + + @override + String get paymentSuccessful => 'تم الدفع بنجاح'; + + @override + String get paymentId => 'معرف الدفع'; + + @override + String get status => 'الحالة'; } diff --git a/lib/l10n/generated/moosyl_localization_en.dart b/lib/l10n/generated/moosyl_localization_en.dart index 8a39e66..4be2f53 100644 --- a/lib/l10n/generated/moosyl_localization_en.dart +++ b/lib/l10n/generated/moosyl_localization_en.dart @@ -234,4 +234,13 @@ class MoosylLocalizationEn extends MoosylLocalization { @override String get paymentNotCompleted => 'Payment not completed'; + + @override + String get paymentSuccessful => 'Payment successful'; + + @override + String get paymentId => 'Payment ID'; + + @override + String get status => 'Status'; } diff --git a/lib/l10n/generated/moosyl_localization_fr.dart b/lib/l10n/generated/moosyl_localization_fr.dart index 4ba8c56..c69a87a 100644 --- a/lib/l10n/generated/moosyl_localization_fr.dart +++ b/lib/l10n/generated/moosyl_localization_fr.dart @@ -240,4 +240,13 @@ class MoosylLocalizationFr extends MoosylLocalization { @override String get paymentNotCompleted => 'Paiement non terminé'; + + @override + String get paymentSuccessful => 'Paiement réussi'; + + @override + String get paymentId => 'ID de paiement'; + + @override + String get status => 'Statut'; } diff --git a/lib/moosyl.dart b/lib/moosyl.dart index c9a74e9..b375f54 100644 --- a/lib/moosyl.dart +++ b/lib/moosyl.dart @@ -1,4 +1,5 @@ export 'src/pages/home.dart'; export 'src/models/payment_method_model.dart'; +export 'src/models/payment_success.dart'; export 'src/pages/moosyl_view.dart'; export 'l10n/moosyl_localization.dart'; diff --git a/lib/src/models/payment_success.dart b/lib/src/models/payment_success.dart new file mode 100644 index 0000000..2271f79 --- /dev/null +++ b/lib/src/models/payment_success.dart @@ -0,0 +1,50 @@ +import 'package:moosyl/moosyl.dart'; + +/// Unified model for payment success, used across Sedad, Bankily, and Masrivi. +/// +/// Normalizes [PaymentGetData] and [PostPayment200Response] into a common shape +/// for display in the success dialog and for [onPaymentSuccess] callback. +class PaymentSuccess { + const PaymentSuccess({ + required this.id, + required this.amount, + required this.status, + }); + + /// Payment ID. + final String id; + + /// Payment amount (e.g. in MRU). + final int amount; + + /// Payment status (e.g. 'completed'). + final String status; + + /// Creates from [PaymentGetData] (Sedad - from getPayment). + factory PaymentSuccess.fromPaymentGetData(PaymentGetData data) { + return PaymentSuccess( + id: data.id, + amount: data.amount, + status: data.status.name, + ); + } + + /// Creates from [PostPayment200Response] (Bankily - from postPayment). + /// [amountFallback] is used when amount is not in the response (e.g. from payment request). + factory PaymentSuccess.fromPostPaymentResponse( + PostPayment200Response response, { + required int amountFallback, + }) { + final amount = response.metadata?.asMap['amount'] != null + ? (response.metadata!.asMap['amount'] is int + ? response.metadata!.asMap['amount'] as int + : int.tryParse(response.metadata!.asMap['amount'].toString()) ?? + amountFallback) + : amountFallback; + return PaymentSuccess( + id: response.id, + amount: amount, + status: response.status, + ); + } +} diff --git a/lib/src/pages/home.dart b/lib/src/pages/home.dart index 790d2ec..e0e01ab 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/pages/home.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/pages/moosyl_view.dart'; /// [Moosyl] provides a widget that handles the payment process. @@ -23,8 +24,8 @@ class Moosyl extends HookWidget { /// The keys are [PaymentMethodTypes] and the values are the paths to the custom icons. final Map? customIcons; - /// Optional callback to be triggered upon successful payment. - final FutureOr Function()? onPaymentSuccess; + /// Optional callback to be triggered upon successful payment with [PaymentSuccess]. + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; /// When true, shows as full page. When false, shows as bottom sheet. final bool isFullPage; @@ -40,7 +41,7 @@ class Moosyl extends HookWidget { BuildContext context, { required String publishableApiKey, required String transactionId, - final FutureOr Function()? onPaymentSuccess, + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess, Map? customIcons, bool isFullPage = false, }) { diff --git a/lib/src/pages/masrivi_view.dart b/lib/src/pages/masrivi_view.dart index d74f3bb..e24da54 100644 --- a/lib/src/pages/masrivi_view.dart +++ b/lib/src/pages/masrivi_view.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/src/models/payment_success.dart'; +import 'package:moosyl_flutter/src/services/pay_service.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// Base URL for Masrivi payment web view. @@ -30,7 +32,7 @@ class MasriviView extends StatelessWidget { final String configurationId; /// Callback when payment is successful (URL contains "/success"). - final FutureOr Function()? onPaymentSuccess; + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; /// Callback when the back button is pressed. final VoidCallback? onBackPress; @@ -51,6 +53,8 @@ class MasriviView extends StatelessWidget { Widget build(BuildContext context) { return _MasriviWebView( payUrl: _payUrl, + publishableApiKey: publishableApiKey, + transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, onBackPress: onBackPress ?? () => Navigator.of(context).pop(), ); @@ -60,12 +64,16 @@ class MasriviView extends StatelessWidget { class _MasriviWebView extends StatefulWidget { const _MasriviWebView({ required this.payUrl, + required this.publishableApiKey, + required this.transactionId, this.onPaymentSuccess, required this.onBackPress, }); final String payUrl; - final FutureOr Function()? onPaymentSuccess; + final String publishableApiKey; + final String transactionId; + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; final VoidCallback onBackPress; @override @@ -85,7 +93,7 @@ class _MasriviWebViewState extends State<_MasriviWebView> { onNavigationRequest: (NavigationRequest request) { if (request.url.contains('/success')) { widget.onBackPress(); - widget.onPaymentSuccess?.call(); + _fetchPaymentAndNotify(); return NavigationDecision.prevent; } return NavigationDecision.navigate; @@ -95,6 +103,23 @@ class _MasriviWebViewState extends State<_MasriviWebView> { ..loadRequest(Uri.parse(widget.payUrl)); } + Future _fetchPaymentAndNotify() async { + final onSuccess = widget.onPaymentSuccess; + if (onSuccess == null) return; + try { + final paymentData = await PayService(widget.publishableApiKey) + .getPayment(transactionId: widget.transactionId); + final payment = PaymentSuccess.fromPaymentGetData(paymentData); + await onSuccess(payment); + } catch (_) { + await onSuccess(PaymentSuccess( + id: widget.transactionId, + amount: 0, + status: 'completed', + )); + } + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index 25d20ab..fda0f08 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/pages/masrivi_view.dart'; import 'package:moosyl_flutter/src/pages/payment_methods_view.dart'; import 'package:moosyl_flutter/src/providers/get_payment_methods_provider.dart'; @@ -31,8 +32,8 @@ class MoosylView extends StatelessWidget { /// The transaction ID for the specific payment transaction. final String transactionId; - /// Optional callback that is invoked when the payment is successful. - final FutureOr Function()? onPaymentSuccess; + /// Optional callback that is invoked when the payment is successful with [PaymentSuccess]. + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; /// Callback when the back arrow is pressed on the payment method selection page. final VoidCallback? onBackPress; diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 0160138..c5bd775 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -5,6 +5,7 @@ import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; import 'package:moosyl_flutter/src/helpers/exception_handling/exception_mapper.dart'; import 'package:moosyl_flutter/src/models/payment_method_model.dart'; +import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/models/selection_error.dart'; import 'package:moosyl_flutter/src/pages/bankily_view.dart'; import 'package:moosyl_flutter/src/pages/sedad_view.dart'; @@ -53,7 +54,7 @@ class SelectPaymentMethodPage extends StatelessWidget { final String transactionId; /// Callback when payment succeeds (for Sedad/Bankily dialogs). - final FutureOr Function()? onPaymentSuccess; + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; @override Widget build(BuildContext context) { @@ -99,7 +100,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { final double tax; final double totalAmount; final String transactionId; - final FutureOr Function()? onPaymentSuccess; + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; final bool isFullPage; final VoidCallback? onClose; @@ -322,7 +323,8 @@ class _SelectPaymentMethodContent extends StatelessWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, method: method, - onPaymentSuccess: () async => await onPaymentSuccess?.call(), + onPaymentSuccess: (payment) async => + await onPaymentSuccess?.call(payment), ); final getPaymentMethodsProvider = context.read(); @@ -391,7 +393,8 @@ class _SelectPaymentMethodContent extends StatelessWidget { publishableApiKey: publishableApiKey, transactionId: transactionId, method: method, - onPaymentSuccess: () async => await onPaymentSuccess?.call(), + onPaymentSuccess: (payment) async => + await onPaymentSuccess?.call(payment), ); final getPaymentMethodsProvider = context.read(); @@ -443,7 +446,7 @@ class _SelectPaymentMethodBottomSheetTrigger extends StatefulWidget { final double tax; final double totalAmount; final String transactionId; - final FutureOr Function()? onPaymentSuccess; + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; final bool isFullPage; @override State<_SelectPaymentMethodBottomSheetTrigger> createState() => diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index 8d9d15e..63ff269 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:moosyl/moosyl.dart'; import 'package:moosyl_flutter/src/helpers/exception_handling/error_handlers.dart'; +import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/services/get_payment_request_service.dart'; import 'package:moosyl_flutter/src/services/pay_service.dart'; @@ -19,8 +20,8 @@ class PayProvider extends ChangeNotifier { /// The payment method selected for the payment process. final ConfigurationListDataInner method; - /// Callback function that gets called on successful payment. - final FutureOr Function()? onPaymentSuccess; + /// Callback function that gets called on successful payment with [PaymentSuccess]. + final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; /// Optional callback called before [onPaymentSuccess] when payment completes. /// Use this to close the dialog before invoking the success callback. @@ -136,8 +137,13 @@ class PayProvider extends ChangeNotifier { notifyListeners(); if (result.result?.metadata?.asMap['provider'] == 'bankily') { if (result.result!.status == 'completed') { + final amount = paymentRequest?.amount ?? 0; + final payment = PaymentSuccess.fromPostPaymentResponse( + result.result!, + amountFallback: amount, + ); onBeforePaymentSuccess?.call(); - onPaymentSuccess?.call(); + onPaymentSuccess?.call(payment); } else { error = 'PaymentNotCompleted'; return notifyListeners(); @@ -172,8 +178,9 @@ class PayProvider extends ChangeNotifier { isLoading = false; if (result.result!.status.name == 'completed') { + final payment = PaymentSuccess.fromPaymentGetData(result.result!); onBeforePaymentSuccess?.call(); - onPaymentSuccess?.call(); + onPaymentSuccess?.call(payment); } else { error = 'PaymentNotCompleted'; return notifyListeners(); From 1ab6b722ae6de6d53bb97941791461ed01b5fdc6 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 13:21:30 +0000 Subject: [PATCH 17/27] Refactor payment flow to utilize MoosylFlutter for opening payment dialogs, enhancing user experience with full-page and bottom sheet options. Update _openPaymentFlow method for improved async handling and success dialog display. Add MoosylFlutter class for streamlined payment handling. --- example/lib/main.dart | 31 +++--- lib/moosyl.dart | 1 + lib/src/moosyl_flutter.dart | 70 +++++++++++++ lib/src/pages/moosyl_view.dart | 3 +- lib/src/pages/payment_methods_view.dart | 134 ++---------------------- 5 files changed, 95 insertions(+), 144 deletions(-) create mode 100644 lib/src/moosyl_flutter.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index eee292a..aa1417d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -48,26 +48,19 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - void _openPaymentFlow() { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => MoosylView( - publishableApiKey: 'your publishable api key', - transactionId: 'your transaction id', - onBackPress: () => Navigator.of(context).pop(), - onPaymentSuccess: (payment) async { - if (context.mounted) { - await showPaymentSuccessDialog(context, payment: payment); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - print( - 'Payment was successful! id=${payment.id} amount=${payment.amount} status=${payment.status}'); - }, - ), - ), + void _openPaymentFlow() async { + final payment = await MoosylFlutter.show( + context, + publishableApiKey: 'your publishable api key', + transactionId: 'your transaction id', + isFullPage: false, ); + if (!mounted) return; + if (payment != null) { + await showPaymentSuccessDialog(context, payment: payment); + print( + 'Payment was successful! id=${payment.id} amount=${payment.amount} status=${payment.status}'); + } } @override diff --git a/lib/moosyl.dart b/lib/moosyl.dart index b375f54..67caa8f 100644 --- a/lib/moosyl.dart +++ b/lib/moosyl.dart @@ -2,4 +2,5 @@ export 'src/pages/home.dart'; export 'src/models/payment_method_model.dart'; export 'src/models/payment_success.dart'; export 'src/pages/moosyl_view.dart'; +export 'src/moosyl_flutter.dart'; export 'l10n/moosyl_localization.dart'; diff --git a/lib/src/moosyl_flutter.dart b/lib/src/moosyl_flutter.dart new file mode 100644 index 0000000..fe986f2 --- /dev/null +++ b/lib/src/moosyl_flutter.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/src/models/payment_success.dart'; +import 'package:moosyl_flutter/src/pages/moosyl_view.dart'; + +/// Convenience class to open the payment flow. +/// +/// When [isFullPage] is true, pushes [MoosylView] as a full page. +/// When [isFullPage] is false, shows [MoosylView] as a bottom sheet. +/// +/// Returns [PaymentSuccess] when payment completes, or `null` when the user +/// closes without completing payment (e.g. back press). +class MoosylFlutter { + MoosylFlutter._(); + + /// Opens the payment flow. Returns [PaymentSuccess] on success, `null` when closed without payment. + static Future show( + BuildContext context, { + required String publishableApiKey, + required String transactionId, + double amountToPay = 0.0, + double tax = 0.0, + bool isFullPage = true, + }) async { + if (isFullPage) { + return Navigator.push( + context, + MaterialPageRoute( + builder: (ctx) => MoosylView( + publishableApiKey: publishableApiKey, + transactionId: transactionId, + amountToPay: amountToPay, + tax: tax, + isFullPage: true, + onBackPress: () => Navigator.pop(ctx, null), + onPaymentSuccess: (payment) async { + Navigator.pop(ctx, payment); + }, + ), + ), + ); + } else { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => DraggableScrollableSheet( + initialChildSize: 0.9, + minChildSize: 0.5, + maxChildSize: 1, + builder: (ctx, scrollController) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: MoosylView( + publishableApiKey: publishableApiKey, + transactionId: transactionId, + amountToPay: amountToPay, + tax: tax, + isFullPage: false, + onBackPress: () => Navigator.pop(ctx, null), + onPaymentSuccess: (payment) async { + Navigator.pop(ctx, payment); + }, + ), + ), + ), + ); + } + } +} diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index fda0f08..1d2311a 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -32,7 +32,8 @@ class MoosylView extends StatelessWidget { /// The transaction ID for the specific payment transaction. final String transactionId; - /// Optional callback that is invoked when the payment is successful with [PaymentSuccess]. + /// Optional callback invoked when the payment is successful with [PaymentSuccess]. + /// The caller is responsible for closing the route (e.g. Navigator.pop). final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; /// Callback when the back arrow is pressed on the payment method selection page. diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index c5bd775..aa7c7a9 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -58,27 +58,15 @@ class SelectPaymentMethodPage extends StatelessWidget { @override Widget build(BuildContext context) { - if (isFullPage) { - return _SelectPaymentMethodContent( - onBackPress: onBackPress, - amountToPay: amountToPay, - tax: tax, - totalAmount: totalAmount, - transactionId: transactionId, - onPaymentSuccess: onPaymentSuccess, - isFullPage: isFullPage, - ); - } else { - return _SelectPaymentMethodBottomSheetTrigger( - onBackPress: onBackPress, - amountToPay: amountToPay, - tax: tax, - totalAmount: totalAmount, - transactionId: transactionId, - onPaymentSuccess: onPaymentSuccess, - isFullPage: isFullPage, - ); - } + return _SelectPaymentMethodContent( + onBackPress: onBackPress, + amountToPay: amountToPay, + tax: tax, + totalAmount: totalAmount, + transactionId: transactionId, + onPaymentSuccess: onPaymentSuccess, + isFullPage: isFullPage, + ); } } @@ -92,7 +80,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { required this.transactionId, required this.onPaymentSuccess, required this.isFullPage, - this.onClose, }); final VoidCallback? onBackPress; @@ -102,7 +89,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { final String transactionId; final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; final bool isFullPage; - final VoidCallback? onClose; @override Widget build(BuildContext context) { @@ -282,7 +268,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { if (methodToShow == null) { if (provider.selected != null) { - onClose?.call(); + Navigator.pop(context, null); } return; } @@ -429,106 +415,6 @@ class _SelectPaymentMethodContent extends StatelessWidget { } } -/// Triggers the payment method selection bottom sheet on mount. -class _SelectPaymentMethodBottomSheetTrigger extends StatefulWidget { - const _SelectPaymentMethodBottomSheetTrigger({ - required this.onBackPress, - required this.amountToPay, - required this.tax, - required this.totalAmount, - required this.transactionId, - required this.onPaymentSuccess, - required this.isFullPage, - }); - - final VoidCallback? onBackPress; - final double amountToPay; - final double tax; - final double totalAmount; - final String transactionId; - final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; - final bool isFullPage; - @override - State<_SelectPaymentMethodBottomSheetTrigger> createState() => - _SelectPaymentMethodBottomSheetTriggerState(); -} - -class _SelectPaymentMethodBottomSheetTriggerState - extends State<_SelectPaymentMethodBottomSheetTrigger> { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _showBottomSheet()); - } - - void _showBottomSheet() { - if (!mounted) return; - final getPaymentMethodsProvider = context.read(); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (sheetContext) => - ChangeNotifierProvider.value( - value: getPaymentMethodsProvider, - child: DraggableScrollableSheet( - initialChildSize: 0.9, - minChildSize: 0.5, - maxChildSize: 1, - builder: (_, scrollController) => Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - Navigator.of(sheetContext).pop(); - widget.onBackPress?.call(); - }, - ), - ], - ), - Expanded( - child: _SelectPaymentMethodContent( - onBackPress: () { - Navigator.of(sheetContext).pop(); - widget.onBackPress?.call(); - }, - amountToPay: widget.amountToPay, - tax: widget.tax, - totalAmount: widget.totalAmount, - transactionId: widget.transactionId, - onPaymentSuccess: widget.onPaymentSuccess, - isFullPage: widget.isFullPage, - onClose: () { - Navigator.of(sheetContext).pop(); - widget.onBackPress?.call(); - }, - ), - ), - ], - ), - ), - ), - ), - ).then((_) { - widget.onBackPress?.call(); - }); - } - - @override - Widget build(BuildContext context) { - return const SizedBox.shrink(); - } -} - /// Shows loading until [PayProvider] has payment request, then builds content. class _DialogWithPayProvider extends StatelessWidget { const _DialogWithPayProvider({ From 6df4eff0d76d626475b0ab5d79361f21dcf0633b Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 13:26:56 +0000 Subject: [PATCH 18/27] Update README to reflect SDK name change to Moosyl Flutter, enhance payment UI instructions, and clarify usage examples for MoosylFlutter and MoosylView. Update API documentation links accordingly. --- README.md | 53 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6e3864a..84f3ad2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Moosyl Flutter SDK -[![Pub Version](https://img.shields.io/pub/v/moosyl.svg)](https://pub.dev/packages/moosyl) +[![Pub Version](https://img.shields.io/pub/v/moosyl_flutter.svg)](https://pub.dev/packages/moosyl_flutter) The **Moosyl Flutter SDK** is a powerful tool for integrating payment solutions with Mauritania's popular banking apps, such as **Bankily**, **Sedad**, and **Masrivi**. Simplify the payment process in your Flutter applications with features like webhooks and an easy-to-use interface. --- @@ -95,7 +95,7 @@ createPaymentRequest(); ### Step 2: Display the Payment View -Before instantiating `MoosylView`, make sure your `MaterialApp` registers the Moosyl localization delegates and supported locales: +Before displaying the payment UI, make sure your `MaterialApp` registers the Moosyl localization delegates and supported locales: ```dart return MaterialApp( @@ -105,35 +105,56 @@ return MaterialApp( ); ``` -Here’s how you can use the **MoosylView** widget in your Flutter app: +#### Option A: MoosylFlutter (recommended) + +Use `MoosylFlutter.show()` for a simple, async API. It returns `PaymentSuccess?` — non-null on success, `null` when the user closes without paying: ```dart import 'package:flutter/material.dart'; -import 'package:moosyl/moosyl.dart'; +import 'package:moosyl_flutter/moosyl.dart'; class PaymentScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return MoosylView( - publishableApiKey: 'YOUR_PUBLISHABLE_API_KEY', - transactionId: 'TRANSACTION_ID', // Retrieved from your backend - onPaymentSuccess: () { - print('Payment was successful!'); + return ElevatedButton( + onPressed: () async { + final payment = await MoosylFlutter.show( + context, + publishableApiKey: 'YOUR_PUBLISHABLE_API_KEY', + transactionId: 'TRANSACTION_ID', // From your backend + isFullPage: true, // false for bottom sheet + ); + if (payment != null) { + print('Payment successful! id=${payment.id} amount=${payment.amount}'); + } }, - onBackPress: () => print('Back pressed'), + child: const Text('Pay'), ); } } ``` -This example showcases the essential **MoosylView** widget, where you can configure: +**Parameters:** `publishableApiKey`, `transactionId`, `isFullPage` (true = full page, false = bottom sheet), `amountToPay`, `tax`. + +#### Option B: MoosylView (custom embedding) + +Use `MoosylView` when you need to embed the payment UI in your own navigation: + +```dart +MoosylView( + publishableApiKey: 'YOUR_PUBLISHABLE_API_KEY', + transactionId: 'TRANSACTION_ID', + onPaymentSuccess: (payment) async { + Navigator.pop(context); + print('Payment successful! id=${payment.id}'); + }, + onBackPress: () => Navigator.pop(context), +) +``` -- **`publishableApiKey`**: Your Moosyl publishable API Key. -- **`transactionId`**: The unique transaction identifier returned by your backend. -- **`onPaymentSuccess`**: Handle successful payment events. -- **`onBackPress`**: Handle back press events. +**Parameters:** `publishableApiKey`, `transactionId`, `onPaymentSuccess` (receives `PaymentSuccess` with id, amount, status — you must close the route yourself), `onBackPress`, `amountToPay`, `tax`, `isFullPage`. -For detailed API documentation, visit the [Moosyl API Documentation](https://pub.dev/documentation/moosyl/latest/moosyl/moosyl-library.html). +For detailed API documentation, visit the [Moosyl Flutter API Documentation](https://pub.dev/documentation/moosyl_flutter/latest/moosyl_flutter/moosyl_flutter-library.html). --- From bdb7333297a9ea3a01da05451bb05fda17a13047 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 14:10:50 +0000 Subject: [PATCH 19/27] Add handling for cancel URL in MasriviWebView and update payment method selection logic in PaymentMethodsView --- lib/src/pages/masrivi_view.dart | 3 +++ lib/src/pages/payment_methods_view.dart | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/pages/masrivi_view.dart b/lib/src/pages/masrivi_view.dart index e24da54..cabec34 100644 --- a/lib/src/pages/masrivi_view.dart +++ b/lib/src/pages/masrivi_view.dart @@ -95,6 +95,9 @@ class _MasriviWebViewState extends State<_MasriviWebView> { widget.onBackPress(); _fetchPaymentAndNotify(); return NavigationDecision.prevent; + } else if (request.url.contains('/cancel')) { + widget.onBackPress(); + return NavigationDecision.prevent; } return NavigationDecision.navigate; }, diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index aa7c7a9..5ebe82e 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -267,9 +267,7 @@ class _SelectPaymentMethodContent extends StatelessWidget { if (!context.mounted) return; if (methodToShow == null) { - if (provider.selected != null) { - Navigator.pop(context, null); - } + // Masrivi etc: provider.selected is set, MoosylView will show the method's view. return; } From 469af9c1ea900b7de498a3d72e0eed34def4ef89 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 14:25:58 +0000 Subject: [PATCH 20/27] Enhance localization by adding payment failure and decline messages in Arabic, English, and French. Update localization files and integrate new messages into the payment flow for improved user feedback during payment issues. --- lib/l10n/ar.arb | 4 ++- lib/l10n/en.arb | 4 ++- lib/l10n/fr.arb | 4 ++- lib/l10n/generated/moosyl_localization.dart | 12 ++++++++ .../generated/moosyl_localization_ar.dart | 7 +++++ .../generated/moosyl_localization_en.dart | 7 +++++ .../generated/moosyl_localization_fr.dart | 7 +++++ lib/src/pages/masrivi_view.dart | 15 +++++++++- lib/src/pages/moosyl_view.dart | 29 +++++++++++++++++++ 9 files changed, 85 insertions(+), 4 deletions(-) diff --git a/lib/l10n/ar.arb b/lib/l10n/ar.arb index e803fa3..bed4d9e 100644 --- a/lib/l10n/ar.arb +++ b/lib/l10n/ar.arb @@ -73,5 +73,7 @@ "paymentNotCompleted": "لم يتم إكمال الدفع", "paymentSuccessful": "تم الدفع بنجاح", "paymentId": "معرف الدفع", - "status": "الحالة" + "status": "الحالة", + "paymentFailed": "فشل الدفع", + "paymentDeclined": "فشل الدفع. قد يكون رصيدك غير كافٍ أو قد يكون حسابك غير نشط." } diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index 23a3b64..67a8938 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -73,5 +73,7 @@ "paymentNotCompleted": "Payment not completed", "paymentSuccessful": "Payment successful", "paymentId": "Payment ID", - "status": "Status" + "status": "Status", + "paymentFailed": "Payment failed", + "paymentDeclined": "Payment failed. Your balance may be insufficient or your account may not be active." } diff --git a/lib/l10n/fr.arb b/lib/l10n/fr.arb index 0734f16..6e826f5 100644 --- a/lib/l10n/fr.arb +++ b/lib/l10n/fr.arb @@ -73,5 +73,7 @@ "paymentNotCompleted": "Paiement non terminé", "paymentSuccessful": "Paiement réussi", "paymentId": "ID de paiement", - "status": "Statut" + "status": "Statut", + "paymentFailed": "Échec du paiement", + "paymentDeclined": "Le paiement a échoué. Votre solde est peut-être insuffisant ou votre compte n'est peut-être pas actif." } diff --git a/lib/l10n/generated/moosyl_localization.dart b/lib/l10n/generated/moosyl_localization.dart index 0098401..45d491b 100644 --- a/lib/l10n/generated/moosyl_localization.dart +++ b/lib/l10n/generated/moosyl_localization.dart @@ -544,6 +544,18 @@ abstract class MoosylLocalization { /// In en, this message translates to: /// **'Status'** String get status; + + /// No description provided for @paymentFailed. + /// + /// In en, this message translates to: + /// **'Payment failed'** + String get paymentFailed; + + /// No description provided for @paymentDeclined. + /// + /// In en, this message translates to: + /// **'Payment failed. Your balance may be insufficient or your account may not be active.'** + String get paymentDeclined; } class _MoosylLocalizationDelegate diff --git a/lib/l10n/generated/moosyl_localization_ar.dart b/lib/l10n/generated/moosyl_localization_ar.dart index 7657ba2..8818e60 100644 --- a/lib/l10n/generated/moosyl_localization_ar.dart +++ b/lib/l10n/generated/moosyl_localization_ar.dart @@ -242,4 +242,11 @@ class MoosylLocalizationAr extends MoosylLocalization { @override String get status => 'الحالة'; + + @override + String get paymentFailed => 'فشل الدفع'; + + @override + String get paymentDeclined => + 'فشل الدفع. قد يكون رصيدك غير كافٍ أو قد يكون حسابك غير نشط.'; } diff --git a/lib/l10n/generated/moosyl_localization_en.dart b/lib/l10n/generated/moosyl_localization_en.dart index 4be2f53..b8d75b0 100644 --- a/lib/l10n/generated/moosyl_localization_en.dart +++ b/lib/l10n/generated/moosyl_localization_en.dart @@ -243,4 +243,11 @@ class MoosylLocalizationEn extends MoosylLocalization { @override String get status => 'Status'; + + @override + String get paymentFailed => 'Payment failed'; + + @override + String get paymentDeclined => + 'Payment failed. Your balance may be insufficient or your account may not be active.'; } diff --git a/lib/l10n/generated/moosyl_localization_fr.dart b/lib/l10n/generated/moosyl_localization_fr.dart index c69a87a..8b5cc50 100644 --- a/lib/l10n/generated/moosyl_localization_fr.dart +++ b/lib/l10n/generated/moosyl_localization_fr.dart @@ -249,4 +249,11 @@ class MoosylLocalizationFr extends MoosylLocalization { @override String get status => 'Statut'; + + @override + String get paymentFailed => 'Échec du paiement'; + + @override + String get paymentDeclined => + 'Le paiement a échoué. Votre solde est peut-être insuffisant ou votre compte n\'est peut-être pas actif.'; } diff --git a/lib/src/pages/masrivi_view.dart b/lib/src/pages/masrivi_view.dart index cabec34..3dc568a 100644 --- a/lib/src/pages/masrivi_view.dart +++ b/lib/src/pages/masrivi_view.dart @@ -10,8 +10,9 @@ const String _masriviPayBaseUrl = 'https://payments.moosyl.com/masrivi/pay'; /// A view that displays Masrivi payment in a WebView. /// -/// Loads the Masrivi pay URL and listens for navigation to a success URL. +/// Loads the Masrivi pay URL and listens for navigation to success/decline URLs. /// When the URL contains "/success", [onPaymentSuccess] is invoked. +/// When the URL contains "/decline", [onPaymentDeclined] is invoked and the view goes back. class MasriviView extends StatelessWidget { const MasriviView({ super.key, @@ -20,6 +21,7 @@ class MasriviView extends StatelessWidget { required this.configurationId, this.onPaymentSuccess, this.onBackPress, + this.onPaymentDeclined, }); /// The API key for authenticating the payment. @@ -37,6 +39,10 @@ class MasriviView extends StatelessWidget { /// Callback when the back button is pressed. final VoidCallback? onBackPress; + /// Callback when payment is declined (URL contains "/decline"). + /// Called after going back to the payment method list. Use to show an error message. + final VoidCallback? onPaymentDeclined; + /// Builds the Masrivi pay URL with query parameters. String get _payUrl { final uri = Uri.parse(_masriviPayBaseUrl).replace( @@ -57,6 +63,7 @@ class MasriviView extends StatelessWidget { transactionId: transactionId, onPaymentSuccess: onPaymentSuccess, onBackPress: onBackPress ?? () => Navigator.of(context).pop(), + onPaymentDeclined: onPaymentDeclined, ); } } @@ -68,6 +75,7 @@ class _MasriviWebView extends StatefulWidget { required this.transactionId, this.onPaymentSuccess, required this.onBackPress, + this.onPaymentDeclined, }); final String payUrl; @@ -75,6 +83,7 @@ class _MasriviWebView extends StatefulWidget { final String transactionId; final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; final VoidCallback onBackPress; + final VoidCallback? onPaymentDeclined; @override State<_MasriviWebView> createState() => _MasriviWebViewState(); @@ -95,6 +104,10 @@ class _MasriviWebViewState extends State<_MasriviWebView> { widget.onBackPress(); _fetchPaymentAndNotify(); return NavigationDecision.prevent; + } else if (request.url.contains('/decline')) { + widget.onBackPress(); + widget.onPaymentDeclined?.call(); + return NavigationDecision.prevent; } else if (request.url.contains('/cancel')) { widget.onBackPress(); return NavigationDecision.prevent; diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index 1d2311a..1e7a424 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:moosyl_flutter/l10n/generated/moosyl_localization.dart'; import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/pages/masrivi_view.dart'; import 'package:moosyl_flutter/src/pages/payment_methods_view.dart'; @@ -88,6 +89,34 @@ class MoosylView extends StatelessWidget { onBackPress: () => context .read() .setPaymentMethod(null), + onPaymentDeclined: () { + final l10n = MoosylLocalization.of(context); + if (l10n != null && context.mounted) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Text( + l10n.paymentFailed, + style: + Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + content: Text(l10n.paymentDeclined), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: Text( + MaterialLocalizations.of(ctx).okButtonLabel), + ), + ], + ), + ); + } + }, ); } }, From 2d36ecc726981f1f9516e4f127983cb1c8b70ddb Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 15:33:24 +0000 Subject: [PATCH 21/27] Add 'try again' messages in Arabic, English, and French localization files. Update payment decline messages for improved clarity and integrate new messages into the payment flow for better user experience. --- lib/l10n/ar.arb | 3 +- lib/l10n/en.arb | 3 +- lib/l10n/fr.arb | 3 +- lib/l10n/generated/moosyl_localization.dart | 8 ++++- .../generated/moosyl_localization_ar.dart | 5 ++- .../generated/moosyl_localization_en.dart | 5 ++- .../generated/moosyl_localization_fr.dart | 5 ++- lib/src/pages/moosyl_view.dart | 32 +++++++++++-------- 8 files changed, 43 insertions(+), 21 deletions(-) diff --git a/lib/l10n/ar.arb b/lib/l10n/ar.arb index bed4d9e..e22d87b 100644 --- a/lib/l10n/ar.arb +++ b/lib/l10n/ar.arb @@ -74,6 +74,7 @@ "paymentSuccessful": "تم الدفع بنجاح", "paymentId": "معرف الدفع", "status": "الحالة", + "tryAgain": "حاول مرة أخرى", "paymentFailed": "فشل الدفع", - "paymentDeclined": "فشل الدفع. قد يكون رصيدك غير كافٍ أو قد يكون حسابك غير نشط." + "paymentDeclined": " . قد يكون رصيدك غير كافٍ أو قد يكون حسابك غير نشط" } diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb index 67a8938..4cea55a 100644 --- a/lib/l10n/en.arb +++ b/lib/l10n/en.arb @@ -74,6 +74,7 @@ "paymentSuccessful": "Payment successful", "paymentId": "Payment ID", "status": "Status", + "tryAgain": "Try again", "paymentFailed": "Payment failed", - "paymentDeclined": "Payment failed. Your balance may be insufficient or your account may not be active." + "paymentDeclined": "Your balance may be insufficient or your account may not be active." } diff --git a/lib/l10n/fr.arb b/lib/l10n/fr.arb index 6e826f5..815af29 100644 --- a/lib/l10n/fr.arb +++ b/lib/l10n/fr.arb @@ -74,6 +74,7 @@ "paymentSuccessful": "Paiement réussi", "paymentId": "ID de paiement", "status": "Statut", + "tryAgain": "Réessayer", "paymentFailed": "Échec du paiement", - "paymentDeclined": "Le paiement a échoué. Votre solde est peut-être insuffisant ou votre compte n'est peut-être pas actif." + "paymentDeclined": "Votre solde est peut-être insuffisant ou votre compte n'est peut-être pas actif." } diff --git a/lib/l10n/generated/moosyl_localization.dart b/lib/l10n/generated/moosyl_localization.dart index 45d491b..c1f1b97 100644 --- a/lib/l10n/generated/moosyl_localization.dart +++ b/lib/l10n/generated/moosyl_localization.dart @@ -545,6 +545,12 @@ abstract class MoosylLocalization { /// **'Status'** String get status; + /// No description provided for @tryAgain. + /// + /// In en, this message translates to: + /// **'Try again'** + String get tryAgain; + /// No description provided for @paymentFailed. /// /// In en, this message translates to: @@ -554,7 +560,7 @@ abstract class MoosylLocalization { /// No description provided for @paymentDeclined. /// /// In en, this message translates to: - /// **'Payment failed. Your balance may be insufficient or your account may not be active.'** + /// **'Your balance may be insufficient or your account may not be active.'** String get paymentDeclined; } diff --git a/lib/l10n/generated/moosyl_localization_ar.dart b/lib/l10n/generated/moosyl_localization_ar.dart index 8818e60..f5c3775 100644 --- a/lib/l10n/generated/moosyl_localization_ar.dart +++ b/lib/l10n/generated/moosyl_localization_ar.dart @@ -243,10 +243,13 @@ class MoosylLocalizationAr extends MoosylLocalization { @override String get status => 'الحالة'; + @override + String get tryAgain => 'حاول مرة أخرى'; + @override String get paymentFailed => 'فشل الدفع'; @override String get paymentDeclined => - 'فشل الدفع. قد يكون رصيدك غير كافٍ أو قد يكون حسابك غير نشط.'; + ' . قد يكون رصيدك غير كافٍ أو قد يكون حسابك غير نشط'; } diff --git a/lib/l10n/generated/moosyl_localization_en.dart b/lib/l10n/generated/moosyl_localization_en.dart index b8d75b0..1d240a6 100644 --- a/lib/l10n/generated/moosyl_localization_en.dart +++ b/lib/l10n/generated/moosyl_localization_en.dart @@ -244,10 +244,13 @@ class MoosylLocalizationEn extends MoosylLocalization { @override String get status => 'Status'; + @override + String get tryAgain => 'Try again'; + @override String get paymentFailed => 'Payment failed'; @override String get paymentDeclined => - 'Payment failed. Your balance may be insufficient or your account may not be active.'; + 'Your balance may be insufficient or your account may not be active.'; } diff --git a/lib/l10n/generated/moosyl_localization_fr.dart b/lib/l10n/generated/moosyl_localization_fr.dart index 8b5cc50..37a718e 100644 --- a/lib/l10n/generated/moosyl_localization_fr.dart +++ b/lib/l10n/generated/moosyl_localization_fr.dart @@ -250,10 +250,13 @@ class MoosylLocalizationFr extends MoosylLocalization { @override String get status => 'Statut'; + @override + String get tryAgain => 'Réessayer'; + @override String get paymentFailed => 'Échec du paiement'; @override String get paymentDeclined => - 'Le paiement a échoué. Votre solde est peut-être insuffisant ou votre compte n\'est peut-être pas actif.'; + 'Votre solde est peut-être insuffisant ou votre compte n\'est peut-être pas actif.'; } diff --git a/lib/src/pages/moosyl_view.dart b/lib/src/pages/moosyl_view.dart index 1e7a424..88be48e 100644 --- a/lib/src/pages/moosyl_view.dart +++ b/lib/src/pages/moosyl_view.dart @@ -5,6 +5,7 @@ import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/pages/masrivi_view.dart'; import 'package:moosyl_flutter/src/pages/payment_methods_view.dart'; import 'package:moosyl_flutter/src/providers/get_payment_methods_provider.dart'; +import 'package:moosyl_flutter/src/widgets/buttons.dart'; import 'package:provider/provider.dart'; /// A widget that provides a payment interface for the Software Pay system. @@ -98,21 +99,24 @@ class MoosylView extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - title: Text( - l10n.paymentFailed, - style: - Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.error, - ), + title: Text(l10n.paymentFailed), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.paymentDeclined), + Align( + alignment: Alignment.centerRight, + child: SizedBox( + height: 44, + child: AppButton( + style: AppButtonStyle.outline, + borderRadius: BorderRadius.circular(16), + labelText: l10n.tryAgain, + onPressed: () => Navigator.of(ctx).pop(), + )), + ), + ], ), - content: Text(l10n.paymentDeclined), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: Text( - MaterialLocalizations.of(ctx).okButtonLabel), - ), - ], ), ); } From 3d45c7ecb832101fa4452dd53a7b060fb8983695 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 17:52:25 +0000 Subject: [PATCH 22/27] Add 'unknown' error type to SelectionErrorType enum and update error handling to return this type for invalid inputs. Enhance localization support by adding a corresponding message for the new error type. --- lib/src/models/selection_error.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/models/selection_error.dart b/lib/src/models/selection_error.dart index 2100939..c160a26 100644 --- a/lib/src/models/selection_error.dart +++ b/lib/src/models/selection_error.dart @@ -9,7 +9,10 @@ enum SelectionErrorType { amountToPayShouldMatchPaymentRequest, /// The payment request has not been completed. - paymentNotCompleted; + paymentNotCompleted, + + /// An unknown error occurred. + unknown; /// Creates a [SelectionErrorType] from its string representation. static SelectionErrorType fromStr(String value) { @@ -18,7 +21,7 @@ enum SelectionErrorType { 'amountToPayShouldMatchPaymentRequest' => SelectionErrorType.amountToPayShouldMatchPaymentRequest, 'paymentNotCompleted' => SelectionErrorType.paymentNotCompleted, - _ => throw Exception('Invalid selection error type: $value'), + _ => SelectionErrorType.unknown, }; } } @@ -32,6 +35,8 @@ extension SelectionErrorTypeExtension on SelectionErrorType { return l10n.amountToPayShouldMatchPaymentRequest; case SelectionErrorType.paymentNotCompleted: return l10n.paymentNotCompleted; + case SelectionErrorType.unknown: + return l10n.unknownError; } } } From 69fa1e5e255bde7b2ed6c65f810e305c2639e6be Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 18:28:09 +0000 Subject: [PATCH 23/27] Update CHANGELOG for version 2.0.0: introduce `MoosylFlutter.show()` async helper, modify `MoosylView` behavior, fix Masrivi navigation issues, and add decline handling for payment failures. Update version in pubspec.yaml to 2.0.0. --- CHANGELOG.md | 7 +++++++ pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac9ba5..431c344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.0.0 + +- Add `MoosylFlutter.show()` — async helper for full page or bottom sheet, returns `PaymentSuccess?`. +- **Breaking**: `MoosylView` no longer auto-pops; caller must close the route. +- Fix Masrivi: selecting and paying no longer navigates back to home incorrectly. +- Add Masrivi decline handling: show dialog when payment fails (e.g. insufficient balance). + ## 1.0.12 - Fix an issue with payment request fetching in the latest version diff --git a/pubspec.yaml b/pubspec.yaml index 8f61362..2df7bfc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: moosyl_flutter description: "The Moosyl Flutter SDK is a powerful tool for integrating payment solutions with Mauritania's popular banking apps, such as Bankily, Sedad, and Masrivi" homepage: https://github.com/SoftwareSavants/moosyl_flutter -version: 1.0.12 +version: 2.0.0 license: MIT From 160388cc0776e054be8e8c173e5ca7815e41557d Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 18:57:24 +0000 Subject: [PATCH 24/27] Refactor README to simplify payment flow instructions for `MoosylFlutter.show()` and remove `MoosylView` section. Update API documentation link for clarity. --- README.md | 22 +--------------------- lib/moosyl.dart | 1 - 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/README.md b/README.md index 84f3ad2..4d59503 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,7 @@ return MaterialApp( ); ``` -#### Option A: MoosylFlutter (recommended) - -Use `MoosylFlutter.show()` for a simple, async API. It returns `PaymentSuccess?` — non-null on success, `null` when the user closes without paying: +Use `MoosylFlutter.show()` to open the payment flow. It returns `PaymentSuccess?` — non-null on success, `null` when the user closes without paying: ```dart import 'package:flutter/material.dart'; @@ -136,24 +134,6 @@ class PaymentScreen extends StatelessWidget { **Parameters:** `publishableApiKey`, `transactionId`, `isFullPage` (true = full page, false = bottom sheet), `amountToPay`, `tax`. -#### Option B: MoosylView (custom embedding) - -Use `MoosylView` when you need to embed the payment UI in your own navigation: - -```dart -MoosylView( - publishableApiKey: 'YOUR_PUBLISHABLE_API_KEY', - transactionId: 'TRANSACTION_ID', - onPaymentSuccess: (payment) async { - Navigator.pop(context); - print('Payment successful! id=${payment.id}'); - }, - onBackPress: () => Navigator.pop(context), -) -``` - -**Parameters:** `publishableApiKey`, `transactionId`, `onPaymentSuccess` (receives `PaymentSuccess` with id, amount, status — you must close the route yourself), `onBackPress`, `amountToPay`, `tax`, `isFullPage`. - For detailed API documentation, visit the [Moosyl Flutter API Documentation](https://pub.dev/documentation/moosyl_flutter/latest/moosyl_flutter/moosyl_flutter-library.html). --- diff --git a/lib/moosyl.dart b/lib/moosyl.dart index 67caa8f..d0c118d 100644 --- a/lib/moosyl.dart +++ b/lib/moosyl.dart @@ -1,6 +1,5 @@ export 'src/pages/home.dart'; export 'src/models/payment_method_model.dart'; export 'src/models/payment_success.dart'; -export 'src/pages/moosyl_view.dart'; export 'src/moosyl_flutter.dart'; export 'l10n/moosyl_localization.dart'; From fba8387da629a1800486d0bd7ca0ffd9a840fa58 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 2 Mar 2026 22:51:01 +0000 Subject: [PATCH 25/27] Update CHANGELOG to reflect the addition of the Package UI update and clarify changes in `MoosylFlutter.show()` and `MoosylView` behavior. Remove outdated notes on auto-pop behavior and Masrivi navigation fixes. --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 431c344..c2a96b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,7 @@ ## 2.0.0 +- Update Package UI - Add `MoosylFlutter.show()` — async helper for full page or bottom sheet, returns `PaymentSuccess?`. -- **Breaking**: `MoosylView` no longer auto-pops; caller must close the route. -- Fix Masrivi: selecting and paying no longer navigates back to home incorrectly. - Add Masrivi decline handling: show dialog when payment fails (e.g. insufficient balance). ## 1.0.12 From c4d6c7b58f5d4ab519a8381af17a130cafaf654c Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 16 Mar 2026 15:22:01 +0000 Subject: [PATCH 26/27] Update version to 2.0.1 in pubspec.yaml, modify payment handling to use PaymentRequestGetData, and remove custom icons from Moosyl widget. Enhance payment flow by integrating GetPaymentRequestService for improved transaction retrieval and success handling. --- CHANGELOG.md | 5 +++++ assets/images/test.png | Bin 31734 -> 233252 bytes example/pubspec.lock | 2 +- lib/src/models/payment_success.dart | 4 ++-- lib/src/pages/home.dart | 8 -------- lib/src/pages/masrivi_view.dart | 8 +++++--- lib/src/providers/pay_provider.dart | 6 +++--- lib/src/services/pay_service.dart | 17 ----------------- pubspec.yaml | 2 +- 9 files changed, 17 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a96b0..7f78dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.1 + +- Remove Get Payment Method +- update package screenshot desc + ## 2.0.0 - Update Package UI diff --git a/assets/images/test.png b/assets/images/test.png index 6636c90c86fe7dd5992d7d98a50c2eb21f5fea9b..9b59cb44e3d0cf7397ebc9c1274254b709a897a2 100644 GIT binary patch literal 233252 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

UH|;&%$w?W%>fJ6+y(kpgVWSoD1xJyrI99Q=9cP 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

t-AC=Y0 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 literal 31734 zcmdSBWpEtL(k>`Q%VK7hESAO0V2f-qGc#EuCX2~pW@cuxm`BXa%*;D_&-w0mcJJ<= zjlB_fM={aWUHMd2WtB23Gpi>=K~5400S^HT3=BzHO6(^X7&sRg7(^Nz4CsjmUI8)~ z*e5}AQBehHQBe{F2U`dK1 z_E9kS1ck}lQnazu2f^W_EY^h(3@v^_AKK1@g%Rgmt*5${=Tq@Ke82a+2lRgDzRvu* z4{UdY1-nJps^-F;1tXDHA_+mdGYyxO)Bn=~j*9~Rxy=VPF_FncTrvn!4Q9DB(S;L? z%(9Yau^uei#4ETew5w0M&<2X_&V-4avCZ0aQr{?dB+fzDpvg#t| zal_N+za7(qOBP2JV;c8S3(Ev7wimS5m!GM6vhu>Q^D$l?yZLS zQPn9zOgH)ms_)C`_v>F2ICC%XxIb0 z-;E6g^<^*88Bq-0{#!co7R0++pIX`%V1&xjwu#@09;7*Gza<&H8AWv5#n(a zY_S2lt=_Pj=IqH(z=bghL6tC=9vE%ep&r^GO!wVdpbnt$03Je%1Uy*?j391wBbdT9 z3QlKR2=xJ-41ACnZ=dNL(??{`o6rDL74{G5IYV63AhAEgrt5o!fG4}2pvLKvl>I4(F(w$~q4>_fplh(^DZiYCD`q}goNiZFaI z*cIp}{mqVvS~{b};U;5s{g=GgM%krW-NpP>fKFn#LB__0=mm!ADw%?VYCR9xa1_K|F(lfRXHVB+Zf5q)^wP0N~j}9T2gopvK0_1QaB4%)elaewh-+PJdC6a=X z(Sm=IS4*+R1R9Z%iG_{|?U5P}A{g=4Ko?8gkp@M_4!Y(+Hb?M@x&4_Rq{a|c{$V`> zS1%%&BNG=8n0Cp1XJvCVo@9gH#D%K zNBG{w$_&qeNY^)9{iO`c(r^6n%UsB<0kJ)q+yHJ9q*fOx00P)UyYtWQrG%N(E}LPKpuO+c0wc>WW6lQ3FphEfB4b+a`|W|T^f znvu$eWDZpxg*P}VN5lkP2z)pY$Q&9gLb_ zEg44Yb24ITT~a}E?*cOgdfJRQ8iY*Nh?=;Xh$*pVvBMmX0*jyJ#cMMF3l3*SXNLL& zw-mRiCkb)!KAApIN(u2IR!QUXVV!u#FUZOQB^Jd?#Wl)`%K0VVm0XH46xz$50wr?Q zCtu8H>mZiUG}uouPQg$8@wCT&MCA6Xrk1N@t5p6HKh5V6U;UY$FCg45)vopG^F(>8 zgA^JV52NW{%JMx|WV@pqN4jLDoZJ~ zZ@O@J7lwZXOd*n6kR|dx9uQxL$K(BiH?Sx>UB7J8si!>9I`CDZU!qIGAlF%TD6o*tk#ZJoC`V)qNiG?H78$A7#DNzZ0W9+EdrDGwKhGh*{q!=4ioSJvPxkT7$Z!B#+&5o_gZmAs4ZQE>S z?CBhO#TJi+n=qPWT$2DE+(+-YjxHys$7=sNcJAxyeAeOCX>Ds6P>VthU*y=UZcOH! zcMo$ZMDC#;Gp&^`tSn}lZydgl&TJ-Lw{2bjn!rGpTAzA5wLYY~q!QN z2=|!=$qac0K?Y8X_<>}Qp5|*2RT0wyJHYKC1VuzjZSd+(DgpWDD8;cuL}%o5+$FpM zJS^-6fs-Fi+D#N~v=9++5o);hOjE2sF&0svbGUxc<(^4uO3FtHpoxZuL>33T;_-g@ zC2_LI<+jq>q-}q`^}3zdzb;!P{fDZKqJ+ADN`<;Z9zjYYdWQSg&O)*w6BRxMVl2L* z6m1CYvf|SKbfhMK1>G!rJ;iN5;P_H;ATv{snbf7bI^+nw867zfbt-1+SH9<_gnen# z>tWi>@QnabYGdHN-}!)U-#ID(Q3Wo;7+_j9cmTWsY9%km?xred%+eVl&Lip?2N~H8 zdu5`>*OHr(aMCt1Mx`YtxTxh(4x`q~v1YJ2{|s8t(IPubJK=V{-4T#ZGIAG)5ftdG z7Q%lUY?QXn>y#qKdcsh_@Jfo|+Wo6ER-998J$+KZkaAZ>Q*_XH_~5j5lYm?Bbq@&O zie+~kp*K=_Doa@0tKAAWJtlSabRBv}YZ5^@< zeCvNHcWdpst2xzm3I64D7QVWuajK);K4LdNg;yt`FAvL2QJS%oNv_)EFuQQ_=8yAit+2NmZpg3tUbQ#-Rx2f^FX zgp#sRDg@Kq4L4l34=>x0W8!14)V*R~hFTn;Qt9R&Vb`Pi;^ZgA28SHs4HQ# z_#Go0Pq%Idj|n-^Y*t#5-DPPtwr!ntu?iz2SJL=Ajj`x!pSs5ea1N`T1q+XZJjXUyV zf^@n|ZF~=j>$=vShR-cJ(mF_OIxiCMW08%Fwwh15&%|vb?)Q(Lhp?~6yn?KP8Lx3S zhl~5Px>CAxUiGg_Hw-7s?Y?ck;4G@$@$U$FRXMo0U~OyQU<|lmJbaly255oaSAI0W zG6$$Pbd@yJ-998AjfZ!lX~cIM8;B|wd_FH>&s!z19CNT=3Ea!WQpF~S1msZ|7^_K}$jX9!0hQsvAi&YU zAVDQ?&>vWkIT+MGWiT)*&=m{}GA0lV7IZ}eeHF4G{;L+8D+}_!$`ENEgu;rV($b)- zqM?Jav5ljdt&<|l8#c&`5j0m)b5fI);W4zeX4E&bH85rbSlfL-!1w?>prW<0lRgQ+ z+RDa}2f$DE4+RgX{PCEHjN~5@Crf@ZHCY7`QCkON5)MXYMrJYr1QHSwJ_jQco}Xe8 z|AvF^_{q$ioa}g*m|R?37+tR`FZJhK03^tDB|7zsF z+7UB$G;}bxb27KJA^B)m-@w+{iJy$@gVF!^{HveF0Q3LmWaIenVSxt7^ijja!pO|@ ze+P3iH~D{neboF5_RqNf#g6ZzF&+hTfU%Xjn7K7bs-Uh3urPBm^Zmo;f2#WLp8gA} z>}c#DYHJNbItl#uy!;#dpOybt;6FUo{(~ns>;H-8e^mVk^kWb_@($)8cKRPu6ky?F z`k$Wto1Ty9L&X0f@_)_eKTkmuDuBSp^gkAd075qx5j_}~5SX->uo3|LL>pF5NqO#V z%AGt97OtHWJi0)wims^15?kweX+%S%y1e!5i1A^W_m`?YIKDD?G+FyS7TQng%sSd3 zZ(#b$kP#~>f3ghFcc}IJ?s@gO!v)xW-@dQUjPr$*3=R&$&j+7u<1o-Vh6)~3CdooU z@r(5(0ae(9`JkjN|0-vbK|z6P!-wb+$JYKmTdK-QQPPVF1qCjgwThxvXNF28n_@(# z%f^L|v4sM9>>?@z2V8YiS|SIv!2$Jq!%87o6x6~B5;!TB%kdBMC=wF51rV27VbagS zLMV2eAexnO$$WkgcfAiVOaK?C^}nki8plSW_`8ojet`eiGVq0IznH>W4j|mz+)U!I zPAS=}HXNvO*d2zM=b)v9e;FAcA84>%w#eJ|kPOEG2gZ7n`3bp`{oRfZ7_iWkl9BmD zMn<-=y*+)fRZ~;*CpmfXz=T??(X zI9u*Y5_>8QZNDiUa$61lA&uY9aJMn8)M@{co12S&`&~w+jBh*q>r7>bg9dBb+JUD3 zxB_vreN!nqfeV>S+Zo%LM}ldl=iFQf0q~NbYX`W;dd+{ z0F#TT;_=#s7cR-wQx8!Bk?<^Yavrf zQw=u!4IQ!j?q@iGO|PZ^DMm$qrewp0jP`3hQ?OHlK=$`ah>X$8G-21yAY_>L2QND} ztGeAENCHo}>>Qpt^MD-|U^i6mK$WMbI{ZeR2Y=PYWO9BFp4$7{>nwXV8ONo(h=@q4 zg?%K{K9-{$A14+Tmc*+W<`MHn6py0og@^cf27jqYDRQmpc>hYXqvcqu{UQgc`QC3PTI}oVoel|y^rG*zO7S18! zxbPslGz61uAiy!>{@KkgVRhJ%D&$;yIvNTy0?EBW1BW~;4@;QEHwLC~Frq;JXr!XA zTU113B3BZbfXgnc@5)L_!lqi*2$;sb!K;F1Zq*ZnJTO4)9Ug#)J!dB9LLXFn5zU)e z&*=%2B+|>Nrj^=}$0z-}9Wg4eoHF<8p?7U{ro@h@44!qf8oDQMXdWgkC`eMV*4m+E zWo3n`(Aq+zabg4DQyBN0{Y2s%n$RYY@%H%O`83k;t^l##Y zh2Z<_+E&f{$n_O7H5AIyfp3mmzCmR{$(S?BrK5RXHDS^Wih-@jGVpj&iI$cY+|o9c z(^lfgj~|M)<__?VQqo*Z>f=BNC}JFM&xA)HCGq`yT84qUfP znR`ci_hG8i$VS^<-s^wgJgfTf zZ{;_SUB23ryz{Ux_48DRrPzl=0VEvQ0gAlLEPc!0;+eI3y@o9n*E}`lyJNdmyHjfV zNa2}A8_Ki&2=<6-=vLpet;w=HTN}t$LpQ9-(Sks_(CHf#0X&dFCmH(8=YVZzYmxah>FKzT)O>#3rLpi+5tWuUg+U; zp?`-{gs?x%tR`*9haE|W1^04fe%~8Q4Z1y^Z#3)COy96+-o?6=15k0Jf-C?E6ai{h zlnfn{ieJ*sL;aBqfPu8!BIjy=(PS(InzS+D;LDIGik&x9(oVFIzxS@2F7kp?&baCHZ@OTSmdIsu8f!!|3SfN`Jh< zBo~R;urtS^^Fnz{QT^{FC+#9ZVFfBI-HW$XJC)DV&J9S zA_mu0kCSqijG9_>w_PRdXmwfwZs0?R*m2PGl7eRSED*FaX}v9~lLXDA6&zgGViwBBe^S#2&DmV;6cK1u zRe`{!{vrgR6|!LiqG{HQ`sV(zulN8XA=rQHD*j(Eu$Hqsp0Bh;33R_ZDZXv4f}xC) z`ezX;k#5}X#L5e3663uPdB~6NIc`qnf1lll;pgMCD~W0WCVz#!Uv|#it!%QIzVBjDN5W%+lhLDp@l=-F ztWwqWRMu9QE|hoqlyI#gv9WC1soYxtA0gN>^4jHjc{Re+=y1r8_?jCf5c=m2%4iCE z=`L%08V>=)nCb}LNcUl`rD2`RT5bYx^Q-DeULehhbiJaK+atEdOjVFT6I~m4`N^_G$q^0 z1%_SEb?+_)9XfEOUANpVSG&is5?QnBr z%@rkcC7Bh-Bka!0j)Ij#%7-b{0h z=~&+KgbyHC#t;OJOm3!Oqmv|74upT6-sdCiwuuIY38ezN7JJI1&tkqa{&3OoWHc$} z|8}}>vZcSt%|(TFhMgh0OO76ib!TXxFEVAHw2<_NZIY$S{4{DzyWOt$kSl`fFKD}1 z0!YKQ)Yak`f3_!>BXjk#yFXiPF|+eX$8P0L6Mw!;nzGR(vv>LewG^>|W86W0t!J{? zH3~Vi*zF%m;=P0cPKC*!s_h&<1x#XA6L>lx&#yjoB#eqGTdWQofa(*JJEqU@#5-+> zDja}b2RjWI2*G-OSdx$Q^Wld$m9V>Ww6UtWD8YWNh8tRR`SILztifV@8g467mR+sV zC)Vqzr?DVo>^H4JIA9rR=2*+^DzyX}d+TV|Vk|zg9=~c%e9>1CKA8Jbj+C69M$81@uH>O?e1(+)9o(hCLy{n2Q`XcqBjrHfIa<)07DeHr~k}twR|bN z0Iq1b(LTxm({dwi*QMTWEAx8t?O!JtUfoXwQS9LS59xjAg7cz2&q@0mNp>fyn%b2G zKgnIhvb5oWgy8WUvtmB7B6Brr&o)OdnqvgJGAL$T5Rp*xHl;;Kl{LL5QgBss{ZNIn zLy<}@#ih?5`pe66se*xLc1E;SMF!zK4zBL;i$$TBIvxW^k)8VaplG%JWefmmJ0VRH z5~z7Bn(w$Ebuz&S(pdF~CqIioYOM|$q}Hr`YvMk1-~VqQmGriVam!7g-fA$tW8}Yo z_u`QE4vo6rlfN0F8)hGNa1w{o*sb+_8RB{s)_azqrl2T7%%NoBdVcA%7S~&Aa|PT? zN?O|y@Vm|I*;M!o-i<;HldYFqGp59Jgz0oUz! zku_@VT9-6#T%=5aX~hDw1Dq&xWe<=`3=D#A54op3D-7w4JJfJ>Y!KzFumi-F4etj` zx;Gv`Y}g={%SB5ZhSj15U=(~L{OTIL)d){j|ERGee_dU|Ax$RK4BSXHImrZ-qn$_f zA!1~w#rV=^-8gy={ThkFdj7~lc2th~j@LU){KzN&ZVk;XA74n=?w+2W1Y%336e})) z!~xfugdXFIw#jLELGA62pU?Qr3yAn!tY8UV^4gwQ--92dZo`tN5cm4>0Ie{ku^$HF4y~rVQGNiJ#skVkVOu zFq26Fxc{Lu@u;8e@!rhQHrtv%2@v!CD*HBYd4$dwd%efOGZ#G+_3FH)MNDNQ8$$rx zwdYFgv)@wEbx$Zm1^7ZV;}^b`b#HBm}3vEj=-Q*`f>hytq8t_sOxPm9^GdrEdfMg?$3!sF)=X) zBdM$OJhth9$6e+p$AB^Rc^W}&$!8BGD(9EU!@beuT!S*r#tPt-Gj9UDdd`+zy7RNM zKw5@)=GT6WljFSQy#*}ow<`S}46HPjflWSjBG(;M(zA~H99>8 zpY1C6{*)jNd}6=`_;bZ?g3gpxy#DT z|G@6kU*Xge1jatk>p;$5?$E&1u|rscTy&?30KQ*XPL^L#djs+YTA+O zgcO|S%7+4~WhnyG`0ECVELpfnCNNW@UTi-q&bgfjGP2mg0(LXzq*1Q$ddPC5D={xe z@%fPJy~i>|?cOnl(&9sB!pA{pE9psB%kuc^IXryBu1!4j@*g~Oy5l4E#13wAJ8HRx z-P6~>z^RZ-l8Gg_x?v8~iaHMOg>ojmU&~<~fQ6{->wqhe_g_jAiH%XocLYcSda*~b zBN1jsdD!R{{5sABN?OJeWU@=kHt`h$yx}aF>mv@tu+(^#! zvSF{yz!et~I@JF7rp#GOWXb`zpteBj83AyM-d3A2$Z;(^8<;v2d^!Jk)Q7J_qqm8V z8noOfZ#edrrjz=Z!gc$@n8X{(_m@zXa)!}H*r=B#RAbn_(aFBOlcsCm)SJN$*ArPi zr~Shgv4BU@xBjJNy2`ekS_O^F_K+behRkJjy!}3kf&eCgIj$xvDz@h)kICyVWb^(^ zS`1o@g^oAdEq3nC?S#hzho_lf8;<=wMC8E4V{2W>e!p!Nd*(|sznvQ`$?{u=CQlwy zSI1|abvsWG`V}!=Sw!<4A@;B$JNu3`P*&QQ(LhUCNj7>Du_sV z);ff)OYv+bxK*6FgoiCVsxojpJXxP714o%2sDaCyK>c*=#J)F;2ft}oGeO`;hnyDn z-E_|J-#0B}0SBVHN4pOCNUtT`{jSHQ{V{X4$LiERbx)_{IUE(V=|{WdmUJzmXGaf) zG{J$%TNf+3Li?LiI@?ub9$SH<1{QsmIXp;^>A$<(e$e1GY78csP=Tw1@2Yn)eCRfc z2p=QwNK(5-`6qt)7ZqI6&oWjVX5bs!UD}u$gVkN|hgU!{fdZw`CuS|xg%r`V_a~#x zWLi2^8A+Du^22evCfO+JYUb;5&Zvk^_^{iKsCE=hD$7(;E1}=q{&8nhK#@}|N7T5R z58tEa!}qY7m+JcfH-m22v4y3XK$e{GBfdKFD?;fbzWToxM5wf*rY$cI%A|JD7@rB# zk{^nXpMP_IAC{OnlM0i}uu!_HSv+SilF%JRL)L%0l0H5mVJVUFOzyYOkdcUl1k3|c zX#}R6s+QKuf)U!fEXqc8Y(fG|#}LR77x&*jr-6coCM6@wX}*oUA;j_B*w8aFH6<4i z=;$hE#`(0t3s6#0@}HIxyB8&)r~foGG<2|7Yg&sp{{@fWtF4fbQ0*705MBWYs|*z- zCAJb$kWZ{S{ycN}NMtxVSAu2#XtRPq;-gf7B znIxM(kSGoS@#hlFnnqulo1yopvf(94;?qD~4KzGli1$KkjKnXwSc-{{+J>g&Ds{&o zd-Y7$r}6sWm;>iz;iqX&1C1X$eev+Peet$*I>`8?3o$W0L`jr8fzp(;bM*IFwl59= zW+3tB{OvtbeyuwEe2owACttbmcIRb_k>8$NeytM$Jl>dY6abXNhpWzEcf>uU{?PO2 z$(}CEguJx~C`af1m+;YQ=_&rwA<;lP48f0K2}AL$l8IJN{tkM(4@9Mr1P@1K+cMGILf*hJlhOp!#fAS!|!hV)eTHm z9Mj0}LdB|N2n?GS*GEqG{*hl}ma_{4@#RRyto|xzx1$~`vM}W%e21QUbwknQBodc! zBNgJjPHJNQggc2;n4a;wDmd$xP{VHMNmBdN=pDX!#c59;o&Ds6uUeuTTIEX{xVPXm zR8RDHi389F9(EJ637$yD>w;b&1n#Y0=thj>faR~Eiel;_v`w^!*BZx`Kf*IBvuC$ zfp=F1Kgd`6!v_{-{0=JC>p`i^Spk_Wo8hMF)7W51+6QvY{9|;Wz8E>nb#O`0e z$(XF%-?GL{MpF)s!*07#ZWOSH9>gyrWtKhzdKgEwo8fg=T-mooO3xEGy z-eQbC=m}H^7~-6zaf#fg5@>_@hII##^{M%Ib!;*!qz0<$_r|b5>O33d2wea;QT|x zesD{40z~961%}8VFQ3Bxp){n$Sf+@oW#WZ8b%k)?e54~Kn2acDPdh3` z)+`P~!1B2tQL}ra*%H@dkl;3(>+MF?H@OhGckt|zIVHSqVFOljm7caFY^sa{y=&Ex z1UFyCBe@x1c#B0aG$^c3mU`P$iIa{Iv`(WlKcV+xc(j8DY4F{GN=N;U z*Ct1{zfa7fjcHsb06?O;Jq*Mj_|yxIr-*jsrJdf@(mv^<=q(pd7mhuV}^2V?ZwkkE-0} zLbA7a+8;gTWW1+A4j`Eng!$U?NrE6?d89ihS1qM+WN%m6ID=3COf~SP=5cmk?R7+e z&uMwTFnmv#^U=~Y!*^e&8Lz`@?ZIow*1@F{)bun; zPs@Nld~(5cBXL!Hysn7L(78%6<|;ft`UNV3&knu?n$~#*s#=FR@~=GOQr0^Hxvrl5 z$ogH<$USnqU>V^1-npoTdcyE%s?D!`XvzgvIS#xkzRbahx%p>kDzd-Ru&tqrRrv>S zK*X~^A_3AbjFy^MZa0-^vnltaWR0p{=O-ejL=aWpm{sD)hXk?UJF@fo;tiRiwJp?P z-qO{7n{H8&cYLJ=<-1Y`c8e z3Tb6-Gv%aTYeR$%G~n}ICjKGf|FBWbHoMRv4UhAk!g&zNmcM2ry!jJ3_B9X2VIO$Mg!eE{?~9;#y8088;G9{=FTPG7)f(l-D8}= z2rIAlndJE&*82tplB3iUWkletHSMW5!*Ru2lyzTRu65{Fo#Fi5oJHTw9@%ogmZfYp zmT)0CvU-cc$cl?8jg^*@U)3Z=rr@Tt)U)IWgO3ODh7o{x|jaq6x)o8Nc7P8S(V%G2fRQ6fTB$;|p>xXu3 zGZN7cPm2Y(EUPfMPGElt*l;i63WDFx4%uy!a4>oY5j2?od3-!0pdKQEVth62Q z&ExpwyO)+1h|0Kw*)V+(S}dy<%}5NkLf%-(SOv+FH|tJ&!YYnJh6$iL3b{EM+L$Fc6aGzACvmgKIw(cfzsWH5FT_0}uU8@MV?4wM}2?g+d51L1?mWBKy(v;TUu*x9kPsPx&oMbaCwc%6>rEl?o8%8$Z7U;={5Aa? zcLE73;Q(b{X*tn*b~&LSabCdh4H7;fdC8rLZJo5evoLA{fQPEFUyZwhmFmNkfdq4Y z1l&!J4A}r=$tmz_RsY`H9_$&x4B02t+Y!HJg)Y;z|vSxG%>X{rvs zW>jhL%cja>I4C#R>;$jg3+TgbJfT-=4$za znkRdUDLVL;!`jAkhfx(uiHbu%3_B1aFG}7ak74gjCJZ!gwMeMj!tWZ}?u-FyN%BY}f-^dqIa&g?hdCbwY3DKpZ ziV1WKb`6rawMu0`PqMJ&RTGLd_KJx~N@m-x3mnXrQvW(%3gy#al?T%G;%3`7MN0K2 z-yGpZ>S*`q4L?I?2xnh})_#V1o|SXIpWr&C`VnSNToL^AxkDd)Go5WR;r8~|ZrgVz z_~3l;D90z~#(=O~bGVBgk2+n|G*ld9L5+9<^cf^NbJq9L6+)a1Y+tyg9e$3cpeuK7 zOMb>rr?80Sku_ueVJNzoL?U&jV4q`gM?iUP=byJhws|s18Z~BK5xY>QrFHkFoidKJ zbJ#>C)ZJ#NB0*6+rLX`cFPF4GAz64$iKHel{=kBDj6`jgMyY?~T zjy>3Samc*(;G{XP3GqE{DYda3w`$~>WlDMxruv_5H~F@_;T}ySs1xBqqrOtn8IaFSEdEUFPrcd<*}|_zSU&}22#mT2;mRCF=pC7+QVj|z zRfoIy+TT@|Ia(xzcr&QXM3K0^ZDx+A!{3Z6FBa9eo7YT8-f6hQDcHF8a&Yw0-f#t3 zv>y22T%B$SD|mQ;YaS9=)A+#kuur4ICb@sQ8RQ)Byvb#mAh)z?Y7y<$r7CK9vi=%`DPPH}@q`bpKY@dpKG z`kvf-dbcQRpq-DF&uC)gMrwXzZ#K9A9OJaZ{{{jX3K@_-+#t8-P_WA#_QPX?{rBfn z@bf>>V|0=~=Y=+y%_Sti$xm2mKm(EF6x*a=M4L;P{xBVur$cjbQ)(m(& zb<$ z&%Ml5YV@HqNh@t_ZF<>{%xf=UIRp=V89`OkYt(zSS@hS(R5zAmBY8Uyg3{!pq6+J# z(sX6T&=2Bi{u^(|-MmG~6idG~THIqo>O$^A84EWHf`uCjt|V41$;#B8C%Go3A_k4v z`0JVbhobItxXC%?j-}tZBFsev1Ug+%C7zt>cUe~G(9TzDP%@U0hNCh~BMQU=g)|rl zg$rsu)d~9LnLd$CC-1|$b`q|mqz{q1ct*ag90gRZy&7y3*{hGvrE-i{o9o|`yLk9| z12v=&txYJL!u}H8XO{X~OUi4Uo(mQk(%UKDXO%ytkK2zY7ovtZ`>GDv|9lnJAV9^1 zM<%9f{FY*On;QVeWb5rV5=6B z6xlD-l;|7_RSQFYMM{V{8tWt|?*8d_RhD_6zBtyvcH=`IiYyp1Rp(Or+B-`FS% zaFza;zK5V~EW>!_o^4luz2qNNaS5v20GV!rl;;v1;aw+O^T*-v8Tc_Vxy5AY(B5y~ z%@AY#wLD+deh0!Xc|0y2&{a0d)tZfGdN27$OUrN7gjCX;%06FTmM?bdU5b=)18?o) zE@PjwTFvYttD4T5sjE{Y8Bb?`PwBzoYee1d;0HGSE^J=u;f;-rWIQQQ{;FcOYOD$1 zVisO3^M4KKgILb@Vc09W;w+^+HQ;B6>IHGbmfOku_RqiQ=-U)s)R;>gm$7Pa0 ztjJSnv|}UIm1px^yKFU=!Nk)=TaZ^ZU62T51^5sA#NJ+u6K|s4mE_C3`^teqlvVf% z;*$9^U^+^BR_=5qD3jZh)0B?r+ar9Z%GkKJ(6yUJMTb89n!7dS@VBz(-Ct945njqE zDCo1g#4n+6=gf>m@A5_k*cjy{<*O1C{>^pqj`V-Fuwifc)epeyQSmhCO*-4~wC#8J%D-;>Y zB&W6@{j9iwdgL(dx$-tk+;H!pk@#%2GhpXlEX zvL7-$4VjO^*McLECH1nHfX#Z$6;1sm(bkWzG&){q?rpD+)#L z*mYD>>f8ZJU-sTeG7-Dmo496+oW#cXDu7EnL4hdo-?VzE=2rtoQpZd3q%e+YGNIe_ z@)$-u${PvzRI^5bC5W=m<31U2lJw@Tol59h$}8WVBQi1PR*(678KwYGfLQu*!^_J2 z0BIX1p}1+QWU({P8;G`t`A4qzm~6mF^T_30+U^&|&XxoFsi}fDeKrS2gui%%f-}He zo4p$sW&%?NLX{#nlS%{r~So2$FtS9z`9x24FFRI6?1=AMKt^{nHSgQmIJ0VyqBcqJ{c0t>eOCi&~xHm=$39> zI+4nruWBZSzf#^_NCt!Lz^ls_B7rmHdR{ErQu~ z+ye$J)Z$xPbU}vb<_^f?Ei)`l{oIYGx$mnXt+o#+=ba5`KUheE&xwRvq1$HZqSZ7< zNOPP`NN$s{QsPJy;AcU@U4y$k? zr_+*>rOsdCOo zZ!~G0oKm_039>(Ke26z$2ws}@V2!KpjGs@pSKqLprmhp(LfEM|KKTc^&*kZ&r?nNw7R zVe8@W)lc-*ew6{8Sx>6|aA>UK+V}eJQeuLoNx0}eQGH{e9e{85`F860@@O5q_)$m_ zU1>i%%8K>fzW)$#)JIqJ0(QEpA4vq`pRKW8p1bt+_BJPd$J0&N{*{$=qTW(Yx zi7q2m&I4?E@_njB0QIx=d(n430Jz_;CijWY4p9@n7st1%p&IatT`)NEZ^)PuUXkNr zgQdg}Xy^I2K3ZhO^7@PdQ0G2kWTIre^4@XA9%3mAzrJa>sF#(yLQ|1eEkni?QV-X3 z_|STPMb%Y>qF51szQAi7R#i>am=K03HpfS|ea}FL zLqzTl+2*$Jvb?{;fP#6P=7fX5P`?mC7wjIu=CVz+liZwLY<#Otlky_|d_G>0-7GE! zvl&Atxe=&IZLsktVOkI0-%t^dS5_uteL1xY*JVy&1{{rh`C_iMyn*=#4B7igsgh)> z&F@h9w_3ih>x0-N-3b2rVjDxRR$T~@*#miUjV)=_vth3)P&Xv6RB^tV{#A4doin_y zCD59voG!EB%0=1Fvh>a+&S7{^W{>kugZ{<|*1THyMq>RgA%TR8<%ctq+rwY#L(~~9 zwsN7x27CqS=XDRVaf!#taa88LuTMuR+rR7+^J8ID{}NZ@W_28IEE^-BDy^b-w|oOe zQV!`(a+1n9;rKbu!CX4>LjlOEwoUZrOO3ZJDWF`wjhZRi&ba0cJx;Hv3IfEVN-uF{ zX=9h5VeZ@#1T!LD(kt6kF{? zEPKb7@^M~3FoU3JWc~4+{@@0w-fh}`Hb5?0K67oM)rEKr6?zH^0N&z^EbR8A^BW2_ z*WE>3+Bq=y0Ujb;Xkc+-O8SY8j87ywC7EcCGg%f63F!yJKD@102^k;Xr?-~?CIrjT zeRy7Yk0}}9EEtHKu34YlS9zGvbt#oEL_F@e%vqhUw1LR@D%)CeAEAAY50ZW{4^a89 z>__G$HZsoSBlBVnI_hd+NG9-+$)Ng3yhuxa`}!}0h+RDkD)C2x0v$xOl1HBZaehVt zADpylDJl5lU4tfg(D50YspzRsAgZDWkRYsOdOuDMu0VlaD&RbnG7tPXA*i(AiQQ0a zZHei(9n_16fRLg=Exk_#mn2hw$MnNWs`kM+@dj-!P%NIyCLAn^@QtJT?E!Q`Zs`kC z+s-g3eN~FkaF?o{x@0Fk-vW2BK~GOF=HbETxt_JBd3Qvo-Y@3~*X+U-Mc6SUE-pUF zdxUqtHOAqZTN&7ym-l7J&iH5LM^K}5J}!uf3z$n z=I)pYK0n6*s+bFIHFcG3ZEvB*n)>oF_^%&>FB8q@{uunn7}>EvM>V0qIYmJ)RE+T` zrF^`NF$)r%vLPws{|5k}Qh^f6adZ{P)kUqBPbHa|y1B%4^ISsV~{08L( zUFlr*A152`j6+({N&i;14h;^13BIhTZ92VBp+y5jz@pOY>guQ>2qk+z@`7DM$loQ! z#JCcu_tW9Py~v?DpMrIhXB0N)cP=23G z3nykbk>JM16EWj|*MJilk^n7kEllu6>ZaA>b+KXJ_~0{pn3osPs>}Vyi>$Ta@Z+Gg zGJhPF%QHMrLzcv@k>$?7OaYce?7z!QGMU4ZW2jOLtQ+ZLsXt6O<}D6ygdtfPP?@gE z;(Y;i@DMhFu>0HizBN;1004lTi5HqG7rx(toHgp4bn98!*Mrp>wmoUmuZOooSnf#$ zXXMJ#*On!bM9^K6!TZuq7YgfaJ?e6`mD_sbid-`NDXh?vbYl)G(d|K1%mqK-+XLFs z8V#*L@f!2-F>!e4@HYx1K4j|1GGARiF4ma&o}OuJOsucbD{?36FYw=xa2wW2m=kmJ zE^xk#<`+@0t~?&E>ytJHKa>4USx%^P3J(t}ogMUen(IBkSp`F#m-t&QJ7c7w#GHW7 zY_#$T1G@lyQhj3^KbUsDO#*L)YU-#>A6=H35xAx_joeE-_6WOR;3fXd$4|$lj(TS2mP{vO47>FaAURW;lr{EkqY48I`Ws3 zrQdbFcmQIhH!_tX_X{LBDlRXIjz9+uaJ+j?cb&TbisWnUCw1D?c!|XHmVnV|>;I#@ zw~C6R`NF+}LkR9V1c%_k-3bsZgy0q|Kydc}A-Dy1XM#I}4-yz$27LNTIh6>D`^GiMsoSj1L-qR6vbnh@(PDX>*)Jthw^zg{{;6>*sem;k@=U{l1HMcV~irEOVl2(v^6o8tezBJg z&XmNA?+>;f#iTK^a5^r(aZjSQy`2=UyG6)RL1Ws&N0%S5>^^cyBp(=WniZ`~oX5C5 z&G*3HFh4R#KS(=Dt%Q@NaFvhjPibI&t+PMgQ9h^;w{;8X7nqLl_)||x2f+^ZaC{Xr z0wa9ljzu{yr?`TK&`&j&if0cUCg z2v$g!y7W_Myp$eIsG;Zb9$)PYthU5&&8tAGgVJ;%rYG?jmobwYxwqGXl)D_H1bGUL z$)xdM@S7MGK3Y!&sy| z3jX>a<+Qvj**X-5<$4^&wY5eU_tUR3r$T0N4JIMyYLABy@-??CE}>}Bd4>4i>nyah z&J71V)2(q%lIPno?f&7xuC@@c#v^gQS{1U~*U*^43DLC-^A7t@JMSyOk|SF+LKPtw z7WkRsZogko!k!xKnVy91m6ZhKw8u6(&XSk0mk7BKgXj378JG$k7$G2_wYe={E&m(^`K_}hD z*)8O1;{I8*KLw$`by9IdWflJY;jgMrwwuM;7%YW8?n*Lf76!c&j=1his`7K2O{d3}>h!}i zvD;h-0kd|Ss0>nC^yRNi&)7Jgyj?n-y|j&Uq9Fk!upF3`#NZ#F#9}0Me0W|iZQqN_ zG@hpOeQ=ajp{#6I^`cZPrp-tQjj2u30SVF(^RqOwlj(j5YEWAOd9)4QaHZvxSDfWz zBnS~CO;k+mfk6vS7~=%%R`UeO&{ zU0h)N4f~sS+mnkq!Crq%^r@itr<%vZ4=si1{9&Cp#`CODiDJ%5fH#@996fK#q&A~N zZP6Y+?l02`KPN#>qMYygxeput^&N#Py1?=Tb|fcWQUu0G$cG-&x}*Fug^kGQq0C~e z(=(apS$k4^ybtq>n*DDR2t#p7n*yR#B}Xsnb+%}vg72r4?b8!=-hVaCho|Mi;!tnI7_KJ z{u@am8uKpHD9U(m;8$ACQlP^P$IOHc)ynKGkv~vt290qsBEaC5zEa{y>AP?v9>~v$ zNo@K;rQ&veBj&_%ZBc-uXG-nqX9(IC8;7>j9+!O(iye?w0Q-Z0`36W#dCI{T6fbSh zU#~aa#W<&_wiE_iDBGz%R8HmhB|J`bFm+Wfvz}-E<3b-?*qKcKK6|hU+ zcQpz`*uCE!xw}k+cwLkYS#&r5*oNpyUuI^ysdC~77<;_^m5^*xg}?#xGAoE^aU3M2 zZ8X~dHLec-K8#R7*gR?DnD>UF&U)m;;57}C{kDQwB`keBSno$?Pcn=8J`-bh%Db}Jzd@iN7CmsIL z3XIajwRNii3f*@S3OI}zJ?6hVWHDdUg$Ysz?CMzvpVP{?{Jb!f!9-;Hem&~7XTJc? zJggI@9w@3(&nXSeLim>TPhbMu{*A~~y`PWjk#EME<~`VXo>CtS)#3nRf7pm6$MdT6fRrdJnbqtM74TbekTzm4B~j^sijf5rBk zZZ=0OHWhE@yV8R%NpxOs#Dm#08iOC!6BoNx0jSU8pEo%kNFh&7BuWW4t@1J=UhKjZ zsM^|Kif?Cu4E6B~im!KdwUT)>k3>Z7wa1qmio@h20G27UvYg1{>nNN@iDq=T=$BVL zamRku2e)taOT$*VK3)q&AVL@GQ!lVP-*cgp0UnT+<0rz>c@f3Wc6xe1@02DhGF*l1 z7khzpR(nA79A}~+)a+kBs2|;k`C^E8RckxIC~E{y(#7AeGWE~$PSl+}@$xx5%jA8< z6`$AfdHl+5X2@K^Mc)PHADz&Be`B5U_N)OK7D@zvC2Nh3`;;2KS>OI}i!{2gA$EJ1 zFX%>`L}F(3&*mF(#gF8S=+ldBfmEFk?eCM$zyB$QP??@lsMPDQmQuU^qt=43@3mTI zS8eYf&QDRXCgu#25^CL_y$xmCoWejvkahG>>7abpy~Dz`D*Qt#5G?_a125{*xzC+N zql+JTCmye0xTq3GOrLAw{8JP6RFKchuSX8mCR~8RWP9fqW3U;U;?@LcpFsh%?suX@ z-}wu>fS7o2dGY%a$0qCauvJk%8FkZAj!9kNGbG`P@cmgB%p%0o>0gGcS$U@x&^WdR zLx!qAP>eIbg0`wWLm6i~C07lE=gQWneCeqzm8Mnjj^5d44|KDe<=Oxh_jvV+t>VQC z043}YiUp>6AWDN5(I!718|7ygqhm$)p7)fQR&oV{^4N+6AJBCr)k%Ci_G~E!%9a5Q zH=7!5Qm`5W?|ChB_X({2(jcnUv5^hVR|!v6@L`VF# z$q#SyMYT4and$F2`^#y``_LMzSyoHghYa~yv;?DgiiU2yi@O*qFp)Ml#p3iCSYj5s z6oZkO42$JHz+z3uNLa-~U5se|p}Xo~*-uBz?5b33y~Ze*`)M{8IuVU}qe&tWU6)YW z06OhNCxqUwBl zjc4lBHX&1dS6-9Nu(>R1$hsDE8`VQun)3SJ2Mym-il5c)zr$$?wakkWI?vUMF z`uTN&bt`h7*>RwU`kQlsVfh&sqs9;q_t4NTlRu3K`;t95?%#d9_F8xv&&g%*EnC!s4fnNce3SAt3| zxGpr8&5Xb~`Js1Nxf#hPC**wC@DCejNlmHlyL``j_zQntmzzxk11Ixug)}NCcCtu-aGxVd`f~de)a;5EN;N347X#+x42|H#EuqV&4&mn_86Sdk%=k*fW z=~YnCVC>Fdeye=v3?y_NrwdTo_A0>t62Qz>o*lIvwrqZ#lALUMX&uaEaW3e4*(WJMx=!F*Mb3O};w$l#1p*bUv z&9j2<{;k)P-TKc@?>daz@GUNhxY+BSoSFFTm!f+ms{R@6H`$(;%2j{tryWuFvD28T zIFNlZzI6zm`y!ClizhK7m8(gx_#^}2imtE0G4jEFH}hTAve7LV7z@H-jHW2@AEjm! z_ZG!6q%BwDEiAUk596()GH3i1$I|S(b*I;^FI!bSFu*PelHIv4V2o?LJrxr$ohx0@ znkO85pYhRRw9W@4XyL#AQCH;QW`(*BV(cGEH&8N9A%l9|x2afgwmbPOrJLsFHcQML zBTv_R>jRv3TKq48g7!}LOtyTtEGNWI^7VLkrBL*~%F_j-$Zwu)rC?TYLd z4q5IxfD!TT2VA$>6OX305Kg_tj=Z%xFVs#9ggpmh6LaBR-O zKf*NlZvIQ{7{_Pc;y+Y>FY8(-@7iG9-@zH4M`*OEE5gzF8KtoO8{3xsE@%@n<# z01>NaAj!;aUk+9KnAD?0U%|lKk>pUsTf{s*uiz$k${0?n^oRmg#n5I-`<1p|$l5U+ zQa1RId_Z~OyA3ae_3Y*h#&7`l%>xeL=G!p~|0mJVd+A18`@0HGc6?2D&T2q2E%gB>_D&-zZvUy-qZle_2#)tXkc~H$JSG6;ur!o!P_JN~lx>2>x17xQ4G2 z(x;@VihX`@L6Pxsy%hsd)G}4#mT|_w8qRyeqvr&8cxwMY^$3Fhm4>@J|N6!T#o}xK zNOYXj+1XiCV0_qd=>PCC@BcV)^b03e(e%PGsJrWB$G=ditZ}f|nz0@5SzXM(&-jN< zE%LpX-y@<74vY1t7X_U7F{u<^A{F%N?Ks>~V)sQSLNmVnvg$l=KEPfYHZKkG`rkD# zYfIo|2`(Xh>Ue38zbrJ=3iFo%j$UZI&&`j-7tK!v+t?m)*)5}9Ho%Mt6{g${hKNNwwy?0~IeuSbu z-sYUwWxF7YZy6g%Gs|aX{~_qtz};0x->kSpC6W^gYr9lUS19{C5DSo7tyTUE+LcmJ z6NeWQyA-z!RZ{-2GRlN%+^Ox)Vc>C1Vp=GB*vqbtPcSco$^2`?R!I~bI`}9|S%w>F z^X=C+ryzHZJSm!QZgdUCN8DP4n&zq=qONRXC$P$c{FC+-K<^j4LPl1|M|X&bwAKhu zLSgrD{?Z)`d1zsjhByct;b#5W_pC|@IJ74jH|o{y@ulW}n`h|mT7Vg!*OX|bTMTxw z;*aGp?15eEFlz^*5)CUau5hQ&GrK6C*c0{aOVFqGJx!{)$aO7sQS6dju$-JlSnxf( zatc!}{fA^o4Ph^uf3UGE&TZdKkrPzi?GHu>AFZOviq!7a^z-M4Rd9ffvaYi^+Br#k z^lruz_DLL92DqRn(TB7+;eM51F2f(GG)QcU=&OYggplfHu>!ratsZ~bs7v;XPy{q} zzTDiBf&blrZg^$TSDl^;%Xc1ZEY7Y-Ws+co33h0~g9Y;wm5BQrI{7F`ILnXRe3w>_ zg&19Ry)ffPvUSR*y{s_4B00ebD72wV~FpIV=Y@f3f#8xF9&j*HGaMtrJCT2tv zJbS%~*L0BXPaXIYU`F;4a-IKe5+_XX-do?mM`5iGN26`;{dxYFm$z1Tz&I={wO5WPRFtAX2dkz1#Z&*n^?#V+CD;gJeCn@2YZ8^>_2d!v6oZ!1SR zrMa)DZ9?ut649vM1^0WxoG4qaSSd5En4!yl6tJ?H&eXJjo2WX2QG(6pS>$CgX`2;# z!HH-9*=qULZdZ9jW#pgq;_k}PuP)b^luI%g0&s# zyBQ~gq^q2mS6T`C%Gn8z$>*&{$OWFu?d+vSs;q*Y`-f_1MA=@?sI($66v^2L2a@2m zwSCKlensLGJnfcluA!s=RY2l5Wo#w_!{d4smo(p%xDjpt38``K+k_jB@qb@xAyQOS zRMIz-HiLeRwwtfFW)+F)k-*N72pq*7?FWty7PeIk72gDazSYhXnoT+WxPf;t>5bek~o%gWMZ#Q4KE5CM|_ki z|GB-T5xkmuC@n4OIMJN4(XM73I${2q9%M`!UgrH`#vIy7zzfADVZvHJy0R{)L%%|Ots08qwm!wxA zjc77e{6Gqw^rmzBqwuuOcN(r%E@e;ITm6-fXfLn{X!-WyU%=X?j{|;|R;K`K1a*mC zX{p)mk;3g?ce)BTQU;%y9?x~I2hTS9;U#G|1`DUjFw2?6tQSW6+Y`FBUf2mi~m>d&Qgp>?|Rd^`~? z3B#YWOU&LU$2gt6jo$z2(`|+o%_n^ALN73y_?TLqnkrb?F8aap1N@57^0tH4>JIr= zb=;NueRdl)y#uPO!2OB-+-e&*$wlA#cGC}gaU(?XQU>Pw7vQ;1yrJb+lKJ^%KuB1YSk6VBhz|Vj?6b#;`XXEjxTb6m83$ zZlJAfMRo3)Oi8&?>h8M}4T0gG%}waK&o}s=a(vK#X+^H$Q3jn~5}J3w=%LSkfY&L$ z@&^{gvY8&3Y$$18#2E?rZw?^wH&=9JepKiZ1 zr$Eudo}Nk-OdT&)%_^-I#Qv(;$YJ^$9=DjGLI@0Qv%+K6_ht+76V{1@!z?k4D`Y@W z6w~v!udt7tai?b|)Bb#OfR*am^!MgNb2e?X49)t(!N6f1&2vBw%U3$=yu3{WnG%fN zqD^D+pPyw&KN7X2{Qj~#fWKPA_GE3kE_i!EtfaOP4U{?j+p15gx3=K4HdBszVCXh_TdSHu_2boMt~-@|Yu< zgBS%XNfKCh40Ei@hmy=xkZCzvF;1C|?#r70FuzmqcM=-eLTqMu`$6C*v?75eYR8j> zDw^fXt>=D}eb&CjSZJ2}qgO+Jw3)1d8#UB;HhAMkJhD^K>>D*FHMKm!pt~KgjZM~a zNr*h?9!FgQUV6{;ZS}+Hn8t%owEX&{a#`D086Ks3-xA*_?B~c72T6d&5V*jn39Bn9 z`EzblSUnfHsc5CAWDSMUVdH3zb$~dh0J(;dk$gHu?1z>MXkh5p4=HZ9YnFuI$IMiI zzO)}-%5^lXTKbT`eS%uwW`>7jL=2f#L?4(K<4~o9M-4c^{rV!%y{O|htSjQip zp%kP$JxiAm z0R%oq&&c=7Myi{A8GY8iXO3oXfs8_p=&6UJw->ybUdBgW8n%e48-afn>}4;9ht!*F z=-6Ko#=&+&e;|nA@%VAEedQ~q?e+&S{C6Mtx%Tv7dYWV!< z*_){S$-0pH>`bj9|NQm4UKY06QSh8ZhBxa)Jx5&Xtd5Qgw6F)ca76mvRdiy^yjQlz zDJy(bE!4^A5Mn($A~8KaOK%zdduNeyM9UCI6-dHNUckLyyR}OhxJk4S*SdplZ6$h8 z%qY0qKTn>ceOiuw^!dDV6TECf2g_=pWNVljYsb5bmgi~sg{{0&Qcw!!uGc!rnJgU%LF(gBVMwY zYg-km)=E&$ongpQq6F!1>2j}Ce>;la>GV{MVY<@|hGqn(edqPciuhjt3rNCxn;Fu; zqhg+klVG8vL;5=*K}(4B_Y#03m`|}1KtN4U_+f~q`rscdV%Rg$i$zJZCo&3EnCbV$ z`Av`4a|eZDLTCwMiufandq4wzpka?jazgCXiu>9;z%oyHY?hqpJT_K-<;Li<|JyO~ zPjuzYOalP+zHrdC**FExTi=h*+mnn~y5dk5uW#?n*ci}^FUI($TkB*cxJb?SSMW+j*F=s7&BJ!(ERTm!cf!}z zgM-K(2|MWzzR15;yv4EE4Pq1Yap=5d9WKHCwWS|Sor!-3%%b%>Hs~$5gvhl{fEAvvn21W#q$ehs>XE zzF_)OjVy$N0PWqc(Ob`w^H}hZA2Huz=&EXJ|7D>FGeH_He}lS%VW|~u8Evc5JOJ`H zSwBw2rS;>FBpau-$L5(y?)>X8M!@p#2~yMFW>8atPquc45D6crWKM`_fGT9OkWVrz zCOl>!F+5@8Jhqqr=FezucY{#*0EqPi_J|~n*`rZX`c2E3N&*S`EWU}#sK7Wtb2)&p zrpISaX;$2Us<-6tH`=$H0EeFp?i!P>zv@LZ7ju58JU=DZi+hjLFyrQ0ecI!QHj|i; zmhs2C^_ooVJ;38@5s+?yn2Ze1F^i=In?Ntm`Hu>E!)SRz+T&cWkPC6&X5ZSdK*2h% z0knE9*4*d*%dc{Fxjt`_t1FuKTh_CP;8;NF_Wo@O^&7V($1o9fk-0O`9b`Rsf%FmK zR>ey@I0K=71 zf$egJQGQ#hD|S&^a1P7&q+zbLK~PR3Hdju}*jw|u)A;*i_k#i7Gs~f80NM4h;ZGB9 z#;XTFPOmdG^TpHf5-%j8aIgOihUTJ}!6uzY4<^CXH zpVvht_Bn!^U>%ooyP`iT{39MEUPjVX*sSlY%7sHVFFLKYZU?qqMS%xPLI%Hs7q@gq zEmgZ=<)`N$s!$zkx}xDh`!*GN!P0l5&kt4T5f$wtf7n2uogghU0Y8m%0X**1nm-j- z1=u1E!&Yf~aK{-)JMs`p)9{d5Cp6J{JzZE_*Klc0Tk4{4M6p>)Ydw^j+)W)I)9pPVD6o)?k$CV2qBoWT|eNA<$rox;bA z=(cnv3Mi7TYlZTw&TY1g;t{{Z2o;EZ` z_I?}ssZjii9}$AHQ`(f(6oNVv5JrL+d{{3qJzHAOzx-^;ugeEFcKX`tMO)db&%1Dh zT2Lm?&&h_q)^m@yVQR6j(LJ0wXAGSkAr9eTJ0tS(WRR;zT&{3)@en396 zk&cp&rl)T6;_C}0iDk@QEe*HAp*-F1LJDUxlMF$AvrD>R%+DVH=3qccGqk?nw!Zhq z{|0J!e8`@eY`ec)Icc`=-4c+1ft@m{#?Hm6N`$JiJl$aXOXm${8!-+m0+qM@ZX30> zII$ufH=RxQkR7lsY~uceTzdsJ;B?Vrtmi+s5OCSqbiSQY9M>)+{_t-shVyBE!fEq( zp)VYmO^sG%Oz!b`9=b8kB+j43u=&qvM4&^oQrLf?X|0Qs4SYOtvMhnUe0R2|xwnUO zcc$N0v-0%az-wivuY)eMxGv6s-~OQs)VEv~8x;&%;Ss;OH;yU_J<;tz16e&^TAyII z7&z}wmGHItt#&?y+{0`UNj$v#h1-XqQ0OIJ)iL(^JEqZS;Ipup4xu4EQa#VrZ!UAITs}&o= zD^EhB9oPFhcTYVVcPGAn2RN&=#LH?5%c;lzIvUQS6Rgc3$j7v8ZT)C8)su)IqwX2C z4t~|Ajoup91FRtl#@gkXmBEzf8$b^EGpr}4`2F3JE$n-D=*jbgp!15MzEE=|sGThE z;P`9BL8+H1qKJ&i$sRbitkTcz?(^BH>vqB8WP9Nb5OCz_m6_FH*=8ktaCy$@p|az= z;Ia80MkMY$MrHQoz+{YU&$QA~xq!yucs#P#nr*d`N$vEsw`Y5M!MO!UeYzdIq=F`F z{Ps&xW6IB@MzDY4_&Jn}j=q>&?OlL8ih0<+zI$06%mh3s%ue%XJUX4Ndmh}&UOd>4 z(f0Gci;pJh3&ma$e#9=T!gTm|AK7V_R1Q)$!Ed^Zcz#43>L@ZB;`hP+2yWyv9DO!{ zoYy^p1`x$vzw`VNQ25cg4xea!kvQh(t3VZE+$I&&t8H{#9ZG~i{mg~#cwCz50YpU5 zK<+Xn0%9Z`jbo&Xz92p#GQY0mDnHp&2A!md6Ed>BlRc(_^p8$PO!P&@U~d;>bhelu zo$RA_Yah^YXU!1;7tGZiVqOJ~;0cOkiFp;IuwyJfJ>ElxIKgcbW*2u02CJ8@ueUrT zGS~F9t^HTL{Dh+$F#}Rgj%Beh=&@b6niJdLT z@NSVqn8uPO${na4f$RY)KVHE<@2q~ZH$z5GL`2lKS)i4qA;N7+Tp&e2Ec(%zG~bAh z3;)iB0Wr6AHtCBGun_S zCH#FwFNWVQY<~0DF%`d_HQR_+pq|Up_Lr(^P}3l}{H5!s<1+i#uA%>{_%r-33mn^l zN9zUnO?HkO05NaZMW2)Qh1CxpH*dvnSA$zAOc1Ib8Gx!;!k}pwc+0kze~1-4D?Ua%!Om3E0Nu0i z-(ui4<^Je2jO&wt=i5st$o8!(K`}Y60x@5sFQQmKAR!V&dp0M!P zBJ7Wd?B3J~cP(9U!s29gECV0i#mrozA;NR->L2hUy6toNhK&CGBRo&v-f6(V)iHsm z^s5w*JUp6eZg@tXnlr;FX%YyZEYN{6RQ^Y2A3TpY^Eb+P6^H_V4?yW~q9jK^q#(d; ziiUaFAtDp&yzGHTU~GV4sQd9^I%d3fWeX^#;hcCVHXdsF^l`acbs72m;fe86Ccb668ecSZ`nTTr? customIcons; - /// Optional callback to be triggered upon successful payment with [PaymentSuccess]. final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess; @@ -35,14 +31,12 @@ class Moosyl extends HookWidget { /// * [context]: The build context. /// * [publishableApiKey]: The API key to authenticate the payment. /// * [transactionId]: The transaction ID for the current session. - /// * [customIcons]: Map for custom icons for payment methods. /// * [onPaymentSuccess]: Callback for when payment is successful. static void show( BuildContext context, { required String publishableApiKey, required String transactionId, final FutureOr Function(PaymentSuccess payment)? onPaymentSuccess, - Map? customIcons, bool isFullPage = false, }) { showBarModalBottomSheet( @@ -60,14 +54,12 @@ class Moosyl extends HookWidget { /// /// * [publishableApiKey]: The API key for payment authentication. /// * [transactionId]: The transaction ID for the current payment session. - /// * [customIcons]: Optional custom icons for specific payment methods. /// * [inputBuilder]: A function to build a custom input widget. /// * [onPaymentSuccess]: Callback when the payment is successful. const Moosyl({ super.key, required this.publishableApiKey, required this.transactionId, - this.customIcons, this.inputBuilder, this.onPaymentSuccess, this.isFullPage = false, diff --git a/lib/src/pages/masrivi_view.dart b/lib/src/pages/masrivi_view.dart index 3dc568a..f9591dc 100644 --- a/lib/src/pages/masrivi_view.dart +++ b/lib/src/pages/masrivi_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:moosyl_flutter/src/models/payment_success.dart'; +import 'package:moosyl_flutter/src/services/get_payment_request_service.dart'; import 'package:moosyl_flutter/src/services/pay_service.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -123,9 +124,10 @@ class _MasriviWebViewState extends State<_MasriviWebView> { final onSuccess = widget.onPaymentSuccess; if (onSuccess == null) return; try { - final paymentData = await PayService(widget.publishableApiKey) - .getPayment(transactionId: widget.transactionId); - final payment = PaymentSuccess.fromPaymentGetData(paymentData); + final paymentData = + await GetPaymentRequestService(widget.publishableApiKey) + .get(widget.transactionId); + final payment = PaymentSuccess.fromPaymentRequestGetData(paymentData); await onSuccess(payment); } catch (_) { await onSuccess(PaymentSuccess( diff --git a/lib/src/providers/pay_provider.dart b/lib/src/providers/pay_provider.dart index 63ff269..34cd818 100644 --- a/lib/src/providers/pay_provider.dart +++ b/lib/src/providers/pay_provider.dart @@ -168,7 +168,7 @@ class PayProvider extends ChangeNotifier { notifyListeners(); final result = await ErrorHandlers.catchErrors( - () => service.getPayment(transactionId: transactionId), + () => GetPaymentRequestService(publishableApiKey).get(transactionId), ); if (result.isError) { @@ -177,8 +177,8 @@ class PayProvider extends ChangeNotifier { } isLoading = false; - if (result.result!.status.name == 'completed') { - final payment = PaymentSuccess.fromPaymentGetData(result.result!); + if (result.result!.amount == 0) { + final payment = PaymentSuccess.fromPaymentRequestGetData(result.result!); onBeforePaymentSuccess?.call(); onPaymentSuccess?.call(payment); } else { diff --git a/lib/src/services/pay_service.dart b/lib/src/services/pay_service.dart index 1b5646c..85cdbc7 100644 --- a/lib/src/services/pay_service.dart +++ b/lib/src/services/pay_service.dart @@ -57,21 +57,4 @@ class PayService { client.setApiKey('ApiKey', publishableApiKey); return client; } - - /// Fetches a payment by its ID. - /// - /// Makes an API call via [PaymentApi] to retrieve the payment. - /// Returns a [PaymentGet] object containing the payment details. - Future getPayment({ - required String transactionId, - }) async { - final client = _createClient(); - final paymentApi = client.getPaymentApi(); - final response = await paymentApi.getPaymentById(id: transactionId); - final data = response.data; - if (data == null) { - throw StateError('payment request not found'); - } - return data.data; - } } diff --git a/pubspec.yaml b/pubspec.yaml index 2df7bfc..49bc45b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: moosyl_flutter description: "The Moosyl Flutter SDK is a powerful tool for integrating payment solutions with Mauritania's popular banking apps, such as Bankily, Sedad, and Masrivi" homepage: https://github.com/SoftwareSavants/moosyl_flutter -version: 2.0.0 +version: 2.0.1 license: MIT From 8215613a9b68a296dd36db4af27e91dfec282a48 Mon Sep 17 00:00:00 2001 From: eminos-sd Date: Mon, 16 Mar 2026 20:17:28 +0000 Subject: [PATCH 27/27] Update version to 2.0.1 in pubspec.lock, enhance documentation with constructor comments for BankilyView, MasriviView, SedadView, and PaymentSuccess classes, and refactor payment method selection to use RadioGroup for improved user experience. --- example/lib/payment_success_dialog.dart | 2 +- example/pubspec.lock | 2 +- lib/src/models/payment_success.dart | 5 +++ lib/src/models/selection_error.dart | 4 +++ lib/src/pages/bankily_view.dart | 19 ++++++++++ lib/src/pages/home.dart | 1 - lib/src/pages/masrivi_view.dart | 9 ++++- lib/src/pages/payment_methods_view.dart | 46 +++++++++++++------------ lib/src/pages/sedad_view.dart | 5 +++ lib/src/widgets/buttons.dart | 8 ++--- 10 files changed, 71 insertions(+), 30 deletions(-) diff --git a/example/lib/payment_success_dialog.dart b/example/lib/payment_success_dialog.dart index f687cd6..6b69b10 100644 --- a/example/lib/payment_success_dialog.dart +++ b/example/lib/payment_success_dialog.dart @@ -21,7 +21,7 @@ Future showPaymentSuccessDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( + const Icon( Icons.check_circle, color: Colors.green, size: 64, diff --git a/example/pubspec.lock b/example/pubspec.lock index b3b5370..e31cd2f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -275,7 +275,7 @@ packages: path: ".." relative: true source: path - version: "2.0.0" + version: "2.0.1" nested: dependency: transitive description: diff --git a/lib/src/models/payment_success.dart b/lib/src/models/payment_success.dart index 2ef1d94..15ffece 100644 --- a/lib/src/models/payment_success.dart +++ b/lib/src/models/payment_success.dart @@ -5,6 +5,11 @@ import 'package:moosyl/moosyl.dart'; /// Normalizes [PaymentGetData] and [PostPayment200Response] into a common shape /// for display in the success dialog and for [onPaymentSuccess] callback. class PaymentSuccess { + /// Creates a new [PaymentSuccess] instance. + /// + /// * [id]: The ID of the payment. + /// * [amount]: The amount of the payment. + /// * [status]: The status of the payment. const PaymentSuccess({ required this.id, required this.amount, diff --git a/lib/src/models/selection_error.dart b/lib/src/models/selection_error.dart index c160a26..bea99fb 100644 --- a/lib/src/models/selection_error.dart +++ b/lib/src/models/selection_error.dart @@ -26,7 +26,11 @@ enum SelectionErrorType { } } +/// Extension methods for [SelectionErrorType] to provide string messages. extension SelectionErrorTypeExtension on SelectionErrorType { + /// Returns the string message for the selection error type. + /// + /// * [l10n]: The localization object to get the message from. String message(MoosylLocalization l10n) { switch (this) { case SelectionErrorType.paymentRequestFullyPaid: diff --git a/lib/src/pages/bankily_view.dart b/lib/src/pages/bankily_view.dart index 54f476f..f4be9d4 100644 --- a/lib/src/pages/bankily_view.dart +++ b/lib/src/pages/bankily_view.dart @@ -16,6 +16,14 @@ import 'package:provider/provider.dart'; /// A widget that represents the Bankily payment process. class BankilyView extends StatelessWidget { + /// Creates a new [BankilyView] instance. + /// + /// * [method]: The payment method to use. + /// * [publishableApiKey]: The publishable API key to use. + /// * [transactionId]: The transaction ID to use. + /// * [onPaymentSuccess]: The callback to call when the payment is successful. + /// * [onClose]: The callback to call when the payment is closed. + /// * [paymentCodeDisplay]: The code to display. const BankilyView({ super.key, required this.method, @@ -26,11 +34,22 @@ class BankilyView extends StatelessWidget { required this.paymentCodeDisplay, }); + /// The payment method to use. final ConfigurationListDataInner method; + + /// The publishable API key to use. final String publishableApiKey; + + /// The transaction ID to use. final String transactionId; + + /// The callback to call when the payment is successful. final FutureOr Function()? onPaymentSuccess; + + /// The callback to call when the payment is closed. final VoidCallback? onClose; + + /// The code to display. final String? paymentCodeDisplay; @override diff --git a/lib/src/pages/home.dart b/lib/src/pages/home.dart index 75d3335..6cd5409 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/pages/home.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -import 'package:moosyl_flutter/src/models/payment_method_model.dart'; import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/pages/moosyl_view.dart'; diff --git a/lib/src/pages/masrivi_view.dart b/lib/src/pages/masrivi_view.dart index f9591dc..d069f05 100644 --- a/lib/src/pages/masrivi_view.dart +++ b/lib/src/pages/masrivi_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:moosyl_flutter/src/models/payment_success.dart'; import 'package:moosyl_flutter/src/services/get_payment_request_service.dart'; -import 'package:moosyl_flutter/src/services/pay_service.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// Base URL for Masrivi payment web view. @@ -15,6 +14,14 @@ const String _masriviPayBaseUrl = 'https://payments.moosyl.com/masrivi/pay'; /// When the URL contains "/success", [onPaymentSuccess] is invoked. /// When the URL contains "/decline", [onPaymentDeclined] is invoked and the view goes back. class MasriviView extends StatelessWidget { + /// Creates a new [MasriviView] instance. + /// + /// * [publishableApiKey]: The API key for authenticating the payment. + /// * [transactionId]: The transaction ID for the payment. + /// * [configurationId]: The configuration ID (payment method ID) from [PaymentMethod.id]. + /// * [onPaymentSuccess]: The callback to call when the payment is successful. + /// * [onBackPress]: The callback to call when the back button is pressed. + /// * [onPaymentDeclined]: The callback to call when the payment is declined. const MasriviView({ super.key, required this.publishableApiKey, diff --git a/lib/src/pages/payment_methods_view.dart b/lib/src/pages/payment_methods_view.dart index 5ebe82e..edc78ea 100644 --- a/lib/src/pages/payment_methods_view.dart +++ b/lib/src/pages/payment_methods_view.dart @@ -132,23 +132,29 @@ class _SelectPaymentMethodContent extends StatelessWidget { padding: const EdgeInsets.all(16), border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - Text(localizationHelper.chooseHowYouWouldLikeToPay, - style: textTheme.titleMedium), - const SizedBox(height: 10), - ...methods.asMap().entries.map((e) { - final method = e.value; - return _MethodRow( - method: method, - isSelected: pendingSelection?.id == method.id, - onTap: () => provider.setPendingSelection(method), - ); - }), - ], + child: RadioGroup( + groupValue: pendingSelection, + onChanged: (value) { + if (value != null) provider.setPendingSelection(value); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text(localizationHelper.chooseHowYouWouldLikeToPay, + style: textTheme.titleMedium), + const SizedBox(height: 10), + ...methods.asMap().entries.map((e) { + final method = e.value; + return _MethodRow( + method: method, + isSelected: pendingSelection?.id == method.id, + onTap: () => provider.setPendingSelection(method), + ); + }), + ], + ), ), ), ], @@ -488,7 +494,6 @@ class _MethodRow extends StatelessWidget { @override Widget build(BuildContext context) { - final provider = context.watch(); final localizationHelper = MoosylLocalization.of(context)!; final textTheme = Theme.of(context).textTheme; final primaryColor = Theme.of(context).colorScheme.primary; @@ -541,10 +546,7 @@ class _MethodRow extends StatelessWidget { // Radio Radio( value: method, - groupValue: provider.pendingSelection, - onChanged: (_) => onTap(), - fillColor: MaterialStateProperty.all(primaryColor), - activeColor: primaryColor, + fillColor: WidgetStateProperty.all(primaryColor), ), ], ), diff --git a/lib/src/pages/sedad_view.dart b/lib/src/pages/sedad_view.dart index 23be590..391c4ea 100644 --- a/lib/src/pages/sedad_view.dart +++ b/lib/src/pages/sedad_view.dart @@ -24,6 +24,11 @@ class SedadView extends StatelessWidget { /// Callback when the dialog is closed (e.g. to go back to payment method selection). final VoidCallback? onClose; + /// Creates a new [SedadView] instance. + /// + /// * [paymentCodeDisplay]: The code to display. + /// * [paymentRequest]: The payment request containing amount and other details. + /// * [onClose]: The callback to call when the dialog is closed. const SedadView({ super.key, required this.paymentCodeDisplay, diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index c4652a4..2471624 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -65,15 +65,15 @@ class AppButton extends StatelessWidget { if (_isDisabled) { bg = surface; - fg = onSurface.withOpacity(0.6); - side = BorderSide(color: outlineColor.withOpacity(0.5)); + fg = onSurface.withValues(alpha: 0.6); + side = BorderSide(color: outlineColor.withValues(alpha: 0.5)); } else if (style == AppButtonStyle.outline) { bg = Colors.transparent; - fg = this.textColor ?? primary; + fg = textColor ?? primary; side = border == BorderSide.none ? BorderSide(color: primary) : border; } else { // primary - bg = this.background ?? primary; + bg = background ?? primary; fg = textColor ?? onPrimary; side = border; }