- GeminiService (integrations/gemini, @Global): extractDocument (OCR fields) + faceMatch; graceful if no GEMINI_API_KEY - documents: face-match now real via Gemini; POST /admin/documents/:id/ocr extracts fields for CS auto-fill - roles: PATCH /admin/users/:id/role (admin) + POST /platform/users/role (bootstrap via x-platform-secret) - config: gemini + platform.secret; .env.example entries Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
607 lines
21 KiB
Dart
Executable File
607 lines
21 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
import 'package:sefer_driver/constant/colors.dart';
|
|
import 'package:sefer_driver/constant/style.dart';
|
|
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
|
|
import 'package:sefer_driver/controller/payment/payment_controller.dart';
|
|
import '../../../constant/box_name.dart';
|
|
import '../../../constant/links.dart';
|
|
import '../../../controller/functions/crud.dart';
|
|
import '../../../main.dart';
|
|
import '../../widgets/my_textField.dart';
|
|
|
|
class PointsCaptain extends StatefulWidget {
|
|
final Color kolor;
|
|
final String countPoint;
|
|
final double pricePoint;
|
|
|
|
const PointsCaptain({
|
|
super.key,
|
|
required this.kolor,
|
|
required this.countPoint,
|
|
required this.pricePoint,
|
|
});
|
|
|
|
@override
|
|
State<PointsCaptain> createState() => _PointsCaptainState();
|
|
}
|
|
|
|
class _PointsCaptainState extends State<PointsCaptain> {
|
|
final PaymentController paymentController = Get.put(PaymentController());
|
|
final CaptainWalletController captainWalletController =
|
|
Get.put(CaptainWalletController());
|
|
|
|
// متغيرات لحالة التحميل العامة
|
|
bool isCardLoading = false;
|
|
bool isWalletLoading = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(16),
|
|
onTap: () async {
|
|
// تم التعديل: إزالة شرط الدولة وتشغيل قائمة Paymob للجميع
|
|
// لأنها تدعم البطاقات الدولية الآن
|
|
_showPaymentBottomSheet(context);
|
|
},
|
|
child: Container(
|
|
width: Get.width * .28,
|
|
height: Get.width * .28,
|
|
margin: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
widget.kolor.withOpacity(0.4),
|
|
widget.kolor,
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: widget.kolor.withOpacity(0.3),
|
|
blurRadius: 8,
|
|
offset: const Offset(2, 4),
|
|
)
|
|
],
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: Colors.white.withOpacity(0.5), width: 1),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.stars_rounded, color: Colors.white, size: 28),
|
|
const SizedBox(height: 5),
|
|
Text(
|
|
'${widget.countPoint} ${"points".tr}',
|
|
style: AppStyle.subtitle.copyWith(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
Container(
|
|
margin: const EdgeInsets.only(top: 5),
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
'${widget.pricePoint} ${box.read(BoxName.countryCode) == 'Jordan' ? 'JOD'.tr : 'L.E'.tr}',
|
|
style: AppStyle.title.copyWith(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showPaymentBottomSheet(BuildContext context) {
|
|
// تصفير الحالات عند فتح القائمة
|
|
isCardLoading = false;
|
|
isWalletLoading = false;
|
|
|
|
// التحقق مما إذا كان المستخدم في مصر (لعرض خيار المحفظة)
|
|
bool isEgypt = box.read(BoxName.countryCode) == 'Egypt';
|
|
|
|
Get.bottomSheet(
|
|
StatefulBuilder(builder: (context, setStateSheet) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 50,
|
|
height: 5,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
'Choose Payment Method'.tr,
|
|
style: AppStyle.headTitle2,
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'${'You will pay'.tr}: ${widget.pricePoint} ${isEgypt ? 'LE'.tr : 'currency_unit'.tr}', // تحسين بسيط للعملة
|
|
style: AppStyle.title.copyWith(color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 25),
|
|
|
|
// --- إضافة تنبيه بخصوص البطاقات الدولية ---
|
|
Container(
|
|
margin: const EdgeInsets.only(bottom: 15),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primaryColor.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border:
|
|
Border.all(color: AppColor.primaryColor.withOpacity(0.3)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.public, color: AppColor.primaryColor, size: 24),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Global Payment".tr,
|
|
style: TextStyle(
|
|
color: AppColor.primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
Text(
|
|
"We accept all international Credit/Debit cards."
|
|
.tr,
|
|
style: TextStyle(
|
|
color: AppColor.primaryColor.withOpacity(0.8),
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// --- زر الدفع بالبطاقة ---
|
|
_buildPaymentButton(
|
|
title: 'Pay with Credit Card'.tr,
|
|
icon: Icons.credit_card,
|
|
color: AppColor.primaryColor,
|
|
isLoading: isCardLoading,
|
|
onTap: () async {
|
|
setStateSheet(() => isCardLoading = true);
|
|
try {
|
|
var res = await CRUD().postWallet(
|
|
link: AppLink.payWithPayMobCardDriver,
|
|
payload: _getPayload(),
|
|
);
|
|
|
|
if (res != null && res['message'] != null) {
|
|
Get.back(); // إغلاق القائمة
|
|
Get.to(() => GenericPaymentWebView(
|
|
iframeUrl: res['message'],
|
|
countPrice: widget.countPoint,
|
|
isWallet: false,
|
|
));
|
|
} else {
|
|
setStateSheet(() => isCardLoading = false);
|
|
mySnackeBarError("Failed to get payment link".tr);
|
|
}
|
|
} catch (e) {
|
|
setStateSheet(() => isCardLoading = false);
|
|
mySnackeBarError("Failed to initialize card payment".tr);
|
|
}
|
|
},
|
|
),
|
|
|
|
// عرض خيار المحفظة فقط إذا كان المستخدم في مصر
|
|
if (isEgypt) ...[
|
|
const SizedBox(height: 15),
|
|
_buildPaymentButton(
|
|
title: 'Pay with Wallet'.tr,
|
|
icon: Icons.account_balance_wallet,
|
|
color: const Color(0xffE60000),
|
|
isLoading: isWalletLoading,
|
|
onTap: () {
|
|
Get.back();
|
|
_showWalletNumberDialog();
|
|
},
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
isScrollControlled: true,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> _getPayload() {
|
|
return {
|
|
"amount": widget.pricePoint.toStringAsFixed(2),
|
|
"email": box.read(BoxName.emailDriver).toString(),
|
|
"first_name": box.read(BoxName.nameDriver).toString().split(' ')[0],
|
|
"last_name": box.read(BoxName.nameDriver).toString().split(' ').length > 1
|
|
? box.read(BoxName.nameDriver).toString().split(' ')[1]
|
|
: "Driver",
|
|
"phone_number": box.read(BoxName.phoneDriver),
|
|
};
|
|
}
|
|
|
|
Widget _buildPaymentButton({
|
|
required String title,
|
|
required IconData icon,
|
|
required Color color,
|
|
required bool isLoading,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: color,
|
|
shape:
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
elevation: 0,
|
|
),
|
|
onPressed: isLoading ? null : onTap,
|
|
child: isLoading
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white, strokeWidth: 2),
|
|
)
|
|
: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, color: Colors.white),
|
|
const SizedBox(width: 10),
|
|
Text(title,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showWalletNumberDialog() {
|
|
bool isDialogLoading = false;
|
|
|
|
Get.defaultDialog(
|
|
barrierDismissible: false,
|
|
title: 'E-Wallet Number'.tr,
|
|
contentPadding: const EdgeInsets.all(20),
|
|
content: StatefulBuilder(builder: (context, setStateDialog) {
|
|
return Column(
|
|
children: [
|
|
Form(
|
|
key: paymentController.formKey,
|
|
child: MyTextForm(
|
|
controller: paymentController.walletphoneController,
|
|
label: 'Phone Number'.tr,
|
|
hint: '01xxxxxxxxx',
|
|
type: TextInputType.phone,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColor.primaryColor,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
onPressed: isDialogLoading
|
|
? null
|
|
: () async {
|
|
if (paymentController.formKey.currentState!
|
|
.validate()) {
|
|
setStateDialog(() => isDialogLoading = true);
|
|
box.write(BoxName.phoneWallet,
|
|
paymentController.walletphoneController.text);
|
|
|
|
try {
|
|
var payload = _getPayload();
|
|
payload['phone_number'] =
|
|
paymentController.walletphoneController.text;
|
|
|
|
var res = await CRUD().postWallet(
|
|
link: AppLink.payWithWallet,
|
|
payload: payload,
|
|
);
|
|
|
|
if (res != null && res['message'] != null) {
|
|
Get.back();
|
|
Get.to(() => GenericPaymentWebView(
|
|
iframeUrl: res['message'],
|
|
countPrice: widget.countPoint,
|
|
isWallet: true,
|
|
));
|
|
} else {
|
|
setStateDialog(() => isDialogLoading = false);
|
|
mySnackeBarError("Failed to get payment link".tr);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setStateDialog(() => isDialogLoading = false);
|
|
}
|
|
mySnackeBarError(
|
|
"Failed to initialize wallet payment".tr);
|
|
}
|
|
}
|
|
},
|
|
child: isDialogLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white, strokeWidth: 2))
|
|
: Text('Confirm & Pay'.tr,
|
|
style: const TextStyle(color: Colors.white)),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (!isDialogLoading)
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: Text("Cancel".tr,
|
|
style: const TextStyle(color: Colors.grey)),
|
|
)
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// صفحة الويب (WebView) مع اللودينج الخاص بها
|
|
// -----------------------------------------------------------------------------
|
|
class GenericPaymentWebView extends StatefulWidget {
|
|
final String iframeUrl;
|
|
final String countPrice;
|
|
final bool isWallet;
|
|
|
|
const GenericPaymentWebView({
|
|
Key? key,
|
|
required this.iframeUrl,
|
|
required this.countPrice,
|
|
required this.isWallet,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<GenericPaymentWebView> createState() => _GenericPaymentWebViewState();
|
|
}
|
|
|
|
class _GenericPaymentWebViewState extends State<GenericPaymentWebView> {
|
|
late final WebViewController _controller;
|
|
final controller = Get.find<CaptainWalletController>();
|
|
bool isLoadingPage = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = WebViewController()
|
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
..setNavigationDelegate(NavigationDelegate(
|
|
onPageStarted: (_) {
|
|
if (mounted) setState(() => isLoadingPage = true);
|
|
},
|
|
onPageFinished: (url) {
|
|
if (mounted) setState(() => isLoadingPage = false);
|
|
if (url.contains("success")) {
|
|
_fetchPaymentStatus();
|
|
} else if (url.contains("failed") || url.contains("error")) {
|
|
showCustomDialog(
|
|
title: "Error".tr,
|
|
message: 'Payment Failed'.tr,
|
|
isSuccess: false,
|
|
);
|
|
}
|
|
},
|
|
onWebResourceError: (error) {
|
|
if (mounted) setState(() => isLoadingPage = false);
|
|
},
|
|
))
|
|
..loadRequest(Uri.parse(widget.iframeUrl));
|
|
}
|
|
|
|
Future<void> _fetchPaymentStatus() async {
|
|
final String userId = widget.isWallet
|
|
? '+2${box.read(BoxName.phoneWallet)}'
|
|
: box.read(BoxName.phoneDriver);
|
|
|
|
Get.dialog(const Center(child: CircularProgressIndicator()),
|
|
barrierDismissible: false);
|
|
|
|
try {
|
|
final response = await CRUD().postWallet(
|
|
link: AppLink.paymetVerifyDriver,
|
|
payload: {
|
|
'user_id': userId,
|
|
'driverID': box.read(BoxName.driverID),
|
|
'paymentMethod': 'visa-in',
|
|
},
|
|
);
|
|
|
|
Get.back(); // إغلاق لودينج التحقق
|
|
|
|
if (response != 'failure' && response != 'token_expired') {
|
|
if (response['status'] == 'success') {
|
|
final payment = response['message'];
|
|
final amount = payment['amount'].toString();
|
|
final bonus = payment['bonus'].toString();
|
|
final paymentID = payment['paymentID'].toString();
|
|
|
|
await controller.refreshCaptainWallet();
|
|
|
|
showCustomDialog(
|
|
title: "payment_success".tr,
|
|
message:
|
|
"${"transaction_id".tr}: $paymentID\n${"amount_paid".tr}: $amount EGP\n${"bonus_added".tr}: $bonus ${"points".tr}",
|
|
isSuccess: true,
|
|
shouldCloseScreen: true,
|
|
);
|
|
} else {
|
|
showCustomDialog(
|
|
title: "transaction_failed".tr,
|
|
message: response['message'].toString(),
|
|
isSuccess: false,
|
|
);
|
|
}
|
|
} else {
|
|
showCustomDialog(
|
|
title: "connection_failed".tr,
|
|
message: "Could not verify payment status".tr,
|
|
isSuccess: false,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
Get.back();
|
|
showCustomDialog(
|
|
title: "server_error".tr,
|
|
message: "server_error_message".tr,
|
|
isSuccess: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
void showCustomDialog({
|
|
required String title,
|
|
required String message,
|
|
required bool isSuccess,
|
|
bool shouldCloseScreen = false,
|
|
}) {
|
|
Get.dialog(
|
|
Dialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(15),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isSuccess
|
|
? Colors.green.withOpacity(0.1)
|
|
: Colors.red.withOpacity(0.1),
|
|
),
|
|
child: Icon(
|
|
isSuccess ? Icons.check_circle : Icons.error_outline,
|
|
color: isSuccess ? Colors.green : Colors.red,
|
|
size: 40,
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
Text(
|
|
title,
|
|
style: AppStyle.headTitle2.copyWith(
|
|
color: isSuccess ? Colors.green : Colors.red,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
message,
|
|
textAlign: TextAlign.center,
|
|
style: AppStyle.title,
|
|
),
|
|
const SizedBox(height: 25),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: isSuccess ? Colors.green : Colors.red,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
onPressed: () {
|
|
Get.back();
|
|
if (shouldCloseScreen) {
|
|
Get.back();
|
|
}
|
|
},
|
|
child: Text("OK".tr,
|
|
style: const TextStyle(color: Colors.white)),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
barrierDismissible: false,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('Secure Payment'.tr),
|
|
centerTitle: true,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Get.back(),
|
|
),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
WebViewWidget(controller: _controller),
|
|
if (isLoadingPage)
|
|
Container(
|
|
color: Colors.white,
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const CircularProgressIndicator(),
|
|
const SizedBox(height: 20),
|
|
Text("Loading Payment Page...".tr,
|
|
style: const TextStyle(
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|