Update: 2026-07-03 01:48:00
This commit is contained in:
@@ -96,6 +96,23 @@ PAYMENT_GATEWAY_URL=https://api.paymentprovider.com
|
||||
PAYMENT_GATEWAY_KEY=<CHANGE_ME_PAYMENT_KEY>
|
||||
PAYMENT_GATEWAY_SECRET=<CHANGE_ME_PAYMENT_SECRET>
|
||||
PAYMENT_WEBHOOK_SECRET=<CHANGE_ME_WEBHOOK_SECRET>
|
||||
# Internal key used for server-to-server calls (Siro Backend → Wallet Server)
|
||||
PAYMENT_KEY=<CHANGE_ME_SHARED_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=<CHANGE_ME_S2S_SHARED_SECRET>
|
||||
|
||||
# =============================================================================
|
||||
# Siro Commissions per Country
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/payments/get_prime_status.php
|
||||
// PURPOSE : جلب حالة اشتراك Siro Prime للراكب
|
||||
// AUTH : JWT (passenger)
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$passengerId = $user_id ?? null;
|
||||
if (!$passengerId || $role !== 'passenger') {
|
||||
jsonError("Unauthorized");
|
||||
exit;
|
||||
}
|
||||
|
||||
$isPrime = false;
|
||||
$expireAt = null;
|
||||
|
||||
// 1. تحقق من Redis أولاً (الأسرع)
|
||||
if (isset($redis) && $redis !== null) {
|
||||
try {
|
||||
$cached = $redis->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");
|
||||
?>
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/payments/initiate_prime.php
|
||||
// PURPOSE : شراء اشتراك Siro Prime عبر خصم رصيد المحفظة الداخلية
|
||||
// AUTH : JWT (passenger)
|
||||
// FLOW :
|
||||
// 1. جلب هوية الراكب من JWT
|
||||
// 2. تحديد السعر حسب الدولة
|
||||
// 3. التحقق من رصيد الراكب في سيرفر المحفظة (S2S)
|
||||
// 4. إذا الرصيد كافٍ → الخصم + تفعيل Prime
|
||||
// 5. إذا الرصيد غير كافٍ → رسالة لإرشاد المستخدم للشحن
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// ── 1. هوية الراكب من JWT ─────────────────────────────────────
|
||||
$passengerId = $user_id ?? null;
|
||||
if (!$passengerId || $role !== 'passenger') {
|
||||
jsonError("Unauthorized");
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── 2. الدولة والسعر ──────────────────────────────────────────
|
||||
$country = filterRequest("country") ?: 'Jordan';
|
||||
|
||||
$pricingMap = [
|
||||
'Jordan' => ['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.");
|
||||
}
|
||||
?>
|
||||
@@ -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;
|
||||
|
||||
// Apply Redis Geohash Surge Multiplier
|
||||
$fare *= $surgeMultiplier;
|
||||
// 1. حساب التسعيرة مع الذروة (للسائق)
|
||||
$fareWithSurge = $baseFareForDistance + ($billableMinutes * $effectivePerMin);
|
||||
$fareWithSurge *= $surgeMultiplier;
|
||||
|
||||
// 2. حساب التسعيرة الطبيعية بدون ذروة (لراكب Prime)
|
||||
$fareNoSurge = $baseFareForDistance + ($billableMinutes * $naturePrice);
|
||||
|
||||
// نحدد التسعيرة الأساسية حسب حالة الراكب
|
||||
$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
|
||||
];
|
||||
|
||||
@@ -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 للسائقين القريبين
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user