diff --git a/app/src/main/java/com/jordanbot/autoride/SubscriptionActivity.kt b/app/src/main/java/com/jordanbot/autoride/SubscriptionActivity.kt
index bd60a05..b58d963 100644
--- a/app/src/main/java/com/jordanbot/autoride/SubscriptionActivity.kt
+++ b/app/src/main/java/com/jordanbot/autoride/SubscriptionActivity.kt
@@ -1,37 +1,56 @@
package com.jordanbot.autoride
import android.os.Bundle
-import android.widget.Button
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
-import com.jordanbot.autoride.api.ActivateSubscriptionRequest
import com.jordanbot.autoride.api.ApiClient
+import com.jordanbot.autoride.api.CheckPaymentRequest
+import com.jordanbot.autoride.api.InitPaymentRequest
import com.jordanbot.autoride.subscription.SubscriptionManager
import com.jordanbot.autoride.utils.DeviceUtils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.*
class SubscriptionActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView
- private lateinit var btnBasic: Button
private lateinit var btnPro: Button
+ private lateinit var btnAnnual: Button
+
+ // Payment Overlay UI
+ private lateinit var layoutOverlay: LinearLayout
+ private lateinit var tvRefCode: TextView
+ private lateinit var tvTimer: TextView
+ private lateinit var btnCancel: Button
+
+ private var pollingJob: Job? = null
+ private var timerJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_subscription)
tvStatus = findViewById(R.id.tv_current_status)
- btnBasic = findViewById(R.id.btn_subscribe_basic)
btnPro = findViewById(R.id.btn_subscribe_pro)
+ btnAnnual = findViewById(R.id.btn_subscribe_annual)
+
+ layoutOverlay = findViewById(R.id.layout_payment_overlay)
+ tvRefCode = findViewById(R.id.tv_ref_code)
+ tvTimer = findViewById(R.id.tv_timer)
+ btnCancel = findViewById(R.id.btn_cancel_payment)
updateStatusUI()
- btnBasic.setOnClickListener { activatePlan("basic") }
- btnPro.setOnClickListener { activatePlan("pro") }
+ btnPro.setOnClickListener { startPaymentFlow("pro", 10.0) }
+ btnAnnual.setOnClickListener { startPaymentFlow("annual", 80.0) }
+
+ btnCancel.setOnClickListener {
+ stopPaymentFlow()
+ }
}
private fun updateStatusUI() {
@@ -40,39 +59,99 @@ class SubscriptionActivity : AppCompatActivity() {
val today = SubscriptionManager.ridesToday
val planText = when(plan) {
- "basic" -> "أساسي ($limit طلب / يوم)"
"pro" -> "احترافي (لا محدود)"
"annual" -> "سنوي (لا محدود)"
else -> "مجاني (1 طلب / يوم)"
}
- tvStatus.text = "الخطة الحالية: $planText\nاستهلاك اليوم: $today"
+ tvStatus.text = "الخطة الحالية: $planText\nاستهلاك اليوم: $today / $limit"
}
- private fun activatePlan(plan: String) {
- // In a real app, integrate payment gateway here.
- // For demonstration, we just call the API directly.
+ private fun startPaymentFlow(plan: String, amount: Double) {
val fingerprint = DeviceUtils.getDeviceFingerprint(this)
lifecycleScope.launch {
try {
val response = withContext(Dispatchers.IO) {
- ApiClient.service.activateSubscription(
- ActivateSubscriptionRequest(fingerprint, plan, "DEMO_REF_123")
- )
+ ApiClient.service.initPayment(InitPaymentRequest(fingerprint, plan, amount))
}
- if (response.success) {
- Toast.makeText(this@SubscriptionActivity, "تم تفعيل الاشتراك بنجاح!", Toast.LENGTH_SHORT).show()
- // Re-check subscription to update local cache
- SubscriptionManager.checkSubscription(this@SubscriptionActivity)
- updateStatusUI()
+ if (response.success && response.reference_code != null) {
+ showPaymentOverlay(response.reference_code, amount)
+ startPolling(response.reference_code)
} else {
- Toast.makeText(this@SubscriptionActivity, "فشل تفعيل الاشتراك: ${response.message}", Toast.LENGTH_SHORT).show()
+ Toast.makeText(this@SubscriptionActivity, "فشل إنشاء طلب الدفع", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
- Toast.makeText(this@SubscriptionActivity, "حدث خطأ في الاتصال", Toast.LENGTH_SHORT).show()
+ Toast.makeText(this@SubscriptionActivity, "حدث خطأ في الاتصال بالسيرفر", Toast.LENGTH_SHORT).show()
}
}
}
+
+ private fun showPaymentOverlay(refCode: String, amount: Double) {
+ tvRefCode.text = refCode
+ layoutOverlay.visibility = View.VISIBLE
+
+ // Start 10 minute timer
+ var secondsLeft = 600
+ timerJob?.cancel()
+ timerJob = lifecycleScope.launch {
+ while (secondsLeft > 0) {
+ val mins = secondsLeft / 60
+ val secs = secondsLeft % 60
+ tvTimer.text = "ننتظر وصول الدفعة... (${String.format("%02d:%02d", mins, secs)})"
+ delay(1000)
+ secondsLeft--
+ }
+ stopPaymentFlow()
+ Toast.makeText(this@SubscriptionActivity, "انتهى وقت طلب الدفع", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ private fun startPolling(refCode: String) {
+ val fingerprint = DeviceUtils.getDeviceFingerprint(this)
+ pollingJob?.cancel()
+ pollingJob = lifecycleScope.launch {
+ while (isActive) {
+ delay(5000) // Poll every 5 seconds
+ try {
+ val response = withContext(Dispatchers.IO) {
+ ApiClient.service.checkPayment(CheckPaymentRequest(refCode, fingerprint))
+ }
+
+ if (response.status == "paid") {
+ onPaymentSuccess()
+ break
+ } else if (response.status == "expired") {
+ stopPaymentFlow()
+ Toast.makeText(this@SubscriptionActivity, "انتهت صلاحية الطلب", Toast.LENGTH_SHORT).show()
+ break
+ }
+ } catch (e: Exception) {
+ // Ignore connection errors during polling
+ }
+ }
+ }
+ }
+
+ private fun onPaymentSuccess() {
+ stopPaymentFlow()
+ Toast.makeText(this, "✅ تم تفعيل الاشتراك بنجاح!", Toast.LENGTH_LONG).show()
+ lifecycleScope.launch {
+ SubscriptionManager.checkSubscription(this@SubscriptionActivity)
+ updateStatusUI()
+ }
+ }
+
+ private fun stopPaymentFlow() {
+ pollingJob?.cancel()
+ timerJob?.cancel()
+ layoutOverlay.visibility = View.GONE
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ pollingJob?.cancel()
+ timerJob?.cancel()
+ }
}
diff --git a/app/src/main/java/com/jordanbot/autoride/api/BackendApi.kt b/app/src/main/java/com/jordanbot/autoride/api/BackendApi.kt
index 5e1e5fd..ade6d19 100644
--- a/app/src/main/java/com/jordanbot/autoride/api/BackendApi.kt
+++ b/app/src/main/java/com/jordanbot/autoride/api/BackendApi.kt
@@ -67,11 +67,24 @@ data class ActivateSubscriptionRequest(
)
@Keep
-data class ActivateSubscriptionResponse(
+data class InitPaymentRequest(val fingerprint: String, val plan: String, val amount: Double)
+
+@Keep
+data class InitPaymentResponse(
val success: Boolean,
- val message: String?,
- val plan: String?,
- val expires_at: String?
+ val reference_code: String?,
+ val amount: Double?,
+ val cliq_alias: String?,
+ val expires_in_minutes: Int?
+)
+
+@Keep
+data class CheckPaymentRequest(val reference_code: String, val fingerprint: String)
+
+@Keep
+data class CheckPaymentResponse(
+ val success: Boolean,
+ val status: String // pending, paid, expired
)
interface BackendApiService {
@@ -86,6 +99,12 @@ interface BackendApiService {
@POST("api/subscription/activate.php")
suspend fun activateSubscription(@Body request: ActivateSubscriptionRequest): ActivateSubscriptionResponse
+
+ @POST("api/subscription/init_payment.php")
+ suspend fun initPayment(@Body request: InitPaymentRequest): InitPaymentResponse
+
+ @POST("api/subscription/check_payment.php")
+ suspend fun checkPayment(@Body request: CheckPaymentRequest): CheckPaymentResponse
}
object ApiClient {
diff --git a/app/src/main/res/layout/activity_subscription.xml b/app/src/main/res/layout/activity_subscription.xml
index c5aabff..ee16da6 100644
--- a/app/src/main/res/layout/activity_subscription.xml
+++ b/app/src/main/res/layout/activity_subscription.xml
@@ -1,163 +1,236 @@
-
+ android:background="#121222">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:padding="32dp"
+ android:visibility="gone">
-
+ android:textAlignment="center" />
+
+
+
+
+
+
+
+ android:textSize="12sp" />
-
-
+
+
+ android:layout_marginTop="8dp"
+ android:text="ننتظر وصول الدفعة... (10:00)"
+ android:textColor="#AAAAAA" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginTop="32dp"
+ android:background="?attr/selectableItemBackground"
+ android:text="إلغاء الطلب"
+ android:textColor="#888888" />
-
+
+
diff --git a/backend/api/subscription/check_payment.php b/backend/api/subscription/check_payment.php
new file mode 100644
index 0000000..e789bf0
--- /dev/null
+++ b/backend/api/subscription/check_payment.php
@@ -0,0 +1,50 @@
+ false, 'message' => 'Method Not Allowed']);
+ exit;
+}
+
+$input = json_decode(file_get_contents('php://input'), true);
+$referenceCode = $input['reference_code'] ?? null;
+$fingerprint = $input['fingerprint'] ?? null;
+
+if (!$referenceCode || !$fingerprint) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'message' => 'Missing reference_code or fingerprint']);
+ exit;
+}
+
+try {
+ $stmt = $pdo->prepare("SELECT status FROM cliq_payments WHERE reference_code = :ref AND fingerprint = :fingerprint LIMIT 1");
+ $stmt->execute([':ref' => $referenceCode, ':fingerprint' => $fingerprint]);
+ $payment = $stmt->fetch();
+
+ if ($payment) {
+ // If it's still pending but older than 15 minutes, mark it as expired
+ if ($payment['status'] === 'pending') {
+ $stmtDate = $pdo->prepare("UPDATE cliq_payments SET status = 'expired' WHERE reference_code = :ref AND created_at < NOW() - INTERVAL 15 MINUTE");
+ $stmtDate->execute([':ref' => $referenceCode]);
+
+ // Re-fetch if we just expired it
+ if ($stmtDate->rowCount() > 0) {
+ $payment['status'] = 'expired';
+ }
+ }
+
+ echo json_encode([
+ 'success' => true,
+ 'status' => $payment['status'] // 'pending', 'paid', or 'expired'
+ ]);
+ } else {
+ http_response_code(404);
+ echo json_encode(['success' => false, 'message' => 'Payment not found']);
+ }
+} catch (PDOException $e) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
+}
diff --git a/backend/api/subscription/init_payment.php b/backend/api/subscription/init_payment.php
new file mode 100644
index 0000000..cc89e4d
--- /dev/null
+++ b/backend/api/subscription/init_payment.php
@@ -0,0 +1,50 @@
+ false, 'message' => 'Method Not Allowed']);
+ exit;
+}
+
+$input = json_decode(file_get_contents('php://input'), true);
+$fingerprint = $input['fingerprint'] ?? null;
+$plan = $input['plan'] ?? null;
+$amount = $input['amount'] ?? null;
+
+if (!$fingerprint || !$plan || !$amount) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'message' => 'Missing required fields']);
+ exit;
+}
+
+// Expire old pending payments for this user to avoid confusion
+try {
+ $stmt = $pdo->prepare("UPDATE cliq_payments SET status = 'expired' WHERE fingerprint = :fingerprint AND status = 'pending'");
+ $stmt->execute([':fingerprint' => $fingerprint]);
+
+ // Generate a unique 6-character reference code (e.g. JB-1A2B3C)
+ $refCode = 'JB-' . strtoupper(substr(md5(uniqid(rand(), true)), 0, 6));
+
+ // Insert new pending payment
+ $stmt = $pdo->prepare("INSERT INTO cliq_payments (fingerprint, reference_code, amount, plan, status) VALUES (:fingerprint, :refCode, :amount, :plan, 'pending')");
+ $stmt->execute([
+ ':fingerprint' => $fingerprint,
+ ':refCode' => $refCode,
+ ':amount' => $amount,
+ ':plan' => $plan
+ ]);
+
+ echo json_encode([
+ 'success' => true,
+ 'reference_code' => $refCode,
+ 'amount' => $amount,
+ 'cliq_alias' => 'JordanBot', // Change this to the actual CliQ alias
+ 'expires_in_minutes' => 10
+ ]);
+} catch (PDOException $e) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
+}
diff --git a/backend/api/subscription/setup_cliq_db.php b/backend/api/subscription/setup_cliq_db.php
new file mode 100644
index 0000000..12bb151
--- /dev/null
+++ b/backend/api/subscription/setup_cliq_db.php
@@ -0,0 +1,22 @@
+exec($sql);
+ echo "Table 'cliq_payments' created or already exists successfully.";
+} catch (PDOException $e) {
+ echo "Error creating table: " . $e->getMessage();
+}
diff --git a/backend/api/subscription/webhook_sms.php b/backend/api/subscription/webhook_sms.php
new file mode 100644
index 0000000..56df62f
--- /dev/null
+++ b/backend/api/subscription/webhook_sms.php
@@ -0,0 +1,91 @@
+ false, 'message' => 'Empty message']);
+ exit;
+}
+
+// 1. Log the incoming SMS for debugging
+error_log("JordanBot SMS Received: Sender: [$sender], Content: [$message]");
+
+// 2. Extract Reference Code (Pattern: JB-XXXXXX)
+// Matches JB- followed by 6 alphanumeric characters
+preg_match('/JB-([A-Z0-9]{6})/', strtoupper($message), $matches);
+$refCode = isset($matches[0]) ? $matches[0] : null;
+
+// 3. Extract Amount (Pattern: finds decimal numbers)
+// Note: Jordan uses 'JOD' or 'دينار'
+preg_match('/([0-9]+(\.[0-9]{2})?)/', $message, $amtMatches);
+$amountReceived = isset($amtMatches[0]) ? floatval($amtMatches[0]) : 0;
+
+if (!$refCode) {
+ echo json_encode(['success' => false, 'message' => 'No Reference Code found in SMS']);
+ exit;
+}
+
+try {
+ // 4. Find the pending payment
+ $stmt = $pdo->prepare("SELECT * FROM cliq_payments WHERE reference_code = :ref AND status = 'pending' LIMIT 1");
+ $stmt->execute([':ref' => $refCode]);
+ $payment = $stmt->fetch();
+
+ if ($payment) {
+ // Optional: Verify amount match (allowing for minor differences or currency symbols)
+ if (abs($payment['amount'] - $amountReceived) > 0.05) {
+ error_log("JordanBot: Amount mismatch for $refCode. Expected: {$payment['amount']}, Received: $amountReceived");
+ // We can still proceed or mark for manual review
+ }
+
+ $pdo->beginTransaction();
+
+ // 5. Update payment status
+ $stmt = $pdo->prepare("UPDATE cliq_payments SET status = 'paid' WHERE id = :id");
+ $stmt->execute([':id' => $payment['id']]);
+
+ // 6. Activate/Extend Subscription
+ $fingerprint = $payment['fingerprint'];
+ $plan = $payment['plan'];
+
+ // Calculate expiration (e.g. basic=30 days, annual=365 days)
+ $days = ($plan === 'annual') ? 365 : 30;
+ $expiresAt = date('Y-m-d H:i:s', strtotime("+$days days"));
+
+ // Check if user already has a subscription to extend it
+ $stmtCheck = $pdo->prepare("SELECT id, expires_at FROM subscriptions WHERE fingerprint = :fingerprint AND is_active = 1 LIMIT 1");
+ $stmtCheck->execute([':fingerprint' => $fingerprint]);
+ $existing = $stmtCheck->fetch();
+
+ if ($existing) {
+ // Extend existing
+ $newExpiry = date('Y-m-d H:i:s', strtotime($existing['expires_at'] . " +$days days"));
+ $stmtUpdate = $pdo->prepare("UPDATE subscriptions SET expires_at = :expiry, plan = :plan WHERE id = :id");
+ $stmtUpdate->execute([':expiry' => $newExpiry, ':plan' => $plan, ':id' => $existing['id']]);
+ } else {
+ // Create new
+ $stmtInsert = $pdo->prepare("INSERT INTO subscriptions (fingerprint, plan, expires_at, is_active) VALUES (:fingerprint, :plan, :expiry, 1)");
+ $stmtInsert->execute([':fingerprint' => $fingerprint, ':plan' => $plan, ':expiry' => $expiresAt]);
+ }
+
+ $pdo->commit();
+
+ error_log("JordanBot: Subscription activated for $fingerprint via $refCode ($plan)");
+ echo json_encode(['success' => true, 'message' => "Subscription activated for $refCode"]);
+
+ } else {
+ echo json_encode(['success' => false, 'message' => 'Reference code not found or already processed']);
+ }
+} catch (Exception $e) {
+ if ($pdo->inTransaction()) $pdo->rollBack();
+ http_response_code(500);
+ echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
+}