Files
tripz-llc/apps/driver/lib/views/home/my_wallet/points_captain.dart
T
HamzaandClaude Opus 4.8 6234e66471 feat: Gemini AI (doc OCR + face match) + role-assignment endpoints
- 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>
2026-07-16 20:01:26 +03:00

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)),
],
),
),
),
],
),
);
}
}