Update: 2026-05-15 03:09:36

This commit is contained in:
Hamza-Ayed
2026-05-15 03:09:36 +03:00
parent 9fc1319946
commit dd4fcb9bee
93 changed files with 766 additions and 55 deletions

View File

@@ -1,4 +1,4 @@
#Thu May 14 20:41:24 EET 2026
#Thu May 14 20:47:58 EET 2026
base.0=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeExtDexDebug/classes.dex
base.1=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeProjectDexDebug/0/classes.dex
base.2=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeProjectDexDebug/12/classes.dex

View File

@@ -1 +1 @@
Α·ΟύΊΛΊ<EFBFBD><EFBFBD>ΰ
ֱ·ֿ÷ֻ÷ׂ<EFBFBD>את

View File

@@ -43,6 +43,8 @@
</intent-filter>
</activity>
<activity android:name=".SubscriptionActivity" android:exported="false" />
<service
android:name=".service.RideNotificationListener"
android:label="@string/app_name"

View File

@@ -21,6 +21,10 @@ import androidx.core.content.ContextCompat
import com.jordanbot.autoride.service.BotForegroundService
import com.jordanbot.autoride.service.OverlayService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.jordanbot.autoride.subscription.SubscriptionManager
class MainActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView
@@ -31,10 +35,22 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Load cached subscription data
SubscriptionManager.loadLocalCache(this)
// Check subscription with server
lifecycleScope.launch {
SubscriptionManager.checkSubscription(this@MainActivity)
}
tvStatus = findViewById(R.id.tv_status)
btnStart = findViewById(R.id.btn_start)
btnStop = findViewById(R.id.btn_stop)
findViewById<Button>(R.id.btn_subscriptions).setOnClickListener {
startActivity(Intent(this, SubscriptionActivity::class.java))
}
findViewById<Button>(R.id.btn_notification).setOnClickListener {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
}

View File

