diff --git a/backend/.env.example b/backend/.env.example index 66f57fa3..888f1b7f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -96,6 +96,23 @@ PAYMENT_GATEWAY_URL=https://api.paymentprovider.com PAYMENT_GATEWAY_KEY= PAYMENT_GATEWAY_SECRET= PAYMENT_WEBHOOK_SECRET= +# Internal key used for server-to-server calls (Siro Backend → Wallet Server) +PAYMENT_KEY= + +# ============================================================================= +# Wallet Servers — Multi-Country (انطلق / Wallet Intaliq) +# ============================================================================= +# Jordan wallet server (walletintaleq.intaleq.xyz) +WALLET_SERVER_JORDAN=https://walletintaleq.intaleq.xyz + +# Egypt wallet server +WALLET_SERVER_EGYPT=https://wallet-egypt.siromove.com + +# Syria wallet server +WALLET_SERVER_SYRIA=https://wallet-syria.siromove.com + +# Shared S2S secret key (must match wallet server's X-S2S-Api-Key config) +S2S_SHARED_KEY= # ============================================================================= # Siro Commissions per Country diff --git a/backend/api/payments/get_prime_status.php b/backend/api/payments/get_prime_status.php new file mode 100644 index 00000000..d8f787b2 --- /dev/null +++ b/backend/api/payments/get_prime_status.php @@ -0,0 +1,66 @@ +get("prime:passenger:{$passengerId}"); + if ($cached) { + $data = json_decode($cached, true); + if (isset($data['is_prime']) && $data['is_prime'] == 1) { + $expTime = strtotime($data['expire_at'] ?? '0'); + if ($expTime > time()) { + $isPrime = true; + $expireAt = $data['expire_at']; + } + } + } + } catch (Exception $e) {} +} + +// 2. إذا ما وُجد في Redis، ارجع للـ DB +if (!$isPrime) { + try { + $stmt = $con->prepare("SELECT is_prime, expire_at FROM passenger_prime_subscriptions WHERE passenger_id = :pid LIMIT 1"); + $stmt->execute([':pid' => $passengerId]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row && $row['is_prime'] == 1 && strtotime($row['expire_at']) > time()) { + $isPrime = true; + $expireAt = $row['expire_at']; + + // تحديث Redis للمرات القادمة + if (isset($redis) && $redis !== null) { + try { + $redis->setex("prime:passenger:{$passengerId}", 3600, json_encode([ + 'is_prime' => 1, + 'expire_at' => $expireAt + ])); + } catch (Exception $e) {} + } + } + } catch (PDOException $e) { + error_log("[Prime Status] DB Error: " . $e->getMessage()); + } +} + +jsonSuccess([ + 'is_prime' => $isPrime, + 'expire_at' => $expireAt, +], "Prime status fetched"); +?> diff --git a/backend/api/payments/initiate_prime.php b/backend/api/payments/initiate_prime.php new file mode 100644 index 00000000..44e67bb5 --- /dev/null +++ b/backend/api/payments/initiate_prime.php @@ -0,0 +1,201 @@ + ['amount' => 3.00, 'currency' => 'JOD'], // ~4 USD/month + 'Egypt' => ['amount' => 200.00,'currency' => 'EGP'], // ~4 USD/month + 'Syria' => ['amount' => 500.00,'currency' => 'SYP'], // ~4 USD/month (New Syrian Pound) +]; + +$amount = $pricingMap[$country]['amount'] ?? 3.00; +$currency = $pricingMap[$country]['currency'] ?? 'JOD'; + +// ── 3. سيرفر المحفظة حسب الدولة ────────────────────────────── +$walletServer = "https://walletintaleq.intaleq.xyz"; // Default +if (strtolower($country) === 'jordan') { + $walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz"; +} elseif (strtolower($country) === 'egypt') { + $walletServer = getenv('WALLET_SERVER_EGYPT') ?: "https://wallet-egypt.siromove.com"; +} elseif (strtolower($country) === 'syria') { + $walletServer = getenv('WALLET_SERVER_SYRIA') ?: "https://wallet-syria.siromove.com"; +} + +$s2sKey = getenv('S2S_SHARED_KEY'); +if (empty($s2sKey)) { + error_log("[Prime] CRITICAL: S2S_SHARED_KEY not set"); + jsonError("Server configuration error"); + exit; +} + +// ── 4. التحقق من رصيد الراكب في سيرفر المحفظة ──────────────── +$balanceUrl = "$walletServer/v2/main/ride/passengerWallet/getWalletByPassenger.php"; + +$chBalance = curl_init($balanceUrl); +curl_setopt_array($chBalance, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query(['passenger_id' => $passengerId]), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/x-www-form-urlencoded', + 'X-S2S-Api-Key: ' . $s2sKey + ] +]); + +$balanceRaw = curl_exec($chBalance); +$balanceCode = curl_getinfo($chBalance, CURLINFO_HTTP_CODE); +$balanceErr = curl_error($chBalance); +curl_close($chBalance); + +if ($balanceErr || $balanceCode !== 200) { + error_log("[Prime] Wallet balance fetch failed: HTTP $balanceCode | err: $balanceErr"); + jsonError("Unable to verify wallet balance. Please try again."); + exit; +} + +$balanceData = json_decode($balanceRaw, true); +$walletBalance = (float)($balanceData['message'][0]['total'] ?? $balanceData['total'] ?? -1); + +if ($walletBalance < 0) { + error_log("[Prime] Unexpected wallet response: $balanceRaw"); + jsonError("Unable to read wallet balance."); + exit; +} + +// ── 5. هل الرصيد كافٍ؟ ──────────────────────────────────────── +if ($walletBalance < $amount) { + // رصيد غير كافٍ — أخبر التطبيق ليوجّه المستخدم للشحن + echo json_encode([ + 'status' => 'insufficient_balance', + 'current_balance' => $walletBalance, + 'required_amount' => $amount, + 'currency' => $currency, + 'message' => 'Your wallet balance is insufficient. Please top up your wallet to subscribe to Siro Prime.' + ]); + exit; +} + +// ── 6. الرصيد كافٍ → بدء عملية الاشتراك ───────────────────── +try { + $con->beginTransaction(); + + // 6a. تسجيل الحركة في قاعدة بيانات سيرو (بادئها paid مباشرةً) + $transactionRef = "PRIME-" . time() . "-" . rand(1000, 9999); + $stmtTx = $con->prepare(" + INSERT INTO prime_payment_transactions (transaction_ref, passenger_id, amount, currency, status) + VALUES (:ref, :pid, :amt, :curr, 'paid') + "); + $stmtTx->execute([ + ':ref' => $transactionRef, + ':pid' => $passengerId, + ':amt' => $amount, + ':curr' => $currency + ]); + + // 6b. تفعيل أو تجديد اشتراك Prime (30 يوماً) + $expireAt = date('Y-m-d H:i:s', strtotime('+30 days')); + $stmtPrime = $con->prepare(" + INSERT INTO passenger_prime_subscriptions (passenger_id, is_prime, expire_at) + VALUES (:pid, 1, :exp) + ON DUPLICATE KEY UPDATE is_prime = 1, expire_at = :exp2, updated_at = NOW() + "); + $stmtPrime->execute([ + ':pid' => $passengerId, + ':exp' => $expireAt, + ':exp2' => $expireAt + ]); + + // 6c. خصم المبلغ من المحفظة عبر S2S (نفس نمط tips/add.php) + $deductUrl = "$walletServer/v2/main/ride/payment/add.php"; + $deductData = [ + "user_id" => $passengerId, + "user_type" => "passenger", + "amount" => -1 * $amount, // سالب = خصم + "action" => "subtract", + "paymentID" => $transactionRef, + "paymentMethod" => "prime-subscription", + "reason" => "Siro Prime Subscription - 1 Month" + ]; + + $chDeduct = curl_init($deductUrl); + curl_setopt_array($chDeduct, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($deductData), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/x-www-form-urlencoded', + 'X-S2S-Api-Key: ' . $s2sKey + ] + ]); + + $deductRaw = curl_exec($chDeduct); + $deductCode = curl_getinfo($chDeduct, CURLINFO_HTTP_CODE); + $deductErr = curl_error($chDeduct); + curl_close($chDeduct); + + $deductRes = json_decode($deductRaw, true); + + if ($deductErr || $deductCode !== 200 || ($deductRes['status'] ?? '') !== 'success') { + // فشل الخصم → نرجع الكل + $con->rollBack(); + error_log("[Prime] Wallet deduct FAILED: HTTP $deductCode | err: $deductErr | response: $deductRaw"); + jsonError("Failed to deduct wallet balance. Please try again."); + exit; + } + + $con->commit(); + + // 6d. تحديث Redis فوراً (التفعيل اللحظي بدون إعادة طلب من DB) + if (isset($redis) && $redis !== null) { + try { + $primeKey = "prime:passenger:{$passengerId}"; + $redis->setex($primeKey, 3600, json_encode([ + 'is_prime' => 1, + 'expire_at' => $expireAt + ])); + } catch (Exception $e) { + // Redis failure is non-critical — DB is source of truth + error_log("[Prime] Redis update failed (non-critical): " . $e->getMessage()); + } + } + + // ── 7. ردّ النجاح للفلاتر ─────────────────────────────────── + jsonSuccess([ + 'is_prime' => true, + 'expire_at' => $expireAt, + 'transaction_ref' => $transactionRef, + 'amount_deducted' => $amount, + 'currency' => $currency, + ], "Welcome to Siro Prime! 👑"); + +} catch (PDOException $e) { + if ($con->inTransaction()) { + $con->rollBack(); + } + error_log("[Prime] DB Error: " . $e->getMessage()); + jsonError("Database error. Please try again."); +} +?> diff --git a/backend/ride/pricing/get.php b/backend/ride/pricing/get.php index 0ccd5001..3fe659da 100644 --- a/backend/ride/pricing/get.php +++ b/backend/ride/pricing/get.php @@ -112,7 +112,7 @@ function getPerKmRate($carType, $kazanRow) { return $rate; } -function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed') { +function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed', $isPrime = false) { global $redis, $redisLocation, $con; $surgeMultiplier = 1.0; @@ -250,11 +250,17 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR $billableMinutes = ($billableMinutes > $minuteCapMedium) ? $minuteCapMedium : $billableMinutes; } - $fare = $billableDistance * $perKmSpeed; - $fare += $billableMinutes * $effectivePerMin; + $baseFareForDistance = $billableDistance * $perKmSpeed; + + // 1. حساب التسعيرة مع الذروة (للسائق) + $fareWithSurge = $baseFareForDistance + ($billableMinutes * $effectivePerMin); + $fareWithSurge *= $surgeMultiplier; + + // 2. حساب التسعيرة الطبيعية بدون ذروة (لراكب Prime) + $fareNoSurge = $baseFareForDistance + ($billableMinutes * $naturePrice); - // Apply Redis Geohash Surge Multiplier - $fare *= $surgeMultiplier; + // نحدد التسعيرة الأساسية حسب حالة الراكب + $fare = $isPrime ? $fareNoSurge : $fareWithSurge; if ($airportCtx) $fare += $airportAddon; if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) { $fare += $damascusAirportBoundAddon; @@ -363,10 +369,21 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR } } - // Apply kazan (e.g. 11%) + // Apply kazan (e.g. 11%) on the passenger's price $withCommission = ceil($price * (1 + $kazanPercent / 100)); - $kazan = $withCommission - $price; - $price_for_driver = $price; + + // Driver price is based on the surged fare ($fareWithSurge) + $driverPriceTarget = max($fareWithSurge, $minFare); + if ($airportCtx) $driverPriceTarget += $airportAddon; + if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) { + $driverPriceTarget += $damascusAirportBoundAddon; + } + + // The driver always gets the non-commissioned part of the SURGE price + $price_for_driver = $driverPriceTarget; + + // Our commission is the difference between what passenger pays and what driver gets + $kazan = $withCommission - $price_for_driver; return [ 'price' => $price, @@ -529,9 +546,58 @@ try { error_log("[Destination Matching] Error: " . $e->getMessage()); } +// ---------------------------------------------------------------------- +// Siro Prime: Check if passenger is a Prime subscriber +// ---------------------------------------------------------------------- +$isPrime = false; +if (!empty($passenger_id)) { + $primeKey = "prime:passenger:{$passenger_id}"; + $cachedPrime = null; + + if (isset($redis) && $redis !== null) { + try { + $cachedPrime = $redis->get($primeKey); + } catch (Exception $e) {} + } + + if ($cachedPrime !== false && $cachedPrime !== null) { + $primeData = json_decode($cachedPrime, true); + if (isset($primeData['is_prime']) && $primeData['is_prime'] == 1) { + $expireAt = strtotime($primeData['expire_at'] ?? '0'); + if ($expireAt > time()) { + $isPrime = true; + } + } + } else { + // Fallback to MySQL if not in Redis + try { + $stmtPrime = $con->prepare("SELECT is_prime, expire_at FROM passenger_prime_subscriptions WHERE passenger_id = :pid LIMIT 1"); + $stmtPrime->execute([':pid' => $passenger_id]); + $primeRow = $stmtPrime->fetch(PDO::FETCH_ASSOC); + + if ($primeRow) { + $expireTime = strtotime($primeRow['expire_at'] ?? '0'); + if ($primeRow['is_prime'] == 1 && $expireTime > time()) { + $isPrime = true; + } + + // Cache in Redis for 1 hour + if (isset($redis) && $redis !== null) { + $redis->setex($primeKey, 3600, json_encode([ + 'is_prime' => $primeRow['is_prime'], + 'expire_at' => $primeRow['expire_at'] + ])); + } + } + } catch (PDOException $e) { + error_log("[Prime] DB error: " . $e->getMessage()); + } + } +} + // Calculate prices for all categories foreach ($categories as $key => $carType) { - $result = calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType); + $result = calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType, $isPrime); $withCommission = $result['withCommission']; $price_for_driver = $result['price_for_driver']; @@ -579,6 +645,7 @@ if (isset($encryptionHelper)) { 'duration' => $duration, 'is_destination_match' => $isDestinationMatch ? 1 : 0, 'matched_driver_id' => $matchedDriverId, + 'is_prime' => $isPrime ? 1 : 0, 'expires' => time() + 420, // Valid for 7 minutes 'prices' => $pricesRaw ]; diff --git a/backend/ride/rides/add_ride.php b/backend/ride/rides/add_ride.php index 6b5df6b5..8d9f146f 100644 --- a/backend/ride/rides/add_ride.php +++ b/backend/ride/rides/add_ride.php @@ -164,6 +164,9 @@ $price_for_passenger = $price; $is_destination_match = isset($tokenData['is_destination_match']) ? (int)$tokenData['is_destination_match'] : 0; $matched_driver_id = isset($tokenData['matched_driver_id']) ? $tokenData['matched_driver_id'] : 0; +// 👑 Siro Prime Status +$is_prime = isset($tokenData['is_prime']) ? (int)$tokenData['is_prime'] : 0; + // ── 2. تنسيق التواريخ ───────────────────────────────────────── $date_formatted = date("Y-m-d"); $time_formatted = date("H:i:s"); @@ -286,6 +289,7 @@ try { (string) $carType, number_format($kazan, 2, '.', ''), (string) $passenger_rating, + (string) $is_prime, // 👑 Index 34: Prime Status ]; // Direct dispatch للسائقين القريبين diff --git a/backend/ride/rides/retry_search_drivers.php b/backend/ride/rides/retry_search_drivers.php index 39a84e48..d0e263e0 100644 --- a/backend/ride/rides/retry_search_drivers.php +++ b/backend/ride/rides/retry_search_drivers.php @@ -174,8 +174,23 @@ try { $payloadTemplate[29] = (string)$startName; $payloadTemplate[30] = (string)$endName; $payloadTemplate[31] = (string)$carType; + // 👑 Check Prime Status from Redis + $isPrimeFlag = "0"; + if (isset($redis)) { + try { + $cachedPrime = $redis->get("prime:passenger:{$passengerId}"); + if ($cachedPrime) { + $primeData = json_decode($cachedPrime, true); + if (isset($primeData['is_prime']) && $primeData['is_prime'] == 1) { + $isPrimeFlag = "1"; + } + } + } catch (Exception $e) {} + } + $payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); // ← Reduced kazan $payloadTemplate[33] = (string)$passengerRating; + $payloadTemplate[34] = $isPrimeFlag; // 👑 Index 34: Prime Status ksort($payloadTemplate); $payloadTemplate = array_values($payloadTemplate); diff --git a/siro_driver/lib/constant/links.dart b/siro_driver/lib/constant/links.dart index 9f1424e3..b2000418 100755 --- a/siro_driver/lib/constant/links.dart +++ b/siro_driver/lib/constant/links.dart @@ -250,11 +250,11 @@ class AppLink { static String get getLeaderboard => "$endPoint/ride/gamification/getLeaderboard.php"; static String get getSubAdminForDriver => - "$serverName/Admin/gamification/getSubAdminForDriver.php"; + "$server/Admin/gamification/getSubAdminForDriver.php"; // 🆕 مسار جلب حالة تتابع الرحلات (Gamification Streak) static String get getDriverStreak => - "$serverName/ride/gamification/get_streak.php"; + "$server/ride/gamification/get_streak.php"; static String get claimChallengeReward => "$endPoint/ride/gamification/claimChallengeReward.php"; static String get getReferralStats => diff --git a/siro_rider/lib/constant/box_name.dart b/siro_rider/lib/constant/box_name.dart index afee642e..730177a1 100644 --- a/siro_rider/lib/constant/box_name.dart +++ b/siro_rider/lib/constant/box_name.dart @@ -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 } diff --git a/siro_rider/lib/constant/links.dart b/siro_rider/lib/constant/links.dart index 8b19834a..87faf9c3 100644 --- a/siro_rider/lib/constant/links.dart +++ b/siro_rider/lib/constant/links.dart @@ -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"; diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index 85094e5e..7a24cb24 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -507,7 +507,7 @@ class RideLifecycleController extends GetxController { currentRideState.value = RideState.noRide; isSearchingWindow = false; update(); - _showIncreaseFeeDialog(); + _showAiNegotiatorDialog(); break; } diff --git a/siro_rider/lib/controller/payment/prime_controller.dart b/siro_rider/lib/controller/payment/prime_controller.dart new file mode 100644 index 00000000..a5b9f32a --- /dev/null +++ b/siro_rider/lib/controller/payment/prime_controller.dart @@ -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> 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 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 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 + }, + ); + } +} diff --git a/siro_rider/lib/views/home/my_wallet/passenger_wallet.dart b/siro_rider/lib/views/home/my_wallet/passenger_wallet.dart index da86571b..9bb1901f 100644 --- a/siro_rider/lib/views/home/my_wallet/passenger_wallet.dart +++ b/siro_rider/lib/views/home/my_wallet/passenger_wallet.dart @@ -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()), // نفس دالتك القديمة ), + + // 👑 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( + 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 لم نعد بحاجة إليه لأنه تم دمجه وتطويره diff --git a/siro_rider/lib/views/home/my_wallet/siro_prime_page.dart b/siro_rider/lib/views/home/my_wallet/siro_prime_page.dart new file mode 100644 index 00000000..9037fa99 --- /dev/null +++ b/siro_rider/lib/views/home/my_wallet/siro_prime_page.dart @@ -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 createState() => _SiroPrimePageState(); +} + +class _SiroPrimePageState extends State + with TickerProviderStateMixin { + late AnimationController _shimmerController; + late AnimationController _pulseController; + late Animation _pulseAnimation; + + final PrimeController ctrl = Get.find(); + + @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(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 _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( + 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 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( + 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, + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } +}