feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق
الباك إند: - تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً، وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً. - OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع. - N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر. N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)، ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird. فلاتر: - docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام). - السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب. - طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط. - الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3ab74a13a8
commit
de4bbd00ac
@@ -1,606 +0,0 @@
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user