@@ -0,0 +1,78 @@
package com.jordanbot.autoride
import android.os.Bundle
import android.widget.Button
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.subscription.SubscriptionManager
import com.jordanbot.autoride.utils.DeviceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubscriptionActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView
private lateinit var btnBasic: Button
private lateinit var btnPro: Button
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)
updateStatusUI()
btnBasic.setOnClickListener { activatePlan("basic") }
btnPro.setOnClickListener { activatePlan("pro") }
}
private fun updateStatusUI() {
val plan = SubscriptionManager.currentPlan
val limit = SubscriptionManager.ridesLimit
val today = SubscriptionManager.ridesToday
val planText = when(plan) {
"basic" -> "أساسي ($limit طلب / يوم)"
"pro" -> "احترافي (لا محدود)"
"annual" -> "سنوي (لا محدود)"
else -> "مجاني (1 طلب / يوم)"
}
tvStatus.text = "الخطة الحالية: $planText\nاستهلاك اليوم: $today"
}
private fun activatePlan(plan: String) {
// In a real app, integrate payment gateway here.
// For demonstration, we just call the API directly.
val fingerprint = DeviceUtils.getDeviceFingerprint(this)
lifecycleScope.launch {
try {
val response = withContext(Dispatchers.IO) {
ApiClient.service.activateSubscription(
ActivateSubscriptionRequest(fingerprint, plan, "DEMO_REF_123")
)
}
if (response.success) {
Toast.makeText(this@SubscriptionActivity, "تم تفعيل الاشتراك بنجاح!", Toast.LENGTH_SHORT).show()
// Re-check subscription to update local cache
SubscriptionManager.checkSubscription(this@SubscriptionActivity)
updateStatusUI()
} else {
Toast.makeText(this@SubscriptionActivity, "فشل تفعيل الاشتراك: ${response.message}", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Toast.makeText(this@SubscriptionActivity, "حدث خطأ في الاتصال", Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@@ -38,12 +38,41 @@ data class ApiResponse(
val message: String
)
data class CheckSubscriptionRequest(val fingerprint: String)
data class CheckSubscriptionResponse(
val success: Boolean,
val plan: String?,
val expires_at: String?,
val rides_today: Int?,
val rides_limit: Int?,
val can_accept: Boolean?
)
data class ActivateSubscriptionRequest(
val fingerprint: String,
val plan: String,
val payment_ref: String?
)
data class ActivateSubscriptionResponse(
val success: Boolean,
val message: String?,
val plan: String?,
val expires_at: String?
)
interface BackendApiService {
@POST("api/rides.php")
suspend fun logRide(@Body request: RideLogRequest): ApiResponse
@POST("api/location.php")
suspend fun updateBulkLocation(@Body request: BulkLocationRequest): ApiResponse
@POST("api/subscription/check.php")
suspend fun checkSubscription(@Body request: CheckSubscriptionRequest): CheckSubscriptionResponse
@POST("api/subscription/activate.php")
suspend fun activateSubscription(@Body request: ActivateSubscriptionRequest): ActivateSubscriptionResponse
}
object ApiClient {

View File

@@ -0,0 +1,67 @@
package com.jordanbot.autoride.engine
import android.util.Log
import com.jordanbot.autoride.model.RideRequest
import kotlinx.coroutines.*
object RideDataMerger {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private var lastRequest: RideRequest? = null
private var lastTimestamp: Long = 0
private const val MERGE_WINDOW_MS = 5000L
var onRideReady: ((RideRequest) -> Unit)? = null
@Synchronized
fun updateFromNotification(request: RideRequest) {
Log.d("JordanBot", "🔔 Merger received Notification data: ${request.priceJod} JOD")
merge(request)
}
@Synchronized
fun updateFromScreen(request: RideRequest) {
Log.d("JordanBot", "👁️ Merger received Screen data: ${request.priceJod} JOD")
merge(request)
}
private fun merge(newPart: RideRequest) {
val now = System.currentTimeMillis()
val current = lastRequest
if (current == null || (now - lastTimestamp) > MERGE_WINDOW_MS || current.appPackage != newPart.appPackage) {
// New request sequence
lastRequest = newPart
lastTimestamp = now
// Wait a bit to see if more data comes from the other source before emitting
scope.launch {
delay(1500) // Wait 1.5s for the other source
emitIfReady()
}
} else {
// Merge into current
lastRequest = current.copy(
priceJod = newPart.priceJod ?: current.priceJod,
minutesAway = newPart.minutesAway ?: current.minutesAway,
distanceKm = newPart.distanceKm ?: current.distanceKm,
pickupAddress = newPart.pickupAddress ?: current.pickupAddress,
dropoffAddress = newPart.dropoffAddress ?: current.dropoffAddress,
title = if (newPart.title.isNotEmpty() && newPart.title != "Screen Scrape") newPart.title else current.title,
text = if (newPart.text.isNotEmpty() && newPart.text != current.text) "${current.text} | ${newPart.text}" else current.text
)
lastTimestamp = now
emitIfReady()
}
}
private fun emitIfReady() {
val ride = lastRequest ?: return
// Criteria for "Ready": We have at least a price
if (ride.priceJod != null) {
Log.d("JordanBot", "🔀 MERGED RIDE READY: $ride")
onRideReady?.invoke(ride)
lastRequest = null // Clear to avoid double emission
}
}
}

View File

@@ -5,6 +5,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.location.Location
import android.os.Build
import android.os.IBinder
import android.os.Looper
@@ -24,13 +25,26 @@ class BotForegroundService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private lateinit var fusedLocationClient: FusedLocationProviderClient
private val locationBuffer = mutableListOf<LocationPoint>()
private val UPLOAD_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30)
private val TRACK_INTERVAL_MS = TimeUnit.SECONDS.toMillis(3)
// Battery-optimized intervals
private val UPLOAD_INTERVAL_MS = TimeUnit.MINUTES.toMillis(5) // Upload every 5 minutes
private val TRACK_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15) // GPS poll every 15 seconds
private val MIN_DISPLACEMENT_METERS = 15f // Ignore movement < 15m
// Displacement filter: track last known location
private var lastTrackedLocation: Location? = null
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
// Displacement filter: skip if driver hasn't moved 15 meters
val last = lastTrackedLocation
if (last != null && last.distanceTo(location) < MIN_DISPLACEMENT_METERS) {
return // Driver is stationary, save battery
}
lastTrackedLocation = location
val point = LocationPoint(
latitude = location.latitude,
longitude = location.longitude,
@@ -58,7 +72,7 @@ class BotForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Jordan Bot يعمل حالياً")
.setContentText("نحن نراقب الإشعارات ونحدث الخرائط.")
.setContentText("نبحث عن طلبات قريبة منك...")
.setSmallIcon(R.drawable.bg_bubble)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
@@ -69,7 +83,10 @@ class BotForegroundService : Service() {
@SuppressLint("MissingPermission")
private fun startTracking() {
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, TRACK_INTERVAL_MS)
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_BALANCED_POWER_ACCURACY, TRACK_INTERVAL_MS
)
.setMinUpdateDistanceMeters(MIN_DISPLACEMENT_METERS)
.setMinUpdateIntervalMillis(TRACK_INTERVAL_MS)
.build()

View File

@@ -2,70 +2,143 @@ package com.jordanbot.autoride.service
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.jordanbot.autoride.engine.RideDataMerger
import com.jordanbot.autoride.filter.FilterEngine
import com.jordanbot.autoride.model.RideRequest
class RideAccessibilityService : AccessibilityService() {
private val handler = Handler(Looper.getMainLooper())
private val filterEngine = FilterEngine()
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
val rootNode = rootInActiveWindow ?: return
val packageName = event.packageName?.toString() ?: ""
val source = event.source ?: return
val packageName = event.packageName?.toString() ?: return
// Only process supported apps
if (!isSupportedApp(packageName)) return
// Check if this package was recently flagged by NotificationListener to be accepted
if (RideNotificationListener.pendingAcceptPackage == packageName) {
val timeSinceRequest = System.currentTimeMillis() - RideNotificationListener.lastRequestTime
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
// Only accept if within 15 seconds of notification
if (timeSinceRequest < 15000) {
findAndClickAcceptButton(source)
} else {
RideNotificationListener.pendingAcceptPackage = null // Expired
val screenData = scrapeScreen(rootNode, packageName)
if (screenData != null) {
RideDataMerger.updateFromScreen(screenData)
}
}
}
private fun findAndClickAcceptButton(rootNode: AccessibilityNodeInfo) {
// Arabic and English accept terms
val acceptTerms = listOf("Accept", "قبول", "Tap to accept", "اضغط للقبول")
private fun isSupportedApp(pkg: String): Boolean {
return pkg == "com.ubercab.driver" || pkg == "com.careem.adma" || pkg == "me.jeeny.driver"
}
private fun scrapeScreen(root: AccessibilityNodeInfo, pkg: String): RideRequest? {
val allText = mutableListOf<String>()
collectAllText(root, allText)
val combinedText = allText.joinToString(" ")
// Extract price
val priceRegex = """(\d+\.?\d*)\s*(JOD|د\.أ|دينار)""".toRegex()
val priceMatch = priceRegex.find(combinedText)
val price = priceMatch?.groupValues?.get(1)?.toDoubleOrNull()
if (price != null) {
Log.d("JordanBot", "👁️ Scraped price from screen: $price $pkg")
return RideRequest(
appPackage = pkg,
priceJod = price,
minutesAway = null,
distanceKm = null,
title = "Screen Scrape",
text = combinedText
)
}
return null
}
private fun collectAllText(node: AccessibilityNodeInfo?, list: MutableList<String>) {
if (node == null) return
node.text?.let { list.add(it.toString()) }
for (i in 0 until node.childCount) {
collectAllText(node.getChild(i), list)
}
}
private fun findAndAccept(root: AccessibilityNodeInfo, pkg: String) {
val acceptTerms = listOf("Accept", "قبول", "Tap to accept", "Confirm", "تأكيد")
for (term in acceptTerms) {
val nodes = rootNode.findAccessibilityNodeInfosByText(term)
val nodes = root.findAccessibilityNodeInfosByText(term)
for (node in nodes) {
if (node.isClickable) {
// Simulating human delay (e.g., 500ms)
handler.postDelayed({
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
Log.d("JordanBot", "Clicked Accept on node: ${node.text}")
// Reset pending accept after clicking
RideNotificationListener.pendingAcceptPackage = null
}, 500)
if (node.isClickable || node.parent?.isClickable == true) {
val target = if (node.isClickable) node else node.parent
Log.d("JordanBot", "🤖 Auto-Accepting via Click: $term")
target.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return
}
}
}
if (pkg == "com.ubercab.driver") {
performUberSwipe()
}
}
override fun onInterrupt() {
Log.e("JordanBot", "Accessibility Service Interrupted")
private fun performUberSwipe() {
Log.d("JordanBot", "🤖 Attempting Uber Swipe...")
val displayMetrics = resources.displayMetrics
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
val path = Path()
path.moveTo(width * 0.2f, height * 0.85f)
path.lineTo(width * 0.8f, height * 0.85f)
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 500))
.build()
dispatchGesture(gesture, null, null)
}
override fun onInterrupt() {}
override fun onServiceConnected() {
super.onServiceConnected()
Log.d("JordanBot", "Accessibility Service Connected")
RideDataMerger.onRideReady = { mergedRide ->
val passesFilters = filterEngine.evaluate(mergedRide)
val canAccept = SubscriptionManager.canAcceptRides
Log.d("JordanBot", "🏁 Merger callback: PassesFilters=$passesFilters, CanAcceptQuota=$canAccept for ${mergedRide.appPackage}")
if (passesFilters) {
if (canAccept) {
val rootNode = rootInActiveWindow
if (rootNode != null) {
findAndAccept(rootNode, mergedRide.appPackage)
} else {
Log.e("JordanBot", "Cannot accept: rootInActiveWindow is null")
}
} else {
Log.w("JordanBot", "⚠️ Ride matches filters but DAILY LIMIT REACHED. Upgrade required.")
}
}
}
val info = AccessibilityServiceInfo().apply {
eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
notificationTimeout = 100
flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
flags = AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
}
this.serviceInfo = info
}

View File

@@ -70,23 +70,11 @@ class RideNotificationListener : NotificationListenerService() {
Log.d("JordanBot", "Notification Content: $title | $text")
val rideRequest = parser.parse(title, text)
if (rideRequest == null) {
Log.d("JordanBot", "Parser failed to extract ride info from: $title | $text")
}
if (rideRequest != null) {
val isAccepted = filterEngine.evaluate(rideRequest)
// Send to Backend for Data Mining
sendLogToBackend(rideRequest, isAccepted, "$title - $text")
if (isAccepted) {
Log.d("JordanBot", "Ride ACCEPTED by filter: $rideRequest")
pendingAcceptPackage = packageName
lastRequestTime = System.currentTimeMillis()
} else {
Log.d("JordanBot", "Ride REJECTED by filter: $rideRequest")
}
// Forward to Merger instead of processing here
RideDataMerger.updateFromNotification(rideRequest)
} else {
Log.d("JordanBot", "Parser failed to extract ride info from: $title | $text")
}
}

View File

@@ -0,0 +1,61 @@
package com.jordanbot.autoride.subscription
import android.content.Context
import android.util.Log
import com.jordanbot.autoride.api.ApiClient
import com.jordanbot.autoride.api.CheckSubscriptionRequest
import com.jordanbot.autoride.utils.DeviceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object SubscriptionManager {
private const val PREFS_NAME = "SubscriptionPrefs"
private const val KEY_PLAN = "plan"
private const val KEY_CAN_ACCEPT = "can_accept"
private const val KEY_RIDES_TODAY = "rides_today"
private const val KEY_RIDES_LIMIT = "rides_limit"
var currentPlan: String = "free"
var canAcceptRides: Boolean = true
var ridesToday: Int = 0
var ridesLimit: Int = 1
fun loadLocalCache(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
currentPlan = prefs.getString(KEY_PLAN, "free") ?: "free"
canAcceptRides = prefs.getBoolean(KEY_CAN_ACCEPT, true)
ridesToday = prefs.getInt(KEY_RIDES_TODAY, 0)
ridesLimit = prefs.getInt(KEY_RIDES_LIMIT, 1)
}
suspend fun checkSubscription(context: Context): Boolean {
return withContext(Dispatchers.IO) {
try {
val fingerprint = DeviceUtils.getDeviceFingerprint(context)
val response = ApiClient.service.checkSubscription(CheckSubscriptionRequest(fingerprint))
if (response.success) {
currentPlan = response.plan ?: "free"
canAcceptRides = response.can_accept ?: false
ridesToday = response.rides_today ?: 0
ridesLimit = response.rides_limit ?: 1
// Save to cache
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().apply {
putString(KEY_PLAN, currentPlan)
putBoolean(KEY_CAN_ACCEPT, canAcceptRides)
putInt(KEY_RIDES_TODAY, ridesToday)
putInt(KEY_RIDES_LIMIT, ridesLimit)
apply()
}
Log.d("JordanBot", "Subscription checked: $currentPlan, canAccept: $canAcceptRides")
return@withContext true
}
} catch (e: Exception) {
Log.e("JordanBot", "Failed to check subscription", e)
}
return@withContext false
}
}
}

View File

@@ -179,7 +179,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="④ تحديث الخرائط (GPS)"
android:text="④ الموافقة على الطلبات القريبة"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold" />
@@ -188,7 +188,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="مطلوب لتتبع مسارك وتحديث خريطة الأردن وحفظ إحصائياتك"
android:text="مطلوب لمطابقة الطلبات القريبة من موقعك وتسريع القبول التلقائي"
android:textColor="#999999"
android:textSize="12sp" />
@@ -198,11 +198,21 @@
android:layout_height="44dp"
android:layout_marginTop="8dp"
android:background="@drawable/bg_button"
android:text="تفعيل تتبع الموقع"
android:text="تفعيل مطابقة الطلبات القريبة"
android:textColor="#FFFFFF"
android:textSize="14sp" />
</LinearLayout>
<!-- Navigation Buttons -->
<Button
android:id="@+id/btn_subscriptions"
android:layout_width="match_parent"
android:layout_height="48dp"
android:backgroundTint="#2A2A4A"
android:text="💳 إدارة الاشتراكات"
android:textColor="#FFFFFF"
android:layout_marginBottom="24dp" />
<!-- Launch Button -->
<Button
android:id="@+id/btn_start"

View File

@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1A1A2E"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="💳 الاشتراكات"
android:textColor="#00D4AA"
android:textSize="32sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="اختر الخطة المناسبة لعملك"
android:textColor="#AAAAAA"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_current_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="الخطة الحالية: مجاني (1 طلب / يوم)"
android:textAlignment="center"
android:textColor="#FFFFFF"
android:textSize="16sp" />
<!-- Free Plan -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="المجانية"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 JOD"
android:textColor="#00D4AA"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="• طلب واحد يومياً\n• تجربة الأساسيات"
android:textColor="#AAAAAA"
android:textSize="14sp" />
</LinearLayout>
<!-- Basic Plan -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="الأساسية"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1 JOD / شهر"
android:textColor="#00D4AA"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="• 10 طلبات يومياً\n• إحصائيات أساسية\n• فلاتر متقدمة"
android:textColor="#AAAAAA"
android:textSize="14sp" />
<Button
android:id="@+id/btn_subscribe_basic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:backgroundTint="#00D4AA"
android:textColor="#FFFFFF"
android:text="اشترك الآن" />
</LinearLayout>
<!-- Pro Plan -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="الاحترافية"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2.5 JOD / شهر"
android:textColor="#00D4AA"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="• طلبات غير محدودة\n• أولوية القبول\n• دعم فني مخصص"
android:textColor="#AAAAAA"
android:textSize="14sp" />
<Button
android:id="@+id/btn_subscribe_pro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:backgroundTint="#FF9800"
android:textColor="#FFFFFF"
android:text="اشترك الآن" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -55,6 +55,51 @@ $latitude = $input['latitude'] ?? null;
$longitude = $input['longitude'] ?? null;
try {
// --- Subscription Quota Check ---
if ($isAccepted === 1) {
$today = date('Y-m-d');
// Get active subscription
$stmt = $pdo->prepare("SELECT plan, expires_at FROM subscriptions WHERE fingerprint = :fingerprint AND is_active = 1 ORDER BY id DESC LIMIT 1");
$stmt->execute([':fingerprint' => $fingerprint]);
$sub = $stmt->fetch(PDO::FETCH_ASSOC);
$plan = 'free';
if ($sub) {
$plan = $sub['plan'];
if ($sub['expires_at'] && strtotime($sub['expires_at']) < time()) {
$plan = 'free'; // Expired
}
}
// Get daily usage
$stmt = $pdo->prepare("SELECT rides_accepted FROM daily_usage WHERE fingerprint = :fingerprint AND usage_date = :today");
$stmt->execute([':fingerprint' => $fingerprint, ':today' => $today]);
$usage = $stmt->fetch(PDO::FETCH_ASSOC);
$ridesToday = $usage ? (int)$usage['rides_accepted'] : 0;
// Determine limit
$limit = 1; // free
if ($plan === 'basic') $limit = 10;
if ($plan === 'pro' || $plan === 'annual') $limit = -1;
if ($limit !== -1 && $ridesToday >= $limit) {
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'Daily limit reached',
'plan' => $plan,
'upgrade_required' => true
]);
exit;
}
// Update daily usage
$stmt = $pdo->prepare("INSERT INTO daily_usage (fingerprint, usage_date, rides_accepted) VALUES (:fingerprint, :today, 1) ON DUPLICATE KEY UPDATE rides_accepted = rides_accepted + 1");
$stmt->execute([':fingerprint' => $fingerprint, ':today' => $today]);
}
// --------------------------------
$sql = "INSERT INTO rides (fingerprint, platform, price, pickup_distance, dropoff_distance, time_to_pickup, pickup_address, dropoff_address, is_accepted, raw_text, latitude, longitude, created_at)
VALUES (:fingerprint, :platform, :price, :pickup_distance, :dropoff_distance, :time_to_pickup, :pickup_address, :dropoff_address, :is_accepted, :raw_text, :latitude, :longitude, NOW())";

View File

@@ -0,0 +1,70 @@
<?php
require_once __DIR__ . '/../../config/db.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method Not Allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$fingerprint = $input['fingerprint'] ?? null;
$plan = $input['plan'] ?? null;
$paymentRef = $input['payment_ref'] ?? null;
if (!$fingerprint || !$plan) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing fingerprint or plan']);
exit;
}
$validPlans = ['free', 'basic', 'pro', 'annual'];
if (!in_array($plan, $validPlans)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid plan type']);
exit;
}
// Calculate expiration date based on plan
$expiresAt = null;
if ($plan === 'basic' || $plan === 'pro') {
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
} elseif ($plan === 'annual') {
$expiresAt = date('Y-m-d H:i:s', strtotime('+365 days'));
}
try {
$pdo->beginTransaction();
// Deactivate previous active subscriptions for this device
$stmt = $pdo->prepare("UPDATE subscriptions SET is_active = 0 WHERE fingerprint = :fingerprint");
$stmt->execute([':fingerprint' => $fingerprint]);
// Insert new subscription
$stmt = $pdo->prepare("INSERT INTO subscriptions (fingerprint, plan, expires_at, payment_ref, is_active)
VALUES (:fingerprint, :plan, :expires_at, :payment_ref, 1)");
$stmt->execute([
':fingerprint' => $fingerprint,
':plan' => $plan,
':expires_at' => $expiresAt,
':payment_ref' => $paymentRef
]);
$pdo->commit();
echo json_encode([
'success' => true,
'message' => 'Subscription activated successfully',
'plan' => $plan,
'expires_at' => $expiresAt
]);
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}

View File

@@ -0,0 +1,68 @@
<?php
require_once __DIR__ . '/../../config/db.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method Not Allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$fingerprint = $input['fingerprint'] ?? null;
if (!$fingerprint) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing fingerprint']);
exit;
}
try {
// 1. Get Subscription Status
$stmt = $pdo->prepare("SELECT * FROM subscriptions WHERE fingerprint = :fingerprint AND is_active = 1 ORDER BY id DESC LIMIT 1");
$stmt->execute([':fingerprint' => $fingerprint]);
$sub = $stmt->fetch(PDO::FETCH_ASSOC);
$plan = $sub ? $sub['plan'] : 'free';
$expiresAt = $sub ? $sub['expires_at'] : null;
// Check expiration
if ($expiresAt && strtotime($expiresAt) < time()) {
// Expired, revert to free
$stmt = $pdo->prepare("UPDATE subscriptions SET is_active = 0 WHERE id = :id");
$stmt->execute([':id' => $sub['id']]);
$plan = 'free';
$expiresAt = null;
}
// 2. Get Daily Usage
$today = date('Y-m-d');
$stmt = $pdo->prepare("SELECT rides_accepted FROM daily_usage WHERE fingerprint = :fingerprint AND usage_date = :today");
$stmt->execute([
':fingerprint' => $fingerprint,
':today' => $today
]);
$usage = $stmt->fetch(PDO::FETCH_ASSOC);
$ridesToday = $usage ? (int)$usage['rides_accepted'] : 0;
// 3. Determine limits
$limit = 1; // Default for free
if ($plan === 'basic') $limit = 10;
if ($plan === 'pro' || $plan === 'annual') $limit = -1; // Unlimited
$canAccept = ($limit === -1) || ($ridesToday < $limit);
echo json_encode([
'success' => true,
'plan' => $plan,
'expires_at' => $expiresAt,
'rides_today' => $ridesToday,
'rides_limit' => $limit,
'can_accept' => $canAccept
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}

View File

@@ -23,6 +23,29 @@ CREATE TABLE IF NOT EXISTS rides (
INDEX (platform)
) ENGINE=InnoDB;
-- Subscription System
CREATE TABLE IF NOT EXISTS subscriptions (
id INT AUTO_INCREMENT PRIMARY KEY,
fingerprint VARCHAR(255) NOT NULL,
plan ENUM('free', 'basic', 'pro', 'annual') DEFAULT 'free',
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
is_active TINYINT(1) DEFAULT 1,
payment_ref VARCHAR(255),
INDEX (fingerprint),
INDEX (expires_at)
) ENGINE=InnoDB;
-- Daily Usage Quotas
CREATE TABLE IF NOT EXISTS daily_usage (
id INT AUTO_INCREMENT PRIMARY KEY,
fingerprint VARCHAR(255) NOT NULL,
usage_date DATE NOT NULL,
rides_accepted INT DEFAULT 0,
UNIQUE KEY unique_daily (fingerprint, usage_date),
INDEX (fingerprint)
) ENGINE=InnoDB;
-- Table to store driver locations for map updating
CREATE TABLE IF NOT EXISTS driver_locations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,