Update: 2026-07-03 01:48:00

This commit is contained in:
Hamza-Ayed
2026-07-03 01:48:00 +03:00
parent 8f59189832
commit 5ce6c00905
13 changed files with 1119 additions and 12 deletions
+1
View File
@@ -111,4 +111,5 @@ class BoxName {
static const String parentTripSelected = 'parentTripSelected';
static const String styleVersion = 'styleVersion';
static const String isDestinationMatch = 'isDestinationMatch'; // 🆕 AI Destination Matching
static const String isPrime = 'isPrime'; // 👑 Siro Prime subscription status
}
+4
View File
@@ -199,6 +199,10 @@ class AppLink {
static String get deletePassengersWallet => "$wallet/delete.php";
static String get updatePassengersWallet => "$wallet/update.php";
//=======================Siro Prime===================
static String get initiatePrime => "$server/api/payments/initiate_prime.php";
static String get getPrimeStatus => "$server/api/payments/get_prime_status.php";
static String get getWalletByDriver => "$walletDriver/getWalletByDriver.php";
static String get getDriversWallet => "$walletDriver/get.php";
static String get addDriversWalletPoints => "$walletDriver/add.php";
@@ -507,7 +507,7 @@ class RideLifecycleController extends GetxController {
currentRideState.value = RideState.noRide;
isSearchingWindow = false;
update();
_showIncreaseFeeDialog();
_showAiNegotiatorDialog();
break;
}
@@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/main.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/views/widgets/error_snakbar.dart';
class PrimeController extends GetxController {
// ── State ──────────────────────────────────────────────────
bool isLoading = false;
bool isPrime = false;
String? expireAt;
double currentWalletBalance = 0.0;
// ── Pricing map (mirrors backend) ─────────────────────────
static Map<String, Map<String, dynamic>> pricingMap = {
'Jordan': {'amount': 1.50, 'currency': 'JOD', 'symbol': 'JOD'},
'Egypt': {'amount': 150.00, 'currency': 'EGP', 'symbol': 'EGP'},
'Syria': {'amount': 50000.0, 'currency': 'SYP', 'symbol': 'SYP'},
};
String get country => box.read(BoxName.countryCode) ?? 'Jordan';
double get primePrice {
return (pricingMap[country]?['amount'] ?? 1.50) as double;
}
String get primeCurrency {
return pricingMap[country]?['symbol'] ?? 'JOD';
}
@override
void onInit() {
super.onInit();
fetchPrimeStatus();
}
// ── Fetch Prime Status ────────────────────────────────────
Future<void> fetchPrimeStatus() async {
isLoading = true;
update();
try {
final res = await CRUD().post(link: AppLink.getPrimeStatus, payload: {});
if (res != 'failure' && res['status'] == 'success') {
isPrime = res['data']['is_prime'] == true;
expireAt = res['data']['expire_at'];
}
} catch (e) {
// Non-critical — default is false
}
isLoading = false;
update();
}
// ── Subscribe to Prime ────────────────────────────────────
Future<void> subscribePrime(BuildContext context) async {
isLoading = true;
update();
try {
final res = await CRUD().post(
link: AppLink.initiatePrime,
payload: {'country': country},
);
if (res == 'failure') {
mySnackbarError('Connection error. Please try again.'.tr);
isLoading = false;
update();
return;
}
final status = res['status'] ?? '';
if (status == 'success') {
// ✅ تم الاشتراك بنجاح
isPrime = true;
expireAt = res['data']['expire_at'];
box.write(BoxName.isPrime, true);
update();
_showSuccessDialog(context);
} else if (status == 'insufficient_balance') {
// 💳 رصيد غير كافٍ
final required = res['required_amount'] ?? primePrice;
final current = res['current_balance'] ?? 0.0;
_showInsufficientBalanceDialog(context, current: current.toDouble(), required: required.toDouble());
} else {
mySnackbarError((res['message'] ?? 'Subscription failed.').toString().tr);
}
} catch (e) {
mySnackbarError('An error occurred. Please try again.'.tr);
}
isLoading = false;
update();
}
// ── Dialogs ───────────────────────────────────────────────
void _showSuccessDialog(BuildContext context) {
Get.defaultDialog(
title: '👑 Siro Prime!',
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.verified, color: Colors.amber, size: 48),
const SizedBox(height: 12),
Text(
'You are now a Siro Prime member! Enjoy no surge pricing for 30 days.'.tr,
textAlign: TextAlign.center,
),
if (expireAt != null) ...[
const SizedBox(height: 8),
Text(
'${'Valid until'.tr}: $expireAt',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
]
],
),
textConfirm: 'Great!'.tr,
onConfirm: () => Get.back(),
);
}
void _showInsufficientBalanceDialog(BuildContext context, {required double current, required double required}) {
Get.defaultDialog(
title: 'Insufficient Balance'.tr,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.account_balance_wallet_outlined, color: Colors.orange, size: 48),
const SizedBox(height: 12),
Text(
'${'Your current balance'.tr}: ${current.toStringAsFixed(2)} $primeCurrency',
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'${'Required'.tr}: ${required.toStringAsFixed(2)} $primeCurrency',
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Please top up your wallet to subscribe to Siro Prime.'.tr,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey),
),
],
),
textConfirm: 'Top Up Wallet'.tr,
textCancel: 'Cancel'.tr,
onConfirm: () {
Get.back(); // close dialog
Get.back(); // close Prime page → return to wallet page to top up
},
);
}
}
@@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/views/home/my_wallet/payment_history_passenger_page.dart';
import 'package:siro_rider/views/home/my_wallet/siro_prime_page.dart';
import 'dart:ui'; // لاستخدام تأثيرات متقدمة
import '../../../constant/box_name.dart';
@@ -12,6 +13,7 @@ import '../../../constant/style.dart';
import '../../../controller/functions/toast.dart';
import '../../../controller/home/payment/credit_card_controller.dart';
import '../../../controller/payment/payment_controller.dart';
import '../../../controller/payment/prime_controller.dart';
import '../../../main.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_scafold.dart';
@@ -26,6 +28,7 @@ class PassengerWallet extends StatelessWidget {
// نفس منطق استدعاء الكنترولرز
Get.put(PaymentController());
Get.put(CreditCardController());
Get.put(PrimeController());
return MyScafolld(
title: 'My Balance'.tr,
@@ -75,6 +78,10 @@ class PassengerWallet extends StatelessWidget {
onTap: () => _showWalletPhoneDialog(context,
Get.find<PaymentController>()), // نفس دالتك القديمة
),
// 👑 Siro Prime Banner
const SizedBox(height: 8),
_buildPrimeBanner(),
],
),
),
@@ -258,6 +265,85 @@ class PassengerWallet extends StatelessWidget {
barrierDismissible: false,
);
}
// 👑 Siro Prime Banner Widget
Widget _buildPrimeBanner() {
return GetBuilder<PrimeController>(
builder: (ctrl) {
final bool isPrime = ctrl.isPrime;
return GestureDetector(
onTap: () => Get.to(
() => const SiroPrimePage(),
transition: Transition.rightToLeftWithFade,
),
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: isPrime
? [const Color(0xFF7C3AED), const Color(0xFFDB2777)]
: [const Color(0xFF1E1E2E), const Color(0xFF2A2A3E)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: (isPrime ? const Color(0xFF7C3AED) : Colors.black)
.withOpacity(0.35),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
shape: BoxShape.circle,
),
child: const Text('👑', style: TextStyle(fontSize: 24)),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isPrime ? 'Siro Prime Active'.tr : 'Siro Prime'.tr,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
isPrime
? '${'Valid until'.tr}: ${ctrl.expireAt?.substring(0, 10) ?? ''}'
: 'No surge pricing. Priority rides. Tap to learn more.'.tr,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
Icon(
isPrime ? Icons.verified : Icons.chevron_right,
color: Colors.white.withOpacity(0.8),
),
],
),
),
);
},
);
}
}
// الكلاس القديم CardSiroWallet لم نعد بحاجة إليه لأنه تم دمجه وتطويره
@@ -0,0 +1,482 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/controller/payment/prime_controller.dart';
import 'package:siro_rider/main.dart';
import 'dart:math' as math;
class SiroPrimePage extends StatefulWidget {
const SiroPrimePage({super.key});
@override
State<SiroPrimePage> createState() => _SiroPrimePageState();
}
class _SiroPrimePageState extends State<SiroPrimePage>
with TickerProviderStateMixin {
late AnimationController _shimmerController;
late AnimationController _pulseController;
late Animation<double> _pulseAnimation;
final PrimeController ctrl = Get.find<PrimeController>();
@override
void initState() {
super.initState();
_shimmerController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat(reverse: true);
_pulseAnimation = Tween<double>(begin: 0.95, end: 1.05).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_shimmerController.dispose();
_pulseController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D0D1A),
body: CustomScrollView(
slivers: [
// ─── Animated Header ────────────────────────────────────
SliverAppBar(
expandedHeight: 280,
backgroundColor: Colors.transparent,
elevation: 0,
pinned: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
onPressed: () => Get.back(),
),
flexibleSpace: FlexibleSpaceBar(
background: _buildHeader(),
),
),
// ─── Content ─────────────────────────────────────────────
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status Card
_buildStatusCard(),
const SizedBox(height: 28),
// Benefits Title
Text(
'What you get with Prime'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Benefits List
_buildBenefit(
icon: '🚫',
title: 'No Surge Pricing'.tr,
subtitle: 'Never pay extra during peak hours. Your price stays fixed regardless of demand.'.tr,
gradient: [const Color(0xFF7C3AED), const Color(0xFF4F46E5)],
),
_buildBenefit(
icon: '⚡',
title: 'Priority Matching'.tr,
subtitle: 'Your ride request reaches drivers who have destinations near you first.'.tr,
gradient: [const Color(0xFFDB2777), const Color(0xFF9333EA)],
),
_buildBenefit(
icon: '👑',
title: 'Prime Badge'.tr,
subtitle: 'Drivers can see your Prime status, making them more likely to accept your request quickly.'.tr,
gradient: [const Color(0xFFD97706), const Color(0xFFEF4444)],
),
_buildBenefit(
icon: '💰',
title: 'Save Every Ride'.tr,
subtitle: 'During rush hours, surge pricing can add 50–200% to your fare. Prime protects you completely.'.tr,
gradient: [const Color(0xFF059669), const Color(0xFF0891B2)],
),
const SizedBox(height: 32),
// Price Card
_buildPriceCard(),
const SizedBox(height: 16),
// Subscribe Button
_buildSubscribeButton(),
const SizedBox(height: 24),
// Fine Print
Text(
'• Subscription renews automatically every 30 days\n• Cancel anytime from your wallet page\n• Amount is deducted from your in-app wallet'.tr,
style: TextStyle(
color: Colors.white.withOpacity(0.4),
fontSize: 12,
height: 1.8,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
],
),
),
),
],
),
);
}
// ─── Header ──────────────────────────────────────────────────
Widget _buildHeader() {
return AnimatedBuilder(
animation: _shimmerController,
builder: (context, child) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A0A2E), Color(0xFF0D0D1A)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
alignment: Alignment.center,
children: [
// Background particles
..._buildParticles(),
// Crown + Title
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 50),
ScaleTransition(
scale: _pulseAnimation,
child: Container(
width: 90,
height: 90,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [Color(0xFF7C3AED), Color(0xFFDB2777)],
),
boxShadow: [
BoxShadow(
color: const Color(0xFF7C3AED).withOpacity(0.6),
blurRadius: 30,
spreadRadius: 5,
),
],
),
child: const Center(
child: Text('👑', style: TextStyle(fontSize: 44)),
),
),
),
const SizedBox(height: 16),
ShaderMask(
shaderCallback: (bounds) => const LinearGradient(
colors: [Color(0xFFE879F9), Color(0xFF818CF8), Color(0xFFF59E0B)],
).createShader(bounds),
child: const Text(
'SIRO PRIME',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 4,
),
),
),
const SizedBox(height: 8),
Text(
'The Premium Ride Experience'.tr,
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 14,
),
),
],
),
],
),
);
},
);
}
List<Widget> _buildParticles() {
return List.generate(8, (i) {
final angle = (i / 8) * 2 * math.pi;
final radius = 120.0 + (i % 3) * 30;
final size = 3.0 + (i % 4).toDouble();
final opacity =
(0.1 + (_shimmerController.value + i / 8) % 1.0 * 0.5).clamp(0.0, 0.6);
return Positioned(
left: MediaQuery.of(context).size.width / 2 + math.cos(angle) * radius - 5,
top: 140 + math.sin(angle) * 80,
child: Opacity(
opacity: opacity,
child: Container(
width: size,
height: size,
decoration: const BoxDecoration(
color: Color(0xFFE879F9),
shape: BoxShape.circle,
),
),
),
);
});
}
// ─── Status Card ──────────────────────────────────────────────
Widget _buildStatusCard() {
return GetBuilder<PrimeController>(
builder: (c) {
if (!c.isPrime) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: const LinearGradient(
colors: [Color(0xFF7C3AED), Color(0xFFDB2777)],
),
),
child: Row(
children: [
const Icon(Icons.verified, color: Colors.white, size: 28),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('You are a Prime member!'.tr,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
if (c.expireAt != null)
Text(
'${'Active until'.tr}: ${c.expireAt!.substring(0, 10)}',
style: TextStyle(
color: Colors.white.withOpacity(0.8), fontSize: 12),
),
],
),
],
),
);
},
);
}
// ─── Benefit Card ────────────────────────────────────────────
Widget _buildBenefit({
required String icon,
required String title,
required String subtitle,
required List<Color> gradient,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.white.withOpacity(0.05),
border: Border.all(color: Colors.white.withOpacity(0.08)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(colors: gradient),
),
child: Center(
child: Text(icon, style: const TextStyle(fontSize: 22)),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 15)),
const SizedBox(height: 4),
Text(subtitle,
style: TextStyle(
color: Colors.white.withOpacity(0.55), fontSize: 13, height: 1.5)),
],
),
),
],
),
);
}
// ─── Price Card ───────────────────────────────────────────────
Widget _buildPriceCard() {
final country = box.read(BoxName.countryCode) ?? 'Jordan';
final pricing = {
'Jordan': {'price': '3', 'currency': 'JOD'},
'Egypt': {'price': '200', 'currency': 'EGP'},
'Syria': {'price': '500', 'currency': 'SYP'},
};
final p = pricing[country] ?? pricing['Jordan']!;
return Container(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF7C3AED).withOpacity(0.4), width: 1.5),
color: const Color(0xFF7C3AED).withOpacity(0.08),
),
child: Column(
children: [
Text('Monthly Subscription'.tr,
style: TextStyle(color: Colors.white.withOpacity(0.6), fontSize: 13)),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
p['price']!,
style: const TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.w900,
),
),
const SizedBox(width: 6),
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
p['currency']!,
style: const TextStyle(
color: Color(0xFFE879F9),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
Text(
'≈ \$4 USD / ${'month'.tr}',
style: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 12),
),
],
),
);
}
// ─── Subscribe Button ─────────────────────────────────────────
Widget _buildSubscribeButton() {
return GetBuilder<PrimeController>(
builder: (c) {
if (c.isPrime) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.green.withOpacity(0.15),
border: Border.all(color: Colors.green.withOpacity(0.4)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Text('Already subscribed'.tr,
style: const TextStyle(
color: Colors.green, fontWeight: FontWeight.bold)),
],
),
);
}
return GestureDetector(
onTap: c.isLoading ? null : () => c.subscribePrime(context),
child: AnimatedBuilder(
animation: _shimmerController,
builder: (context, child) {
return Container(
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: const [Color(0xFF7C3AED), Color(0xFFDB2777), Color(0xFFD97706)],
stops: [
0,
(_shimmerController.value + 0.5) % 1.0,
1,
],
),
boxShadow: [
BoxShadow(
color: const Color(0xFF7C3AED).withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Center(
child: c.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('👑', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Text(
'Subscribe Now'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
);
},
),
);
},
);
}
}