إضافة الدفع باستخدام PayPal في Flutter مع Flutter PayPal Payment
إذا كنت تعمل على متجر إلكتروني أو تطبيق خدمات باستخدام Flutter وتريد إضافة الدفع بواسطة PayPal، فإن مكتبة flutter_paypal_payment توفر طريقة سهلة لفتح صفحة PayPal Checkout داخل التطبيق والتعامل مع نتيجة عملية الدفع.
تدعم المكتبة بيئة Sandbox للاختبار قبل إطلاق التطبيق، كما تسمح بإرسال تفاصيل المنتجات والأسعار والعملة وعنوان الشحن، ثم معرفة ما إذا كانت عملية الدفع نجحت أو فشلت أو قام المستخدم بإلغائها.
ما هي مكتبة flutter_paypal_payment؟
flutter_paypal_payment هي مكتبة Flutter تعمل كواجهة تسهّل دمج PayPal Checkout داخل التطبيق.
بدلًا من بناء عملية الدفع والتعامل مع صفحات PayPal من الصفر، توفر المكتبة Widget جاهزة باسم:
PaypalCheckoutView
وتقوم بتمرير بيانات الدفع إليها مثل:
- Client ID.
- Secret Key.
- وضع Sandbox أو Production.
- إجمالي العملية.
- العملة.
- المنتجات.
- عنوان الشحن.
- ملاحظة الطلب.
ثم تستطيع التعامل مع نتيجة الدفع باستخدام Callbacks جاهزة.
مميزات flutter_paypal_payment
- إضافة PayPal Checkout بسهولة إلى Flutter.
- دعم Sandbox Mode للاختبار.
- دعم بيئة Production.
- إرسال أكثر من منتج داخل المعاملة.
- تحديد العملة.
- تحديد تكلفة الشحن.
- إضافة خصومات الشحن.
- إضافة عنوان الشحن اختياريًا.
- معرفة نجاح عملية الدفع.
- التعامل مع أخطاء PayPal.
- معرفة إلغاء المستخدم للعملية.
- إمكانية تخصيص Loading Indicator.
- فتح عملية Checkout داخل التطبيق.
تثبيت flutter_paypal_payment
الإصدار المنشور حاليًا على pub.dev هو:
flutter_paypal_payment: ^1.0.8
قم بإضافته داخل:
pubspec.yaml
بهذا الشكل:
dependencies:
flutter:
sdk: flutter
flutter_paypal_payment: ^1.0.8
ثم نفذ:
flutter pub get
قد تجد أن README الرسمي للحزمة لا يزال يستخدم:
flutter_paypal_payment: ^1.0.7
لكن الإصدار المنشور حاليًا على pub.dev هو 1.0.8.
استيراد المكتبة
بعد تثبيت المكتبة قم باستيرادها:
import 'package:flutter_paypal_payment/flutter_paypal_payment.dart';
الحصول على PayPal Client ID
قبل البدء تحتاج إلى إنشاء تطبيق من خلال حساب PayPal Developer.
من لوحة PayPal Developer يمكنك إنشاء Application والحصول على:
Client ID
Client Secret
وسيكون لديك عادة Credentials منفصلة لبيئة:
Sandbox
وأخرى لبيئة:
Live
ما هو Sandbox Mode؟
أثناء تطوير التطبيق لا يجب تنفيذ عمليات دفع حقيقية.
لذلك يوفر PayPal بيئة:
Sandbox
والتي تسمح لك باختبار عملية الدفع باستخدام حسابات تجريبية وبدون خصم أموال حقيقية.
داخل المكتبة يتم تفعيلها باستخدام:
sandboxMode: true
وعند الانتقال إلى Production تصبح:
sandboxMode: false
فتح صفحة PayPal Checkout
يمكن فتح شاشة الدفع باستخدام:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return PaypalCheckoutView(
sandboxMode: true,
clientId:
'YOUR_CLIENT_ID',
secretKey:
'YOUR_SECRET_KEY',
transactions: const [],
onSuccess: (params) async {},
onError: (error) {},
onCancel: () {},
);
},
),
);
بمجرد فتح الصفحة تبدأ المكتبة عملية PayPal Checkout.
إنشاء أول عملية دفع
لنفترض أننا نريد تنفيذ عملية دفع بقيمة:
100 USD
يمكن إنشاء العملية بالشكل التالي:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
PaypalCheckoutView(
sandboxMode: true,
clientId:
'YOUR_CLIENT_ID',
secretKey:
'YOUR_SECRET_KEY',
transactions: const [
{
"amount": {
"total": "100",
"currency": "USD",
"details": {
"subtotal": "100",
"shipping": "0",
"shipping_discount": 0,
}
},
"description":
"Payment for order",
"item_list": {
"items": [
{
"name":
"Flutter Course",
"quantity": 1,
"price": "100",
"currency": "USD",
}
]
}
}
],
note:
"Thank you for your order.",
onSuccess:
(Map params) async {
debugPrint(
'Payment Success',
);
debugPrint(
params.toString(),
);
Navigator.pop(context);
},
onError: (error) {
debugPrint(
error.toString(),
);
Navigator.pop(context);
},
onCancel: () {
debugPrint(
'Payment cancelled',
);
Navigator.pop(context);
},
),
),
);
فهم transactions
الجزء الأساسي في عملية الدفع هو:
transactions
وهو عبارة عن List تحتوي على تفاصيل المعاملة.
داخلها نجد:
{
"amount": {},
"description": "",
"item_list": {}
}
تحديد إجمالي عملية الدفع
داخل:
amount
يتم تحديد إجمالي العملية:
"amount": {
"total": "100",
"currency": "USD",
}
القيمة:
total
تمثل المبلغ الإجمالي الذي سيدفعه المستخدم.
تحديد العملة
يمكن تحديد العملة باستخدام:
"currency": "USD"
ومن المهم استخدام Currency Code صحيح ومتوافق مع العملات التي يدعمها PayPal وحساب التاجر.
تفاصيل المبلغ
يمكن تقسيم المبلغ إلى:
"details": {
"subtotal": "100",
"shipping": "0",
"shipping_discount": 0,
}
حيث:
subtotal
إجمالي قيمة المنتجات قبل إضافة الشحن.
shipping
تكلفة الشحن.
shipping_discount
الخصم المطبق على تكلفة الشحن.
مثال مع تكلفة شحن
لنفترض أن قيمة المنتجات:
100 USD
وتكلفة الشحن:
10 USD
فيصبح إجمالي العملية:
"amount": {
"total": "110",
"currency": "USD",
"details": {
"subtotal": "100",
"shipping": "10",
"shipping_discount": 0,
}
}
إضافة المنتجات إلى PayPal
يمكن إرسال تفاصيل المنتجات داخل:
item_list
مثال:
"item_list": {
"items": [
{
"name":
"Flutter Course",
"quantity":
1,
"price":
"50",
"currency":
"USD",
},
{
"name":
"Dart Course",
"quantity":
1,
"price":
"50",
"currency":
"USD",
}
]
}
وهكذا سيظهر للمستخدم أن العملية تحتوي على منتجين بدل إرسال مبلغ إجمالي بدون تفاصيل.
استخدام Cart حقيقية من التطبيق
في المشاريع الحقيقية غالبًا ستكون المنتجات موجودة داخل Cart.
مثلًا:
class CartItem {
final String name;
final double price;
final int quantity;
CartItem({
required this.name,
required this.price,
required this.quantity,
});
}
ولدينا:
final cart = [
CartItem(
name: 'Product 1',
price: 25,
quantity: 2,
),
CartItem(
name: 'Product 2',
price: 50,
quantity: 1,
),
];
يمكن تحويل المنتجات إلى الشكل المطلوب بواسطة PayPal:
final paypalItems =
cart.map((item) {
return {
"name":
item.name,
"quantity":
item.quantity,
"price":
item.price.toStringAsFixed(2),
"currency":
"USD",
};
}).toList();
حساب إجمالي Cart
final subtotal =
cart.fold<double>(
0,
(total, item) {
return total +
(
item.price *
item.quantity
);
},
);
ثم:
final total =
subtotal + shipping;
ويتم إرسال القيم إلى PayPal:
"amount": {
"total":
total.toStringAsFixed(2),
"currency":
"USD",
"details": {
"subtotal":
subtotal.toStringAsFixed(2),
"shipping":
shipping.toStringAsFixed(2),
"shipping_discount":
0,
}
}
إضافة عنوان الشحن
عنوان الشحن ليس إلزاميًا في المثال الرسمي، ولكن يمكن إضافته داخل:
item_list
بهذا الشكل:
"shipping_address": {
"recipient_name":
"Ahmed",
"line1":
"King Fahd Road",
"line2":
"",
"city":
"Riyadh",
"country_code":
"SA",
"postal_code":
"12345",
"phone":
"+966500000000",
"state":
"Riyadh",
}
ليصبح:
"item_list": {
"items": [
...
],
"shipping_address": {
"recipient_name":
"Ahmed",
"line1":
"King Fahd Road",
"city":
"Riyadh",
"country_code":
"SA",
"postal_code":
"12345",
"phone":
"+966500000000",
"state":
"Riyadh",
}
}
إضافة وصف للعملية
يمكن استخدام:
"description":
"Payment for order #123"
لوصف سبب الدفع.
ويفضل تضمين Reference واضح للعملية مثل:
Order #123
حتى تستطيع ربط عملية PayPal بالطلب الموجود داخل قاعدة البيانات لديك.
إضافة Note
توفر PaypalCheckoutView أيضًا:
note
مثال:
note:
"Contact us for any questions about your order."
معرفة نجاح عملية الدفع
عند نجاح عملية الدفع يتم استدعاء:
onSuccess
مثال:
onSuccess:
(Map params) async {
debugPrint(
'Payment Success',
);
debugPrint(
params.toString(),
);
}
الـ params تحتوي على البيانات التي ترجع من عملية الدفع.
ماذا أفعل بعد onSuccess؟
لا يفضل الاكتفاء بعرض رسالة:
Payment Success
فقط.
في التطبيق الحقيقي يمكنك بعد نجاح العملية:
- تحديث حالة الطلب.
- حفظ Transaction ID.
- إظهار صفحة نجاح الدفع.
- إفراغ سلة المشتريات.
- إرسال فاتورة إلى العميل.
- إرسال Notification.
لكن في تطبيقات Production من الأفضل ألا يكون اعتمادك النهائي على callback داخل الهاتف وحده، بل يجب التحقق من حالة الدفع من خلال Backend موثوق.
التعامل مع أخطاء الدفع
عند حدوث مشكلة يتم استدعاء:
onError
مثال:
onError: (error) {
debugPrint(
'PayPal Error: $error',
);
Navigator.pop(context);
}
ويفضل عرض رسالة واضحة للمستخدم بدل عرض تفاصيل الخطأ التقنية مباشرة.
التعامل مع إلغاء المستخدم
إذا قام المستخدم بإلغاء Checkout يتم استدعاء:
onCancel
مثال:
onCancel: () {
debugPrint(
'Payment cancelled',
);
Navigator.pop(context);
}
يمكن عندها إعادة المستخدم إلى Checkout داخل تطبيقك بدون إنشاء الطلب كطلب مدفوع.
تخصيص Loading Indicator
توفر PaypalCheckoutView الخاصية:
loadingIndicator
وبالتالي تستطيع عرض Loader خاص بتصميم تطبيقك.
PaypalCheckoutView(
...
loadingIndicator:
const Center(
child:
CircularProgressIndicator(),
),
)
مثال Function كاملة لفتح PayPal
Future<void> payWithPaypal(
BuildContext context,
) async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
PaypalCheckoutView(
sandboxMode: true,
clientId:
'YOUR_CLIENT_ID',
secretKey:
'YOUR_SECRET_KEY',
transactions: const [
{
"amount": {
"total":
"115.00",
"currency":
"USD",
"details": {
"subtotal":
"100.00",
"shipping":
"15.00",
"shipping_discount":
0,
}
},
"description":
"Order #1001",
"item_list": {
"items": [
{
"name":
"Product 1",
"quantity":
2,
"price":
"25.00",
"currency":
"USD",
},
{
"name":
"Product 2",
"quantity":
1,
"price":
"50.00",
"currency":
"USD",
}
]
}
}
],
note:
"Thank you for your purchase.",
onSuccess:
(Map params) async {
debugPrint(
params.toString(),
);
Navigator.pop(context);
},
onError: (error) {
debugPrint(
error.toString(),
);
Navigator.pop(context);
},
onCancel: () {
Navigator.pop(context);
},
),
),
);
}
⚠️ تحذير مهم بخصوص Client Secret
هذه واحدة من أهم النقاط التي يجب معرفتها قبل استخدام المكتبة في تطبيق حقيقي.
المثال الرسمي للمكتبة يطلب منك تمرير:
clientId
و:
secretKey
مباشرة إلى:
PaypalCheckoutView
وهذا قد يكون مناسبًا أثناء التجربة باستخدام Sandbox، لكنه ليس التصميم الأمني المناسب لتطبيق Production.
توصي PayPal بشكل واضح بعدم وضع:
Client Secret
أو API Credentials داخل Client Application.
وتطبيق Flutter يعتبر Client Application ويمكن استخراج البيانات المضمنة داخله حتى لو استخدمت Obfuscation.
لا تفعل هذا في Production
const paypalSecret =
'LIVE_PAYPAL_SECRET';
ولا تعتبر:
--obfuscate
أو:
flutter_secure_storage
حلًا لمشكلة وضع PayPal Client Secret داخل التطبيق.
الـ Secret الخاص بالتاجر لا يجب أن يصل إلى تطبيق العميل من الأساس.
البنية الأفضل للدفع في Production
الأفضل أن تكون عملية الدفع بالشكل التالي:
Flutter Application
↓
Your Backend API
↓
PayPal API
↓
Create Order
↓
Flutter Checkout
↓
User Approves
↓
Backend
↓
Capture / Verify Payment
↓
Update Order
ويبقى:
PAYPAL_CLIENT_SECRET
داخل Backend فقط.
لماذا وجود Backend مهم؟
بالإضافة إلى حماية PayPal Secret، وجود Backend يمنع المستخدم من التلاعب بالقيمة.
مثلًا لا يجب أن يستطيع التطبيق إرسال:
Product Price = 1 USD
بينما السعر الحقيقي في قاعدة البيانات هو:
Product Price = 100 USD
لذلك يجب أن يقوم Backend بحساب المبلغ الحقيقي بالاعتماد على المنتجات الموجودة في قاعدة البيانات.
لا تثق بالسعر القادم من Flutter
في تطبيق Production لا تجعل Flutter هو المصدر النهائي لهذه القيمة:
totalAmount
بل أرسل مثلًا:
productId
quantity
orderId
إلى Backend.
ثم يقوم الخادم بقراءة الأسعار الحقيقية وحساب الإجمالي وإنشاء عملية PayPal.
لا تعتمد فقط على onSuccess
وجود:
onSuccess
مفيد جدًا لتحديث واجهة التطبيق، لكنه لا يجب أن يكون المصدر الوحيد لتحديد أن الطلب أصبح:
PAID
في نظام Production.
الأفضل التحقق من العملية من PayPal على Backend وربطها برقم الطلب الخاص بك.
احفظ PayPal Transaction ID
بعد نجاح الدفع من الأفضل الاحتفاظ بالمعرف الخاص بالعملية داخل قاعدة البيانات.
مثلًا:
orderId
paypalTransactionId
paymentStatus
paymentAmount
paymentCurrency
createdAt
هذه البيانات ستكون مهمة جدًا إذا احتجت لاحقًا إلى:
- مراجعة عملية دفع.
- تنفيذ Refund.
- حل مشكلة مع العميل.
- مطابقة الطلبات مع حساب PayPal.
اختبار PayPal قبل Production
ابدأ دائمًا باستخدام:
sandboxMode: true
واختبر الحالات التالية:
- عملية دفع ناجحة.
- إلغاء المستخدم للدفع.
- بيانات حساب غير صحيحة.
- انقطاع الإنترنت.
- إغلاق التطبيق أثناء Checkout.
- الضغط المتكرر على زر الدفع.
- فشل Backend بعد نجاح PayPal.
- تكرار Callback.
منع الضغط المتكرر على زر الدفع
يفضل منع المستخدم من فتح أكثر من Checkout في نفس الوقت.
bool isPaymentLoading = false;
ثم:
Future<void> startPayment() async {
if (isPaymentLoading) {
return;
}
setState(() {
isPaymentLoading = true;
});
try {
await payWithPaypal(
context,
);
} finally {
if (mounted) {
setState(() {
isPaymentLoading = false;
});
}
}
}
عرض زر PayPal
يمكن إنشاء زر بسيط:
ElevatedButton(
onPressed:
startPayment,
child:
const Text(
'Pay with PayPal',
),
)
أو إنشاء تصميم خاص يناسب Checkout الخاص بتطبيقك.
استخدام PayPal مع Riverpod
إذا كنت تستخدم Riverpod، فمن الأفضل أن تكون بيانات Cart والطلب موجودة داخل Provider، بينما تظل عملية فتح Checkout في طبقة UI.
مثلًا:
final order =
ref.read(
cartProvider,
);
final total =
order.total;
ثم تقوم بإنشاء transaction باستخدام بيانات الطلب.
لكن مرة أخرى، في Production يجب أن يتم التأكد من الأسعار والإجمالي على Backend وليس بالاعتماد على State الموجودة في التطبيق فقط.
كيف تعمل المكتبة؟
تعتمد flutter_paypal_payment على مجموعة من المكتبات من بينها:
flutter_inappwebview
dio
وبالتالي يتم التعامل مع عملية PayPal Checkout من خلال Web Flow داخل التطبيق مع تنفيذ طلبات الشبكة اللازمة للعملية.
هل flutter_paypal_payment تستخدم PayPal Native SDK؟
لا تعتبر هذه المكتبة Wrapper مباشرًا حول PayPal Native Android وiOS SDK بالشكل التقليدي.
فهي تعتمد على WebView لتنفيذ Checkout داخل التطبيق.
وهذه نقطة يجب أخذها في الاعتبار إذا كان مشروعك يشترط Native PayPal SDK أو Server-first architecture.
هل يمكن استخدامها بدون Backend؟
تقنيًا توضح أمثلة المكتبة إمكانية تمرير Client ID وSecret Key مباشرة داخل التطبيق وتنفيذ العملية.
لكن هذا لا يعني أن هذا الأسلوب مناسب لبيئة Production.
إذا كان المشروع مجرد:
- Prototype.
- Demo.
- مشروع تعليمي.
- Sandbox Test.
فيمكن استخدام المثال لفهم عملية PayPal Checkout.
أما في مشروع تجاري حقيقي فمن الأفضل استخدام بنية تحافظ على Secret داخل Backend.
Sandbox و Production
أثناء الاختبار:
sandboxMode: true
وتستخدم:
Sandbox Client ID
Sandbox Credentials
أما عندما يصبح التطبيق جاهزًا للنشر:
sandboxMode: false
وتستخدم إعدادات Live المناسبة، لكن مع تطبيق معمارية آمنة وعدم كشف Client Secret في التطبيق.
هل الحزمة مناسبة لمشروع جديد؟
الحزمة تقدم API بسيطة جدًا وتوفر طريقة سريعة لتجربة PayPal Checkout داخل Flutter.
لكن هناك نقطتين مهمتين قبل اعتمادها في مشروع جديد:
- الإصدار الحالي 1.0.8 منشور منذ فترة طويلة نسبيًا.
- الـ API الحالية للحزمة تتطلب تمرير Secret Key إلى Widget داخل التطبيق.
لذلك إذا كان التطبيق تجاريًا وسيتم تشغيل PayPal في Production، فمن الأفضل مراجعة أحدث حلول PayPal الرسمية والحزم التي تعتمد على Server-side Orders قبل اتخاذ القرار النهائي.
متى تكون flutter_paypal_payment مناسبة؟
يمكن أن تكون مناسبة في حالات مثل:
- تعلم كيفية دمج PayPal مع Flutter.
- إنشاء Prototype سريع.
- تجربة PayPal Sandbox.
- إنشاء Proof of Concept.
- تطبيق داخلي للاختبار.
متى يجب التفكير في حل آخر؟
إذا كان مشروعك يحتوي على:
- دفعات مالية حقيقية.
- عدد كبير من المستخدمين.
- منتجات وأسعار حساسة.
- Marketplace.
- Subscriptions.
- Refunds.
- Webhooks.
- متطلبات أمنية قوية.
فمن الأفضل بناء PayPal Integration تعتمد على Backend بشكل واضح.
الخلاصة
مكتبة flutter_paypal_payment توفر طريقة بسيطة وسريعة لإضافة PayPal Checkout داخل تطبيق Flutter.
يمكن من خلالها تحديد قيمة العملية والعملة والمنتجات والشحن، ثم التعامل بسهولة مع حالات نجاح الدفع والخطأ والإلغاء.
كما أن وجود Sandbox Mode يجعل المكتبة مناسبة جدًا لتجربة PayPal وتعلم طريقة عمل Checkout قبل الانتقال إلى الدفع الحقيقي.
لكن أهم نقطة يجب الانتباه إليها هي أن مثال المكتبة يعتمد على تمرير PayPal Secret Key مباشرة إلى تطبيق Flutter، وهذا الأسلوب لا ينبغي استخدامه مع Client Secret حقيقي في Production.
في التطبيقات التجارية يفضل أن يحتفظ Backend بالمفاتيح السرية، ويتولى إنشاء والتحقق من عمليات الدفع، بينما يكون تطبيق Flutter مسؤولًا عن واجهة المستخدم وتجربة Checkout فقط.
مكتبة flutter_paypal_payment على pub.dev
PayPal Developer
مزيد من المقالات : تعلم Zustand في React.js: إدارة الحالة بسهولة مع تطبيق Todo عملي






