Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
#Sun Jun 21 02:56:57 EET 2026
gradle.version=8.13
+2
View File
@@ -0,0 +1,2 @@
#Sun Jun 21 02:56:49 EET 2026
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
/build
+64
View File
@@ -0,0 +1,64 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.siro.android_bot"
compileSdk = 36
defaultConfig {
applicationId = "com.siro.android_bot"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BOT_SECRET_KEY", "\"SIRO_BOT_SUPER_SECRET_123\"")
// مضيف الباك إند في مكان واحد: تبديله عند نسخ البوت لعلامة أخرى
// (مثل انطلق على api.intaleqapp.com) يصير سطراً واحداً لا بحثاً في الكود.
buildConfigField("String", "BACKEND_HOST", "\"https://jordan-siro.intaleqapp.com\"")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.siro.android_bot
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.siro.android_bot", appContext.packageName)
}
}
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<package android:name="ae.com.yalla.go.dubai.client" />
<package android:name="com.zakinn.app" />
<package android:name="com.bis.taxi" />
<package android:name="com.careem.acma" />
<package android:name="com.ubercab" />
<package android:name="com.taxif.passenger" />
<package android:name="me.com.easytaxi" />
</queries>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Android_bot"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Android_bot">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".service.ScraperAccessibilityService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>
@@ -0,0 +1,104 @@
package com.siro.android_bot
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.siro.android_bot.ui.theme.Android_botTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN"
enableEdgeToEdge()
setContent {
Android_botTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
BotSetupScreen(
deviceId = androidId,
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun BotSetupScreen(deviceId: String, modifier: Modifier = Modifier) {
val context = LocalContext.current
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Siro Scraper Bot 🤖",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "Your Device ID (Android ID):",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = deviceId,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Black,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
Button(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Device ID", deviceId)
clipboard.setPrimaryClip(clip)
Toast.makeText(context, "Copied to clipboard!", Toast.LENGTH_SHORT).show()
}) {
Text("Copy ID")
}
Spacer(modifier = Modifier.height(48.dp))
Text(
text = "Make sure to add this ID to your Siro Server.\nAlso, remember to enable this app in Settings > Accessibility.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Android_botTheme {
BotSetupScreen("a1b2c3d4e5f6g7h8")
}
}
@@ -0,0 +1,129 @@
package com.siro.android_bot.network
import com.siro.android_bot.BuildConfig
import android.content.Context
import android.provider.Settings
import android.util.Log
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class WorkerClient(private val context: Context) {
private val TAG = "WorkerClient"
// TODO: Fetch this from server during first handshake instead of hardcoding
// For now, must match the BOT_SECRET_KEY env var on the server
private val SECRET_KEY = BuildConfig.BOT_SECRET_KEY
// Read the real hardware Android ID
val deviceId: String by lazy {
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN_DEVICE"
}
// المضيف من BuildConfig.BACKEND_HOST (app/build.gradle.kts) — لا يُشفَّر هنا،
// حتى يكون تبديله عند نسخ البوت لعلامة أخرى سطراً واحداً في gradle.
// للتجريب المحلي: اضبط BACKEND_HOST على "http://10.0.2.2:8000" مع تقديم
// نفس المسار /backend/bot/ من جذر السيرفر المحلي.
// worker.php هو المسار الإنتاجي (Redis + MySQL). لا تُعِده إلى
// standalone_worker.php: تلك نسخة اختبار محلي تخزّن في ملفات JSON
// ولا تصل نتائجها إلى جدول scraped_competitor_prices ولا إلى محرّك التسعير.
private val BASE_URL = "${BuildConfig.BACKEND_HOST}/backend/bot/worker.php"
private fun generateSignature(deviceId: String, ts: Long): String {
val message = "$deviceId$ts"
val algorithm = "HmacSHA256"
val mac = Mac.getInstance(algorithm)
val secretKeySpec = SecretKeySpec(SECRET_KEY.toByteArray(), algorithm)
mac.init(secretKeySpec)
val hashBytes = mac.doFinal(message.toByteArray())
return hashBytes.joinToString("") { "%02x".format(it) }
}
fun fetchTask(): JSONObject? {
try {
val ts = System.currentTimeMillis() / 1000
val sig = generateSignature(deviceId, ts)
val urlString = "$BASE_URL?device_id=$deviceId&ts=$ts&sig=$sig"
val url = URL(urlString)
Log.d(TAG, "Fetching task from: $urlString")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 10000
connection.readTimeout = 10000
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.readText()
reader.close()
return JSONObject(response)
} else {
Log.e(TAG, "Fetch Error: HTTP $responseCode")
}
} catch (e: Exception) {
Log.e(TAG, "Exception during fetchTask: ${e.message}")
}
return null
}
fun submitPrice(taskId: String, appName: String, startLat: Double, startLng: Double, endLat: Double, endLng: Double, distanceKm: Double, price: Double): Boolean {
try {
val ts = System.currentTimeMillis() / 1000
val sig = generateSignature(deviceId, ts)
val payload = JSONObject().apply {
put("device_id", deviceId)
put("ts", ts)
put("sig", sig)
put("task_id", taskId)
put("type", "price_check")
put("status", "success")
val resultData = JSONObject().apply {
put("app", appName)
put("distance_km", distanceKm)
put("price", price)
put("start_lat", startLat)
put("start_lng", startLng)
put("end_lat", endLat)
put("end_lng", endLng)
}
put("result_data", resultData)
}
val url = URL(BASE_URL)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
connection.doOutput = true
connection.connectTimeout = 10000
connection.readTimeout = 10000
val writer = OutputStreamWriter(connection.outputStream)
writer.write(payload.toString())
writer.flush()
writer.close()
val responseCode = connection.responseCode
return if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
Log.d(TAG, "Submit Success: ${reader.readText()}")
reader.close()
true
} else {
Log.e(TAG, "Submit Error: HTTP $responseCode")
false
}
} catch (e: Exception) {
Log.e(TAG, "Exception during submitPrice: ${e.message}")
return false
}
}
}
@@ -0,0 +1,47 @@
package com.siro.android_bot.service
import android.content.Context
import android.content.Intent
import android.util.Log
object AppLauncher {
private const val TAG = "AppLauncher"
// Maps the server app name to the actual Android package name
// Important: We will verify these exact package names tomorrow once you install them.
private val APP_PACKAGES = mapOf(
"yallago" to "ae.com.yalla.go.dubai.client",
"ae.com.yalla.go.dubai.client" to "ae.com.yalla.go.dubai.client",
"zaken" to "com.zakinn.app",
"com.zakinn.app" to "com.zakinn.app",
"tufaddal" to "com.bis.taxi",
"com.bis.taxi" to "com.bis.taxi",
"careem" to "com.careem.acma",
"com.careem.acma" to "com.careem.acma",
"uber" to "com.ubercab",
"com.ubercab" to "com.ubercab",
"taxif" to "com.taxif.passenger",
"com.taxif.passenger" to "com.taxif.passenger",
"jeeny" to "me.com.easytaxi",
"me.com.easytaxi" to "me.com.easytaxi"
)
fun launchApp(context: Context, appName: String): Boolean {
val packageName = APP_PACKAGES[appName.lowercase()]
if (packageName == null) {
Log.e(TAG, "Unknown app name from server: $appName")
return false
}
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(launchIntent)
Log.i(TAG, "Successfully launched $appName ($packageName)")
return true
} else {
Log.e(TAG, "App $appName ($packageName) is not installed on this device!")
return false
}
}
}
@@ -0,0 +1,11 @@
package com.siro.android_bot.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package com.siro.android_bot.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun Android_botTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package com.siro.android_bot.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<resources>
<string name="app_name">android_bot</string>
<string name="accessibility_service_description">Siro Scraper Accessibility Service helps automate competitor price checks.</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Android_bot" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true"
android:description="@string/accessibility_service_description"
android:notificationTimeout="100" />
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">location.intaleq.xyz</domain>
<domain includeSubdomains="true">intaleq.xyz</domain>
<domain includeSubdomains="true">siromove.com</domain>
</domain-config>
</network-security-config>
@@ -0,0 +1,17 @@
package com.siro.android_bot
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+6
View File
@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
+23
View File
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+32
View File
@@ -0,0 +1,32 @@
[versions]
agp = "8.13.2"
kotlin = "2.0.21"
coreKtx = "1.18.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Sun Jun 21 02:56:41 EET 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+10
View File
@@ -0,0 +1,10 @@
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file should *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=/Users/hamzaaleghwairyeen/Library/Android/sdk
+23
View File
@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "android_bot"
include(":app")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

After

Width:  |  Height:  |  Size: 149 KiB

+14
View File
@@ -129,4 +129,18 @@ class BoxName {
static const String isBusMode = 'isBusMode';
static const String busModeTripId = 'busModeTripId';
static const String busModeRouteId = 'busModeRouteId';
// توفّر Google Play Services — يحدّد شراسة ملف تتبّع الموقع
// (بلا GMS يسقط الباكيج إلى LocationManager الخام: بطارية أعلى، دقة أقل)
static const String isGmsAvailable = 'isGmsAvailable';
// وضع التوصيل مفعّل — تقرأه العزلة الخلفية لتقرر فتح سوكيت الطعام
static const String foodDeliveryModeEnabled = 'foodDeliveryModeEnabled';
// معرّفات مهام التوصيل النشطة — يقرأها بثّ الموقع في LocationController
// فيستمر البثّ بعد إغلاق شاشة التوصيل
static const String foodActiveOrders = 'foodActiveOrders';
// بيانات اعتماد TURN المؤقتة (مكالمات الصوت) — مخزّنة حتى قرب انتهائها
static const String turnIceCache = 'turnIceCache';
}
@@ -69,14 +69,14 @@ class CountryPolygons {
static String getRoutingApiUrl(String countryName) {
switch (countryName) {
case 'Jordan':
return 'https://routec.tripz.xyz/route-jo';
return 'https://routec.intaleq.xyz/route-jo';
case 'Syria':
return 'https://routec.tripz.xyz/route';
return 'https://routec.intaleq.xyz/route';
case 'Egypt':
return 'https://routec.tripz.xyz/route-eg';
return 'https://routec.intaleq.xyz/route-eg';
default:
// الافتراضي في حالة لم يقع الموقع ضمن أي من المضلعات
return 'https://routec.tripz.xyz/route';
return 'https://routec.intaleq.xyz/route';
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import 'package:tripz_driver/main.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:siro_driver/main.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:get/get.dart';
class CurrencyHelper {
@@ -1,44 +1,59 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'colors.dart';
/// كان هذا نظام ألوان **ثانياً منافساً** لـ [AppColor]: نيفي `#0A0E21` وأزرق
/// `#3D5AFE` بدل نيفي العلامة `#1A2340` والـ Periwinkle `#8C9CF8`. النتيجة أن
/// صفحات المحفظة والأرباح والتحديات كانت تبدو من تطبيق آخر.
///
/// أُبقي الصنف باسمه حتى لا تتكسّر عشرات مواضع الاستدعاء، لكنه صار **واجهة
/// رقيقة فوق ألوان العلامة**. الجديد يُكتب بـ `SiroUi` / `Theme.of(context)`.
class FinanceDesignSystem {
// --- Colors ---
static Color get primaryDark => Get.isDarkMode ? const Color(0xFFE0E0E0) : const Color(0xFF0A0E21);
static Color get accentBlue => const Color(0xFF3D5AFE);
static Color get successGreen => const Color(0xFF00C853);
static Color get dangerRed => const Color(0xFFD50000);
static Color get backgroundColor => Get.isDarkMode ? const Color(0xFF0A0E21) : const Color(0xFFF6F8FA);
static Color get cardColor => Get.isDarkMode ? const Color(0xFF1E1E2E) : const Color(0xFFFFFFFF);
static Color get textSecondary => Get.isDarkMode ? Colors.white70 : const Color(0xFF757575);
static Color get textMuted => Get.isDarkMode ? Colors.white38 : const Color(0xFFBDBDBD);
static Color get borderColor => Get.isDarkMode ? Colors.white10 : Colors.grey.withOpacity(0.1);
static Color get primaryDark =>
Get.isDarkMode ? const Color(0xFFF1F5F9) : AppColor.primaryColor;
static Color get accentBlue =>
Get.isDarkMode ? AppColor.secondaryColorStatic : AppColor.primaryColor;
static Color get successGreen => AppColor.greenColor;
static Color get dangerRed => AppColor.redColor;
static Color get backgroundColor =>
Get.isDarkMode ? const Color(0xFF0F1424) : const Color(0xFFF6F8FA);
static Color get cardColor =>
Get.isDarkMode ? const Color(0xFF171D33) : const Color(0xFFFFFFFF);
static Color get textSecondary =>
Get.isDarkMode ? const Color(0xFF94A3B8) : const Color(0xFF64748B);
static Color get textMuted =>
Get.isDarkMode ? const Color(0xFF64748B) : const Color(0xFF94A3B8);
static Color get borderColor =>
Get.isDarkMode ? const Color(0xFF262E4A) : const Color(0xFFE6EAF0);
// --- Shadows ---
static BoxShadow get softShadow => BoxShadow(
color: Colors.black.withOpacity(Get.isDarkMode ? 0.3 : 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
);
color: Colors.black.withValues(alpha: Get.isDarkMode ? 0.35 : 0.05),
blurRadius: 18,
offset: const Offset(0, 6),
);
// --- Gradients ---
/// تدرّج العلامة: نيفي ← Periwinkle. يُستعمل في بطاقات الرصيد والرؤوس.
static LinearGradient get balanceGradient => LinearGradient(
colors: Get.isDarkMode
? [const Color(0xFF1E1E2E), const Color(0xFF2A2A3E)]
: [const Color(0xFF0A0E21), const Color(0xFF1A237E)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
colors: Get.isDarkMode
? [const Color(0xFF1E2745), const Color(0xFF2E3A63)]
: [AppColor.primaryColor, const Color(0xFF3A4674)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
static const LinearGradient dangerGradient = LinearGradient(
colors: [Color(0xFFD50000), Color(0xFFFF5252)],
colors: [AppColor.redColor, Color(0xFFF87171)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// --- Radius ---
static const double mainRadius = 16.0;
static const double cardRadius = 20.0;
static const double buttonRadius = 12.0;
static const double cardRadius = 18.0;
static const double buttonRadius = 14.0;
// --- Spacing ---
static const double horizontalPadding = 16.0;
@@ -46,19 +61,19 @@ class FinanceDesignSystem {
// --- Text Styles ---
static TextStyle get balanceStyle => const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
);
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
);
static TextStyle get headingStyle => TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: primaryDark,
);
fontSize: 17,
fontWeight: FontWeight.bold,
color: primaryDark,
);
static TextStyle get subHeadingStyle => TextStyle(
fontSize: 14,
color: textSecondary,
);
fontSize: 13,
color: textSecondary,
);
}
+15 -15
View File
@@ -1,12 +1,12 @@
class AppInformation {
static const String companyName = 'Tripz';
static const String appName = 'Tripz DRIVER';
static const String appVersion = 'Tripz DRIVER';
static const String companyName = 'Siro';
static const String appName = 'Siro DRIVER';
static const String appVersion = 'Siro DRIVER';
static const String phoneNumber = '';
static const String linkedInProfile =
'https://www.linkedin.com/in/hamza-ayed/';
static const String website = 'https://tripzapp.com';
static const String email = 'support@tripzapp.com';
static const String website = 'https://siromove.com';
static const String email = 'support@siromove.com';
static const String complaintPrompt =
'for this data for complaint from driver or passenger i collect all data i want you analyze this complaint and show what is reason and what is solution .this data collected from many table to find solution if payment in visa not complete and if ride status is finished it will be paymnet in payment table if ride status is not finished there is no need to pay and payment table is null for this ride and if paymentFromPaymentTable not null and visa type not cash the payment sucssessed . if ratingpassenger is low or passengr rating drivers low grade then dont mine of this passenger ,look at driver too like passengerratingdriver with rating or ratingtopassenger .in json add status of complaint and message to passenger and message to driver and message to call center write in arabic in json output with key in english .for output please just json i want';
static const String addd = 'BlBlNl';
@@ -16,7 +16,7 @@ class AppInformation {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tripz - Privacy Policy & Terms of Use</title>
<title>Siro - Privacy Policy & Terms of Use</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -71,11 +71,11 @@ class AppInformation {
</div>
<h2>1. Introduction and Acceptance</h2>
<p>By downloading, registering, or using the Tripz application ("App"), you agree to be bound by this Privacy Policy and our Terms of Use. If you do not agree, you must stop using the App immediately. Your continued use constitutes acceptance of these terms and any future updates.</p>
<p>By downloading, registering, or using the Siro application ("App"), you agree to be bound by this Privacy Policy and our Terms of Use. If you do not agree, you must stop using the App immediately. Your continued use constitutes acceptance of these terms and any future updates.</p>
<h2>2. Definitions</h2>
<ul>
<li><strong>"Tripz", "we", "us":</strong> Refers to the Tripz for Ride Hailing company, Damascus – Syria (Owner & operator), which provides the technology platform.</li>
<li><strong>"Siro", "we", "us":</strong> Refers to the Siro for Ride Hailing company, Damascus – Syria (Owner & operator), which provides the technology platform.</li>
<li><strong>"Driver":</strong> An independent service provider who uses the App to offer transportation services.</li>
<li><strong>"Passenger", "you":</strong> An individual who uses the App to request transportation services.</li>
<li><strong>"Services":</strong> The connection between Passengers and Drivers facilitated by our App.</li>
@@ -136,14 +136,14 @@ class AppInformation {
</ul>
<h2>5. Disclaimer of Liability</h2>
<p>The App is provided "as is". Tripz is an intermediary platform and is not liable for the actions of Drivers or Passengers, accidents, delays, or any disputes between users. Our liability is limited to the fullest extent permitted by law.</p>
<p>The App is provided "as is". Siro is an intermediary platform and is not liable for the actions of Drivers or Passengers, accidents, delays, or any disputes between users. Our liability is limited to the fullest extent permitted by law.</p>
<h2>6. Policy Updates</h2>
<p>We may update these terms. If we make significant changes, we will notify you within the App. You will be required to review and accept the new terms to continue using the Services, ensuring your consent is active and informed.</p>
<h2>7. Account Deletion & Contact</h2>
<p>You have the right to request the deletion of your account and personal data. To do so, or for any other questions, please contact us. We will respond to deletion requests within 30 days.</p>
<p><strong>Email:</strong> <a href="mailto:support@tripzapp.com">support@tripzapp.com</a></p>
<p><strong>Email:</strong> <a href="mailto:support@siromove.com">support@siromove.com</a></p>
</body>
</html>
@@ -155,7 +155,7 @@ class AppInformation {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>تريبز - سياسة الخصوصية وشروط الاستخدام</title>
<title>سيرو - سياسة الخصوصية وشروط الاستخدام</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -211,11 +211,11 @@ class AppInformation {
</div>
<h2>1. المقدمة والقبول</h2>
<p>عبر تحميل أو تسجيل أو استخدام تطبيق "تريبز" ("التطبيق")، فإنك توافق على الالتزام بسياسة الخصوصية وشروط الاستخدام هذه. إذا كنت لا توافق، يجب عليك التوقف فورًا عن استخدام التطبيق. استمرارك في الاستخدام يُعد قبولاً لهذه الشروط وأي تحديثات مستقبلية لها.</p>
<p>عبر تحميل أو تسجيل أو استخدام تطبيق "سيرو" ("التطبيق")، فإنك توافق على الالتزام بسياسة الخصوصية وشروط الاستخدام هذه. إذا كنت لا توافق، يجب عليك التوقف فورًا عن استخدام التطبيق. استمرارك في الاستخدام يُعد قبولاً لهذه الشروط وأي تحديثات مستقبلية لها.</p>
<h2>2. التعريفات</h2>
<ul>
<li><strong>"تريبز"، "نحن":</strong> تشير إلى شركة تريبز لنقل الركاب، دمشق – سوريا (مالك ومشغل التطبيق)، التي توفر المنصة التقنية.</li>
<li><strong>"سيرو"، "نحن":</strong> تشير إلى شركة سيرو لنقل الركاب، دمشق – سوريا (مالك ومشغل التطبيق)، التي توفر المنصة التقنية.</li>
<li><strong>"السائق":</strong> مقدم خدمة مستقل يستخدم التطبيق لتقديم خدمات النقل.</li>
<li><strong>"الراكب"، "أنت":</strong> الفرد الذي يستخدم التطبيق لطلب خدمات النقل.</li>
<li><strong>"الخدمات":</strong> عملية الربط بين الركاب والسائقين التي يسهلها تطبيقنا.</li>
@@ -276,14 +276,14 @@ class AppInformation {
</ul>
<h2>5. إخلاء المسؤولية</h2>
<p>يتم تقديم التطبيق "كما هو". "تريبز" هي منصة وسيطة وليست مسؤولة عن تصرفات السائقين أو الركاب، أو الحوادث، أو التأخير، أو أي نزاعات بين المستخدمين. مسؤوليتنا محدودة إلى أقصى حد يسمح به القانون.</p>
<p>يتم تقديم التطبيق "كما هو". "سيرو" هي منصة وسيطة وليست مسؤولة عن تصرفات السائقين أو الركاب، أو الحوادث، أو التأخير، أو أي نزاعات بين المستخدمين. مسؤوليتنا محدودة إلى أقصى حد يسمح به القانون.</p>
<h2>6. تحديثات السياسة</h2>
<p>قد نقوم بتحديث هذه الشروط. في حال إجراء تغييرات جوهرية، سنقوم بإعلامك داخل التطبيق. سيُطلب منك مراجعة الشروط الجديدة وقبولها لمواصلة استخدام الخدمات، لضمان أن موافقتك فعالة ومبنية على معرفة.</p>
<h2>7. حذف الحساب والتواصل</h2>
<p>لديك الحق في طلب حذف حسابك وبياناتك الشخصية. للقيام بذلك، أو لأي استفسارات أخرى، يرجى التواصل معنا. سنرد على طلبات الحذف في غضون 30 يومًا.</p>
<p><strong>البريد الإلكتروني:</strong> <a href="mailto:support@tripzapp.com">support@tripzapp.com</a></p>
<p><strong>البريد الإلكتروني:</strong> <a href="mailto:support@siromove.com">support@siromove.com</a></p>
</body>
</html>
+105 -53
View File
@@ -1,4 +1,4 @@
// import 'package:tripz_driver/env/env.dart';
// import 'package:siro_driver/env/env.dart';
import '../main.dart';
import 'box_name.dart';
@@ -9,35 +9,48 @@ class AppLink {
static String get paymentServer {
switch (currentCountry) {
case 'Syria':
return 'https://wallet-syria.tripzmove.com/v1/main';
return 'https://wallet-syria.siromove.com/v1/main';
case 'Egypt':
return 'https://wallet-egypt.tripzmove.com/v1/main';
return 'https://wallet-egypt.siromove.com/v1/main';
case 'Jordan':
return 'https://wallettripz.tripz.xyz/v2/main';
return 'https://jordan-siro.intaleqapp.com/v2/main';
default:
return 'https://wallet.tripzmove.com/v1/main';
return 'https://wallet.siromove.com/v1/main';
}
}
static const String appDomain = 'tripzmove.com';
static const String appDomain = 'siromove.com';
// ── روابط متجر تطبيق السائق (سيرو كابتن) ────────────────────────────────
// تُستخدم في كل رسائل مشاركة/دعوة السائقين.
static const String driverAppStoreIOS =
'https://apps.apple.com/jo/app/id6785282536';
static const String driverAppStoreAndroid =
'https://play.google.com/store/apps/details?id=com.siro.siro_driver';
/// سطرا التحميل الجاهزان للإلصاق في نص المشاركة — نرسل الرابطين معاً
/// لأننا لا نعرف نظام تشغيل المستلم عند مشاركة نص عبر واتساب.
static String get driverDownloadLinks =>
'📱 Android: $driverAppStoreAndroid\n🍏 iPhone: $driverAppStoreIOS';
static String get inviteRedirectUrl {
if (currentCountry == 'Syria') {
return "https://tripzmove.com/inviteSyria.php";
return "https://siromove.com/inviteSyria.php";
}
return "https://tripzmove.com/invite.php";
return "https://siromove.com/invite.php";
}
static String get location {
switch (currentCountry) {
case 'Syria':
return 'https://api-syria.tripzmove.com/tripz_v3/ride/location';
return 'https://api-syria.siromove.com/siro_v3/ride/location';
case 'Egypt':
return 'https://api-egypt.tripzmove.com/tripz_v3/ride/location';
return 'https://api-egypt.siromove.com/siro_v3/ride/location';
case 'Jordan':
return 'https://jordan-tripz.tripzapp.com/backend/ride/location';
return 'https://jordan-siro.intaleqapp.com/backend/ride/location';
default:
return 'https://api.tripzmove.com/tripz_v3/ride/location';
return 'https://api.siromove.com/siro_v3/ride/location';
}
}
@@ -46,65 +59,82 @@ class AppLink {
static String get locationServerSide {
switch (currentCountry) {
case 'Syria':
return 'https://location-syria.tripzmove.com/tripz/ride/location';
return 'https://location-syria.siromove.com/siro/ride/location';
case 'Egypt':
return 'https://location-egypt.tripzmove.com/tripz/ride/location';
return 'https://location-egypt.siromove.com/siro/ride/location';
case 'Jordan':
return 'https://jordan-tripz.tripzapp.com/loction_server/tripz/ride/location';
return 'https://jordan-siro.intaleqapp.com/loction_server/siro/ride/location';
default:
return 'https://location-jordan.tripzmove.com/tripz/ride/location';
return 'https://location-jordan.siromove.com/siro/ride/location';
}
}
static String get locationSocketUrl {
switch (currentCountry) {
case 'Syria':
return 'https://location-syria.tripzmove.com';
return 'https://location-syria.siromove.com';
case 'Egypt':
return 'https://location-egypt.tripzmove.com';
return 'https://location-egypt.siromove.com';
case 'Jordan':
return 'https://jordan-tripz.tripzapp.com:2020';
return 'https://jordan-siro.intaleqapp.com:2020';
default:
return 'https://jordan-tripz.tripzapp.com:2020';
return 'https://jordan-siro.intaleqapp.com:2020';
}
}
/// سوكيت الطعام — قناة مستقلة تماماً عن سوكيت الرحلات (locationSocketUrl).
/// عملية منفصلة (food_socket.php) وبورت منفصل 4040، فانقطاع أحدهما لا يُسقط
/// الآخر: السائق يظل يستقبل الرحلات وإن سقط سوكيت الطعام والعكس.
/// nginx على المضيف يستمع 4040 بالشهادة ويمرّر إلى 14040 داخل دوكر.
static String get foodSocketUrl {
switch (currentCountry) {
case 'Syria':
return 'https://food-syria.siromove.com:4040';
case 'Egypt':
return 'https://food-egypt.siromove.com:4040';
case 'Jordan':
return 'https://jordan-siro.intaleqapp.com:4040';
default:
return 'https://jordan-siro.intaleqapp.com:4040';
}
}
static String get mapSaasRoute {
switch (currentCountry) {
case 'Syria':
return 'https://map-syria.tripzmove.com/api/maps/route';
return 'https://map-syria.siromove.com/api/maps/route';
case 'Egypt':
return 'https://map-egypt.tripzmove.com/api/maps/route';
return 'https://map-egypt.siromove.com/api/maps/route';
case 'Jordan':
return 'https://map-saas.tripzapp.com/api/maps/route';
return 'https://map-saas.intaleqapp.com/api/maps/route';
default:
return 'https://map-saas.tripzapp.com/api/maps/route';
return 'https://map-saas.intaleqapp.com/api/maps/route';
}
}
static String get mapSaasPlaces {
switch (currentCountry) {
case 'Syria':
return 'https://map-syria.tripzmove.com/api/geocoding/places';
return 'https://map-syria.siromove.com/api/geocoding/places';
case 'Egypt':
return 'https://map-egypt.tripzmove.com/api/geocoding/places';
return 'https://map-egypt.siromove.com/api/geocoding/places';
case 'Jordan':
return 'https://map-saas.tripzapp.com/api/geocoding/places';
return 'https://map-saas.intaleqapp.com/api/geocoding/places';
default:
return 'https://map-saas.tripzapp.com/api/geocoding/places';
return 'https://map-saas.intaleqapp.com/api/geocoding/places';
}
}
static String get routeApiBaseUrl {
switch (currentCountry) {
case 'Syria':
return "https://routes-syria.tripzmove.com/route/v1/driving";
return "https://routes-syria.siromove.com/route/v1/driving";
case 'Egypt':
return "https://routes-egypt.tripzmove.com/route/v1/driving";
return "https://routes-egypt.siromove.com/route/v1/driving";
case 'Jordan':
return "https://routesjo.tripz.xyz/route/v1/driving";
return "https://routesjo.intaleq.xyz/route/v1/driving";
default:
return "https://routes.tripzmove.com/route/v1/driving";
return "https://routes.siromove.com/route/v1/driving";
}
}
@@ -113,35 +143,35 @@ class AppLink {
static String get endPoint {
switch (currentCountry) {
case 'Syria':
return 'https://api-syria.tripzmove.com/tripz';
return 'https://api-syria.siromove.com/siro';
case 'Egypt':
return 'https://api-egypt.tripzmove.com/tripz';
return 'https://api-egypt.siromove.com/siro';
case 'Jordan':
return 'https://jordan-tripz.tripzapp.com/backend';
return 'https://jordan-siro.intaleqapp.com/backend';
default:
return 'https://api.tripzmove.com/tripz';
return 'https://api.siromove.com/siro';
}
}
static String get syria => 'https://api-syria.tripzmove.com/tripz';
static String get syria => 'https://api-syria.siromove.com/siro';
static String get server => endPoint;
///=================ride==========================///
///https://jordan-tripz.tripzapp.com/backend
///https://jordan-siro.intaleqapp.com/backend
static String get ride => '$server/ride';
/// this ride server is for socket communication not for rest api calls
/// https://rides.tripz.xyz/tripz
/// https://rides.intaleq.xyz/intaleq
static String get rideServer {
switch (currentCountry) {
case 'Syria':
return 'https://ride-syria.tripzmove.com/tripz/ride';
return 'https://ride-syria.siromove.com/siro/ride';
case 'Egypt':
return 'https://ride-egypt.tripzmove.com/tripz/ride';
return 'https://ride-egypt.siromove.com/siro/ride';
case 'Jordan':
return 'https://jordan-tripz.tripzapp.com/backend';
return 'https://jordan-siro.intaleqapp.com/backend';
default:
return 'https://jordan-tripz.tripzapp.com/backend';
return 'https://jordan-siro.intaleqapp.com/backend';
}
}
@@ -151,17 +181,17 @@ class AppLink {
static String get getPredictiveDemand =>
"$server/api/demand/getPredictiveDemandZones.php";
///mapOSM = 'https://routesy.tripz.xyz'
///mapOSM = 'https://routesy.intaleq.xyz'
static String get mapOSM {
switch (currentCountry) {
case 'Syria':
return 'https://routes-syria.tripzmove.com';
return 'https://routes-syria.siromove.com';
case 'Egypt':
return 'https://routes-egypt.tripzmove.com';
return 'https://routes-egypt.siromove.com';
case 'Jordan':
return 'https://routesjo.tripz.xyz';
return 'https://routesjo.intaleq.xyz';
default:
return 'https://routes.tripzmove.com';
return 'https://routes.siromove.com';
}
}
@@ -248,6 +278,13 @@ class AppLink {
static String get getDriverDetails =>
"$seferCairoServer/ride/driverWallet/getDriverDetails.php";
// ================= Earnings Endpoints =================
// نقطة واحدة لكل أرقام شاشة الأرباح بختم زمني واحد. النداءات الأربعة
// السابقة كانت تصل في لحظات مختلفة فتُنتج أرقاماً لا تتفق.
static String get getEarningsSummary =>
"$endPoint/ride/earnings/summary.php";
static String get getEarningsTrips => "$endPoint/ride/earnings/trips.php";
// ================= Gamification Endpoints =================
static String get getWeeklyAggregate =>
"$endPoint/ride/gamification/getWeeklyAggregate.php";
@@ -396,13 +433,13 @@ class AppLink {
static String get reverseGeocoding {
switch (currentCountry) {
case 'Syria':
return 'https://map-syria.tripzmove.com/api/geocoding/reverse';
return 'https://map-syria.siromove.com/api/geocoding/reverse';
case 'Egypt':
return 'https://map-egypt.tripzmove.com/api/geocoding/reverse';
return 'https://map-egypt.siromove.com/api/geocoding/reverse';
case 'Jordan':
return 'https://map-saas.tripzapp.com/api/geocoding/reverse';
return 'https://map-saas.intaleqapp.com/api/geocoding/reverse';
default:
return 'https://map-saas.tripzapp.com/api/geocoding/reverse';
return 'https://map-saas.intaleqapp.com/api/geocoding/reverse';
}
}
@@ -432,7 +469,7 @@ class AppLink {
static String get updateTips => "$ride/tips/update.php";
//-----------------Help Center------------------
static String get addhelpCenter => "$ride/helpCenter/add.php";
static String get addhelpCenter => "$ride/helpCenter/ask_gemini.php";
static String get gethelpCenter => "$ride/helpCenter/get.php";
static String get getByIdhelpCenter => "$ride/helpCenter/getById.php";
static String get updatehelpCenter => "$ride/helpCenter/update.php";
@@ -460,6 +497,9 @@ class AppLink {
// =====================================
static String get addRateToPassenger => "$ride/rate/add.php";
/// آخر رحلة منتهية لم يقيّم السائق راكبها بعد.
static String get pendingRating => "$ride/rate/pending_rating.php";
static String get addRateToDriver => "$ride/rate/addRateToDriver.php";
static String get addRateApp => "$ride/rate/add_rate_app.php";
static String get sendEmailRateingApp => "$ride/rate/sendEmailRateingApp.php";
@@ -645,4 +685,16 @@ class AppLink {
if (clean.startsWith('01') || clean.startsWith('1')) return 'Egypt';
return '';
}
// ================= Scheduled Rides =================
static String get scheduledList => "$server/ride/scheduled/list.php";
// ================= Driver Insurance =================
// ملاحظة: addHealthInsuranceProvider/getHealthInsuranceProvider أعلاه هما
// المسار القديم على جدول driver_health_assurance. هذه هي نقاط الخطط
// والأقساط الجديدة، وتعمل على جداول insurance_* المنفصلة.
static String get insurancePlans => "$server/driver_assurance/plans.php";
static String get insuranceSubscribe =>
"$server/driver_assurance/subscribe.php";
static String get insuranceCancel => "$server/driver_assurance/cancel.php";
}
+2 -2
View File
@@ -12,13 +12,13 @@ List<String> syrianDriverMessages = [
"رزقتك جاهزة: 💸 لا تخلّي الزباين يستنّوا، افتح التطبيق وخليك متأهّب.",
// --- التقدير والشكر ---
"شكراً إلك: 🙏 إنت مو بس سائق، إنت أساس نجاح تطبيق تريبز.",
"شكراً إلك: 🙏 إنت مو بس سائق، إنت أساس نجاح تطبيق سيرو.",
"نفتخر فيك: 🌟 الكباتن متلك هنن يلي رافعين اسمنا بالعالي.",
"أداء ممتاز: 👍 استمر بنفس الروح الحلوة، زباينك مبسوطين منك.",
"نجاح مشترك: 🗺️ كل توصيلة بتعملها بتكبر فيها شركتنا وانت كمان.",
// --- التحفيز والتشجيع ---
"صباح النشاط: ☀️ بلّش نهارك بطاقة إيجابية وتريبز لتكسب أكتر.",
"صباح النشاط: ☀️ بلّش نهارك بطاقة إيجابية وسيرو لتكسب أكتر.",
"كل مشوار فرصة: 🏁 لا توقف، الطريق لإلك والنجاح ناطرك.",
"يوم مربح: 💼 السوق ناشط اليوم، لا تفوّت الفرصة!",
"جاهز للطلب الجاي: 🔔 الزبون الجاي ممكن يكون أوفر ممتاز، خليك مستعد.",
+2 -2
View File
@@ -1,5 +1,5 @@
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/main.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/main.dart';
class RechargeTier {
final double amount;
+2 -2
View File
@@ -1,5 +1,5 @@
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/main.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/main.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'colors.dart';
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
@@ -1,4 +1,4 @@
import 'package:tripz_driver/constant/currency.dart';
import 'package:siro_driver/constant/currency.dart';
import 'dart:convert';
import 'dart:developer';
@@ -7,15 +7,15 @@ import 'package:flutter_contacts/contact.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/constant/links.dart';
import 'package:tripz_driver/controller/functions/crud.dart';
import 'package:tripz_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:tripz_driver/print.dart';
import 'package:tripz_driver/controller/functions/country_logic.dart';
import 'package:tripz_driver/main.dart';
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
import 'package:tripz_driver/views/widgets/mydialoug.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:siro_driver/print.dart';
import 'package:siro_driver/controller/functions/country_logic.dart';
import 'package:siro_driver/main.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/views/widgets/mydialoug.dart';
import 'package:share_plus/share_plus.dart';
import '../../firebase/local_notification.dart';
@@ -43,10 +43,10 @@ class InviteController extends GetxController {
Future<void> shareDriverCode() async {
if (driverCouponCode != null) {
final String shareText =
'''Join Tripz as a driver using my referral code!
'''Join Siro as a driver using my referral code!
Use code: $driverCouponCode
Download the Tripz Driver app now and earn rewards:
https://tripzmove.com/invite.php?code=$driverCouponCode&app=driver
Download the Siro Captain app now and earn rewards:
${AppLink.driverDownloadLinks}
''';
await Share.share(shareText);
}
@@ -54,10 +54,10 @@ https://tripzmove.com/invite.php?code=$driverCouponCode&app=driver
Future<void> sharePassengerCode() async {
if (couponCode != null) {
final String shareText = '''Get a discount on your first Tripz ride!
final String shareText = '''Get a discount on your first Siro ride!
Use my referral code: $couponCode
Download the Tripz app now and enjoy your ride:
https://tripzmove.com/invite.php?code=$couponCode&app=rider
Download the Siro app now and enjoy your ride:
https://siromove.com/invite.php?code=$couponCode&app=rider
''';
await Share.share(shareText);
}
@@ -66,62 +66,53 @@ https://tripzmove.com/invite.php?code=$couponCode&app=rider
@override
void onInit() {
super.onInit();
// **MODIFIED**: Sync contacts automatically on controller initialization.
syncContactsToServerOnce();
// ‏لا نقرأ دفتر الهاتف هنا. القراءة تحدث فقط حين يطلب الكابتن
// ‏اختيار جهة اتصال — انظر loadDeviceContacts أدناه.
// fetchDriverStats();
}
// --- NEW LOGIC: ONE-TIME CONTACTS SYNC ---
// --- قراءة جهات الاتصال محلياً، بطلب المستخدم وحده ---
/// **NEW**: Syncs all phone contacts to the server, but only runs once per user.
Future<void> syncContactsToServerOnce() async {
final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}';
// 1. Check if contacts have already been synced for this user.
if (box.read(syncFlagKey) == true) {
print("Contacts have already been synced for this user.");
return;
}
/// ‏كان هنا `syncContactsToServerOnce`: يقرأ دفتر هاتف الكابتن كاملاً
/// ‏تلقائياً عند فتح شاشة الدعوة، ويرفع **كل** اسم ورقم إلى
/// ‏`ride/egyptPhones/syrianAdd.php` — طلب لكل جهة اتصال.
///
/// ‏أُزيل الرفع بالكامل، ولثلاثة أسباب لا واحد:
///
/// ‏١) سياسة Google Play: رفع دفتر العناوين جملةً يتطلب إفصاحاً بارزاً
/// ‏وموافقة صريحة، وإذن النظام وحده لا يكفي. والتطبيق موقوف أصلاً.
///
/// ‏٢) البيانات ليست بيانات الكابتن ليتبرّع بها: هي أسماء وأرقام أطراف
/// ‏ثالثة لم تُستأذن ولا تعلم بوجود سيرو.
///
/// ‏٣) لا ميزة تعتمد عليها. الدعوة تعمل بالمشاركة أو باختيار جهة واحدة
/// ‏عمداً، وكلاهما لا يحتاج نسخة من الدفتر على السيرفر.
///
/// ‏ما بقي هنا قراءة محلية بحتة لعرض قائمة يختار منها الكابتن — لا
/// ‏شيء يغادر الجهاز.
Future<void> loadDeviceContacts() async {
if (contactMaps.isNotEmpty) return; // مُحمّلة سلفاً في هذه الجلسة
try {
// 2. Request permission and fetch all contacts.
if (await FlutterContacts.requestPermission(readonly: true)) {
// mySnackbarSuccess('Starting contacts sync in background...'.tr);
final List<Contact> allContacts =
await FlutterContacts.getContacts(withProperties: true);
// **FIX**: Assign fetched contacts to the class variable.
contacts = allContacts;
contactMaps.value = contacts.map((contact) {
return {
'name': contact.displayName,
'phones':
contact.phones.map((phone) => phone.normalizedNumber).toList(),
'emails': contact.emails.map((email) => email.address).toList(),
};
}).toList();
contactMaps.value = allContacts
.where((c) => c.phones.isNotEmpty)
.map((contact) => {
'name': contact.displayName,
'phones': contact.phones
.map((phone) => phone.normalizedNumber)
.toList(),
'emails':
contact.emails.map((email) => email.address).toList(),
})
.toList();
update();
// 3. Loop through contacts and save them to the server.
for (var contact in allContacts) {
if (contact.phones.isNotEmpty) {
// Use the normalized phone number for consistency.
var phone = contact.phones.first.normalizedNumber;
if (phone.isNotEmpty) {
CRUD().post(link: AppLink.savePhonesSyria, payload: {
"driverId": box.read(BoxName.driverID), // Associate with driver
"name": contact.displayName ?? 'No Name',
"phone": phone,
});
}
}
}
// 4. After a successful sync, set the flag to prevent future syncs.
await box.write(syncFlagKey, true);
// mySnackbarSuccess('Contacts sync completed successfully!'.tr);
}
} catch (e) {
// mySnackbarError('An error occurred during contact sync: $e'.tr);
// قراءة دفتر الهاتف ليست حرجة — الدعوة تعمل بالمشاركة أيضاً.
}
}
@@ -494,7 +485,7 @@ https://tripzmove.com/invite.php?code=$couponCode&app=rider
var d = response;
mySnackbarSuccess('Invite sent successfully'.tr);
String inviteCode = d['message']['inviteCode'].toString();
String message = '${'*Tripz DRIVER CODE*'.tr}\n\n'
String message = '${'*Siro DRIVER CODE*'.tr}\n\n'
'${"Use this code in registration".tr}\n'
'${"To get a gift for both".tr}\n\n'
'${"The period of this code is 24 hours".tr}\n\n'
@@ -539,7 +530,7 @@ https://tripzmove.com/invite.php?code=$couponCode&app=rider
mySnackbarSuccess('Invite sent successfully'.tr);
String inviteCode = d['message']['inviteCode'].toString();
String message = '${'*Tripz APP CODE*'.tr}\n\n'
String message = '${'*Siro APP CODE*'.tr}\n\n'
'${"Use this code in registration".tr}\n\n'
'${"To get a gift for both".tr}\n\n'
'${"The period of this code is 24 hours".tr}\n\n'
@@ -5,17 +5,17 @@ import 'package:crypto/crypto.dart';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:permission_handler/permission_handler.dart';
import 'package:tripz_driver/views/auth/syria/registration_view.dart';
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/views/auth/syria/registration_view.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/constant/links.dart';
import 'package:tripz_driver/controller/functions/crud.dart';
import 'package:tripz_driver/main.dart';
import 'package:tripz_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/main.dart';
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:location/location.dart';
import '../../../constant/api_key.dart';
@@ -55,7 +55,7 @@ class LoginDriverController extends GetxController {
void togglePasswordVisibility() {
isPasswordHidden = !isPasswordHidden;
update([
'passwordVisibility'
'passwordVisibility',
]); // Use a unique ID to only update the password field
}
@@ -110,8 +110,9 @@ class LoginDriverController extends GetxController {
}
var res = await CRUD().post(
link: AppLink.isPhoneVerified,
payload: {'phone_number': box.read(BoxName.phoneDriver)});
link: AppLink.isPhoneVerified,
payload: {'phone_number': box.read(BoxName.phoneDriver)},
);
if (res != 'failure') {
// Get.offAll(() => SyrianCardAI());
@@ -197,13 +198,14 @@ class LoginDriverController extends GetxController {
Log.print('Security check passed');
dev = Platform.isAndroid ? 'android' : 'ios';
var fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ??
var fingerPrint =
box.read(BoxName.deviceFingerprint)?.toString() ??
await DeviceHelper.getDeviceFingerprint();
var payload = {
'id': box.read(BoxName.driverID),
'password': AK.passnpassenger,
'aud': '${AK.allowedWallet}$dev',
'fingerPrint': fingerPrint
'fingerPrint': fingerPrint,
};
var response1 = await http.post(
Uri.parse(AppLink.loginJwtWalletDriver),
@@ -228,104 +230,148 @@ class LoginDriverController extends GetxController {
return '';
}
getJWT() async {
static Future<bool>? _jwtFuture;
static DateTime _jwtCooldownUntil = DateTime(2000);
static int _jwtFailures = 0;
static Duration get jwtCooldownRemaining {
final d = _jwtCooldownUntil.difference(DateTime.now());
return d.isNegative ? Duration.zero : d;
}
/// [force] يتجاوز الـ backoff cooldown — يُستخدم فقط في المسارات التي
/// يبدأها المستخدم (فتح التطبيق / تسجيل الدخول)، لا في الطلبات الخلفية،
/// حتى لا يعلق الكابتن خارج التطبيق بانتظار انتهاء الـ cooldown.
Future<bool> getJWT({bool force = false}) async {
if (force) {
_jwtCooldownUntil = DateTime(2000);
_jwtFailures = 0;
}
if (_jwtFuture != null) {
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
return _jwtFuture!;
}
_jwtFuture = _getJwtInternal().catchError((e) {
return _onJwtFailure('exception: $e');
});
try {
return await _jwtFuture!;
} finally {
_jwtFuture = null;
}
}
/// يفكّ payload الـ JWT محلياً (بدون التحقق من التوقيع) لفحص نوعه فقط.
static Map<String, dynamic>? decodeJwtPayload(String? token) {
if (token == null || token.isEmpty) return null;
try {
final parts = token.split('.');
if (parts.length != 3) return null;
String payload = parts[1];
switch (payload.length % 4) {
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
}
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
return decoded is Map ? Map<String, dynamic>.from(decoded) : null;
} catch (_) {
return null;
}
}
/// توكن تسجيل (`registration` / `new`): يصدره loginFirstTimeDriver قبل أن
/// يصير للسائق id. الباك إند يسمح له بمسارات التسجيل فقط، وأي endpoint
/// آخر يردّ 403 "Token not authorized for this action".
static bool isRegistrationToken(Map<String, dynamic> payload) {
final type = payload['token_type']?.toString();
return type == 'registration' ||
type == 'new' ||
payload['user_id']?.toString() == 'new';
}
Future<bool> _getJwtInternal() async {
await EncryptionHelper.initialize();
// 1. Check secure storage first to avoid redundant API calls
String? secureJwt = await storage.read(key: BoxName.jwt);
if (secureJwt != null && secureJwt.isNotEmpty) {
bool isTokenValid = false;
try {
final parts = secureJwt.split('.');
if (parts.length == 3) {
String payload = parts[1];
switch (payload.length % 4) {
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
}
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
if (exp != null) {
// Check if token is valid with a 30-second buffer
isTokenValid =
DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
}
}
} catch (_) {}
dev = Platform.isAndroid ? 'android' : 'ios';
final driverId = box.read(BoxName.driverID)?.toString() ?? '';
final isRegistering = driverId.isEmpty;
if (isTokenValid) {
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
return;
String? secureJwt = await storage.read(key: BoxName.jwt);
final cached = decodeJwtPayload(secureJwt);
if (cached != null) {
final exp = cached['exp'];
final notExpired = exp is int &&
DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
if (notExpired) {
// ═══════════════════════════════════════════════════════════
// الصلاحية الزمنية وحدها لا تكفي: توكن التسجيل يبقى صالحاً
// ساعة كاملة لكنه بلا صلاحيات. لو اكتفينا بفحص exp يبقى
// مخزّناً بعد اكتمال الدخول فيردّ كل endpoint محمي 403.
// ═══════════════════════════════════════════════════════════
if (isRegistrationToken(cached) && !isRegistering) {
Log.print(
'♻️ getJWT: توكن registration مع driverID موجود — ترقية إلى توكن driver.',
);
} else if (!isRegistering &&
cached['user_id'] != null &&
cached['user_id'].toString() != driverId) {
// توكن يخص سائقاً/جهازاً سابقاً على نفس التثبيت.
Log.print(
'♻️ getJWT: التوكن مُصدر لـ ${cached['user_id']} لا $driverId — إعادة توليد.',
);
} else {
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
return true;
}
}
}
dev = Platform.isAndroid ? 'android' : 'ios';
Log.print(
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = {
'id': box.read(BoxName.driverID) ?? AK.newId,
'password': AK.passnpassenger,
'aud': '${AK.allowed}$dev',
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
// Log.print('payload: ${payload}');
var response0 = await http.post(
Uri.parse(AppLink.loginFirstTimeDriver),
body: payload,
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
Log.print(
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.',
);
Log.print('response0: ${response0.body}');
Log.print('request: ${response0.request}');
if (response0.statusCode == 200) {
final decodedResponse1 = jsonDecode(response0.body);
Log.print('decodedResponse1: ${decodedResponse1}');
return false;
}
String? jwt;
if (decodedResponse1['message'] is Map &&
decodedResponse1['message']['jwt'] != null) {
jwt = decodedResponse1['message']['jwt'];
} else {
jwt = decodedResponse1['jwt'];
}
if (jwt != null) {
// box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
}
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
// await AppInitializer().getKey();
} else {}
if (isRegistering &&
box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
return _getRegistrationJwt();
} else {
await EncryptionHelper.initialize();
if (isRegistering) {
return _onJwtFailure('renew: لا يوجد driverID');
}
var payload = {
'id': box.read(BoxName.driverID),
'password': box.read(BoxName.emailDriver),
'aud': '${AK.allowed}$dev',
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
'fingerPrint':
box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
// print(payload);
var response1 = await http.post(
Uri.parse(AppLink.loginJwtDriver),
body: payload,
);
Log.print('response1.request: ${response1.request}');
Log.print('response1.body: ${response1.body}');
var response1 = await http
.post(Uri.parse(AppLink.loginJwtDriver), body: payload)
.timeout(const Duration(seconds: 30));
if (response1.statusCode == 429) {
final retryAfter =
int.tryParse(response1.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
return false;
}
if (response1.statusCode == 200) {
final decodedResponse1 = jsonDecode(response1.body);
// Log.print('decodedResponse1: ${decodedResponse1}');
String? jwt;
if (decodedResponse1['message'] is Map &&
decodedResponse1['message']['jwt'] != null) {
@@ -335,15 +381,98 @@ class LoginDriverController extends GetxController {
}
if (jwt != null) {
// await box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
needsDeviceVerification = false;
return _onJwtSuccess();
}
return _onJwtFailure('renew: لا يوجد jwt بالرد');
}
if (response1.statusCode == 401) {
// سائق لم يُكمل التسجيل بعد: عنده driverID من تحقق الهاتف لكن لا
// صفّ له في جدول driver، فـ loginJwtDriver يردّ 401 دائماً. هنا
// توكن التسجيل هو الصحيح — وبدون هذا الرجوع يعلق في منتصف التسجيل.
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
Log.print('↩️ getJWT: التسجيل لم يكتمل — الرجوع لتوكن التسجيل.');
return _getRegistrationJwt();
}
// await AppInitializer().getKey();
// سائق مكتمل: البصمة لا تطابق المخزّنة (جهاز جديد). الحل الوحيد
// OTP يعيد ربط البصمة — لا فائدة من إعادة المحاولة، فنرفع العلم
// ليحوّله loginDriver لصفحة التحقق.
needsDeviceVerification = true;
Log.print('🔐 getJWT: 401 من loginJwtDriver — الجهاز يحتاج تحقق OTP.');
}
return _onJwtFailure('renew: HTTP ${response1.statusCode}');
}
}
/// يُرفع عندما يرفض loginJwtDriver بصمة هذا الجهاز (401): السائق مسجّل
/// لكن الجهاز غير مربوط، ولا يمكن إصدار توكن driver قبل تحقق OTP.
static bool needsDeviceVerification = false;
/// توكن التسجيل — صالح لمسارات التسجيل وحدها (loginFromGoogle، register،
/// رفع المستندات…). يُستعمل قبل وجود صفّ للسائق، ويُستبدل بتوكن driver
/// فور اكتمال الدخول.
Future<bool> _getRegistrationJwt() async {
final payload = {
'id': box.read(BoxName.driverID) ?? AK.newId,
'password': AK.passnpassenger,
'aud': '${AK.allowed}$dev',
'fingerPrint':
box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
final response0 = await http
.post(Uri.parse(AppLink.loginFirstTimeDriver), body: payload)
.timeout(const Duration(seconds: 30));
if (response0.statusCode == 429) {
final retryAfter =
int.tryParse(response0.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
return false;
}
if (response0.statusCode == 200) {
final decodedResponse1 = jsonDecode(response0.body);
String? jwt;
if (decodedResponse1['message'] is Map &&
decodedResponse1['message']['jwt'] != null) {
jwt = decodedResponse1['message']['jwt'];
} else {
jwt = decodedResponse1['jwt'];
}
if (jwt != null) {
await storage.write(key: BoxName.jwt, value: jwt);
await EncryptionHelper.initialize();
return _onJwtSuccess();
}
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
}
return _onJwtFailure('firstTime: HTTP ${response0.statusCode}');
}
bool _onJwtSuccess() {
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
Log.print('✅ getJWT: تم توليد توكن جديد بنجاح.');
return true;
}
bool _onJwtFailure(String reason) {
_jwtFailures++;
final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures);
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds));
Log.print(
'❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث',
);
return false;
}
Future<void> getLocationPermission() async {
var status = await Permission.locationAlways.status;
if (!status.isGranted) {
@@ -357,9 +486,7 @@ class LoginDriverController extends GetxController {
Future updateInvitationCodeFromRegister() async {
var res = await CRUD().post(
link: AppLink.updateDriverInvitationDirectly,
payload: {
"inviterDriverPhone": box.read(BoxName.phoneDriver).toString(),
},
payload: {"inviterDriverPhone": box.read(BoxName.phoneDriver).toString()},
);
Log.print('invite: ${res}');
@@ -371,7 +498,11 @@ class LoginDriverController extends GetxController {
update();
box.write(BoxName.isInstall, '1');
NotificationController().showNotification(
"Code approved".tr, "Code approved".tr, 'tone2', '');
"Code approved".tr,
"Code approved".tr,
'tone2',
'',
);
try {
NotificationService.sendNotification(
@@ -389,6 +520,23 @@ class LoginDriverController extends GetxController {
}
}
/// جلسة محلية صالحة: عندنا driverID + رقم متحقق منه مسبقاً.
/// في هذه الحالة أي فشل شبكة/توكن هو عارض ولا يجوز إرجاع الكابتن لـ OTP.
bool get hasLocalVerifiedSession {
final id = box.read(BoxName.driverID)?.toString() ?? '';
return id.isNotEmpty &&
box.read(BoxName.phoneVerified).toString() == '1' &&
box.read(BoxName.firstTimeLoadKey).toString() == 'false';
}
/// نتائج عارضة من CRUD (ليست JSON) — شبكة/توكن/ضغط سيرفر.
static const _transientResults = {
'failure',
'no_internet',
'token_expired',
'rate_limited',
};
loginDriver(String driverID, email) async {
isloading = true;
update();
@@ -396,8 +544,47 @@ class LoginDriverController extends GetxController {
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
// await getJWT();
var res = await CRUD().get(
link: AppLink.loginFromGoogleCaptin, payload: {'driver_id': driverID});
link: AppLink.loginFromGoogleCaptin,
payload: {'driver_id': driverID},
);
Log.print('loginDriver: ${res}');
// ═══════════════════════════════════════════════════════════════
// فشل عارض (شبكة / توكن منتهي / rate limit):
// إذا الجهاز محفوظ والرقم متحقق ⇒ لا نرجع لـ OTP إطلاقاً.
// نجدّد التوكن مرة (متجاوزين الـ cooldown) ونعيد المحاولة مرة واحدة.
// ═══════════════════════════════════════════════════════════════
if (res is String && _transientResults.contains(res)) {
if (hasLocalVerifiedSession) {
Log.print(
'⚠️ loginDriver: نتيجة عارضة "$res" مع جلسة محلية صالحة — '
'إعادة محاولة بعد تجديد التوكن قسراً.',
);
final refreshed = await getJWT(force: true);
if (refreshed) {
res = await CRUD().get(
link: AppLink.loginFromGoogleCaptin,
payload: {'driver_id': driverID},
);
Log.print('loginDriver(retry): $res');
}
}
// ما زالت النتيجة عارضة
if (res is String && _transientResults.contains(res)) {
isloading = false;
update();
if (hasLocalVerifiedSession) {
// ندخله للتطبيق بجلسته المحفوظة بدل رميه على OTP.
mySnackbarError('Connection problem, please try again'.tr);
Get.offAll(() => HomeCaptain());
return true;
}
await isPhoneVerified();
return false;
}
}
if (res == 'failure') {
await isPhoneVerified();
isloading = false; // <--- أضفت هذا أيضاً
@@ -405,17 +592,46 @@ class LoginDriverController extends GetxController {
return false;
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
} else {
var jsonDecoeded = jsonDecode(res);
dynamic jsonDecoeded;
try {
jsonDecoeded = jsonDecode(res);
} catch (e) {
// رد غير JSON — لا نُسقط الجلسة بسببه.
Log.print('❌ loginDriver: رد غير صالح "$res" ($e)');
isloading = false;
update();
if (hasLocalVerifiedSession) {
Get.offAll(() => HomeCaptain());
return true;
}
await isPhoneVerified();
return false;
}
var d = jsonDecoeded['data'][0];
if (jsonDecoeded.isNotEmpty) {
if (jsonDecoeded['status'] == 'success' &&
d['is_verified'].toString() == '1') {
final driverStatus = d['status']?.toString() ?? '';
final isAccountBlocked =
driverStatus == 'blocked' ||
driverStatus == 'banned' ||
driverStatus == 'disabled';
final isVerifiedUser =
!isAccountBlocked &&
(d['is_verified'].toString() == '1' ||
d['is_test'].toString() == '1' ||
driverStatus == 'actives' ||
driverStatus == 'active');
if (jsonDecoeded['status'] == 'success' && isVerifiedUser) {
box.write(BoxName.emailDriver, d['email']);
box.write(BoxName.firstTimeLoadKey, 'false');
box.write(BoxName.driverID, (d['id']));
box.write(BoxName.isTest, '1');
box.write(
BoxName.isTest,
d['is_test']?.toString() == '1' ? '1' : '0',
);
box.write(BoxName.gender, (d['gender']));
box.write(BoxName.phoneVerified, d['is_verified'].toString());
box.write(BoxName.phoneVerified, '1');
box.write(BoxName.phoneDriver, (d['phone']));
box.write(BoxName.is_claimed, d['is_claimed']);
box.write(BoxName.isInstall, d['isInstall']);
@@ -426,9 +642,10 @@ class LoginDriverController extends GetxController {
box.write(BoxName.bankCodeDriver, (d['bankCode']));
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
box.write(
BoxName.nameDriver,
'${(d['first_name'])}'
' ${(d['last_name'])}');
BoxName.nameDriver,
'${(d['first_name'])}'
' ${(d['last_name'])}',
);
if (((d['model']).toString().contains('دراجه') ||
d['make'].toString().contains('دراجه '))) {
if ((d['gender']).toString() == 'Male') {
@@ -449,15 +666,39 @@ class LoginDriverController extends GetxController {
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
}
// ✅ الحصول على توكن access بدل registration قبل أي طلبات بعد تسجيل الدخول
// ═══════════════════════════════════════════════════════════
// ترقية التوكن: حتى هنا كنا نستعمل توكن registration (المسموح
// له بـ loginFromGoogle فقط). الآن صار عندنا driverID، فنستبدله
// بتوكن driver قبل أي طلب لاحق — بدون هذه الخطوة يردّ كل
// endpoint محمي 403 "Token not authorized for this action".
// ═══════════════════════════════════════════════════════════
Log.print('🔑 Getting access token after login...');
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
// await storage.write(
// key: BoxName.fingerPrint, value: fingerPrint.toString());
// await getJWT();
// Log.print('🔑 Access token obtained.');
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
final upgraded = await getJWT(force: true);
Log.print(
upgraded
? '🔑 Access token obtained.'
: '⚠️ تعذّر الحصول على توكن driver بعد الدخول.',
);
// add invitations
// جهاز جديد: البصمة غير مربوطة، فلا يمكن إصدار توكن driver إلا
// بعد OTP يعيد الربط. نحوّله مباشرة بدل إدخاله بتوكن بلا صلاحيات.
if (!upgraded && needsDeviceVerification) {
isloading = false;
update();
Get.offAll(
() => OtpVerificationPage(
phone: d['phone'].toString(),
deviceToken: fingerPrint,
token: '',
ptoken: '',
),
);
return true;
}
// add invitations
if (box.read(BoxName.isInstall) == null ||
box.read(BoxName.isInstall).toString() == '0') {
updateInvitationCodeFromRegister();
@@ -467,14 +708,13 @@ class LoginDriverController extends GetxController {
final driverStatus = d['status'].toString();
if (driverStatus != 'yet' && driverStatus != 'pending_review') {
var token = await CRUD().get(
link: AppLink.getDriverToken,
payload: {
'captain_id': (box.read(BoxName.driverID)).toString()
});
link: AppLink.getDriverToken,
payload: {'captain_id': (box.read(BoxName.driverID)).toString()},
);
// print(jsonDecode(token)['data'][0]['token'].toString());
// print(box.read(BoxName.tokenDriver).toString());
// if (box.read(BoxName.emailDriver).toString() !=
// '963992952235@tripzapp.com') {
// '963992952235@intaleqapp.com') {
if (token != 'failure') {
var serverData = jsonDecode(token);
final serverToken = serverData['data'][0]['token'].toString();
@@ -483,9 +723,19 @@ class LoginDriverController extends GetxController {
final serverFp = serverData['data'][0]['fingerPrint'].toString();
final tokenMismatch =
storedToken.isNotEmpty && serverToken != storedToken;
// البصمة تُخزَّن مُهشّمة (sha256 + FP_PEPPER) بعد أول تجديد
// توكن. التطبيق لا يملك الـ pepper فلا يستطيع مقارنتها —
// ومقارنتها خاماً تعطي "اختلاف" دائماً وتفرض OTP بلا سبب.
// في هذه الحالة نتّكل على تحقّق السيرفر عند إصدار التوكن
// (loginJwtDriver يردّ 401 لجهاز غير مربوط).
final isHashedFp = RegExp(r'^[a-f0-9]{64}$').hasMatch(serverFp);
final fingerprintMismatch = serverFp.isNotEmpty &&
!isHashedFp &&
serverFp != fingerPrint.toString();
Log.print(
'🔍 [FP_COMPARE] serverFp: $serverFp | localFp: ${fingerPrint.toString()} | tokenMismatch: $tokenMismatch | serverToken: $serverToken | storedToken: $storedToken');
if (tokenMismatch) {
'🔍 [FP_COMPARE] serverFp: $serverFp | localFp: ${fingerPrint.toString()} | fingerprintMismatch: $fingerprintMismatch | tokenMismatch: $tokenMismatch | serverToken: $serverToken | storedToken: $storedToken',
);
if (fingerprintMismatch || tokenMismatch) {
Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
@@ -504,8 +754,9 @@ class LoginDriverController extends GetxController {
phone: d['phone'].toString(),
deviceToken: fingerPrint.toString(),
token: token.toString(),
ptoken:
jsonDecode(token)['data'][0]['token'].toString(),
ptoken: jsonDecode(
token,
)['data'][0]['token'].toString(),
),
);
},
@@ -523,7 +774,8 @@ class LoginDriverController extends GetxController {
return true;
} else {
Get.offAll(
() => DriverVerificationScreen()); // افترض أن هذا الكلاس موجود
() => DriverVerificationScreen(),
); // افترض أن هذا الكلاس موجود
isloading = false; // <--- أضفت هذا
update(); // <--- أضفت هذا
return false;
@@ -552,7 +804,9 @@ class LoginDriverController extends GetxController {
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
var res = await CRUD().get(
link: AppLink.loginFromGoogleCaptin, payload: {'driver_id': driverID});
link: AppLink.loginFromGoogleCaptin,
payload: {'driver_id': driverID},
);
// print('res is $res');
// if (res == 'failure') {
@@ -583,9 +837,10 @@ class LoginDriverController extends GetxController {
box.write(BoxName.bankCodeDriver, (d['bankCode']));
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
box.write(
BoxName.nameDriver,
'${(d['first_name'])}'
' ${(d['last_name'])}');
BoxName.nameDriver,
'${(d['first_name'])}'
' ${(d['last_name'])}',
);
if (((d['model']).toString().contains('دراجه') ||
d['make'].toString().contains('دراجه '))) {
if ((d['gender']).toString() == 'Male') {
@@ -645,51 +900,80 @@ class LoginDriverController extends GetxController {
'aud': AppInformation.appName,
};
Log.print(
'🚀 loginUsingCredentialsWithoutGoogle Request URL: ${AppLink.loginUsingCredentialsWithoutGoogle}',
);
Log.print('📦 loginUsingCredentialsWithoutGoogle Payload: $payload');
var response = await http.post(
Uri.parse(AppLink.loginUsingCredentialsWithoutGoogle),
body: payload,
);
Log.print(
'✅ loginUsingCredentialsWithoutGoogle Status Code: ${response.statusCode}',
);
Log.print(
'📄 loginUsingCredentialsWithoutGoogle Response Body: ${response.body}',
);
if (response.statusCode == 200 || response.statusCode == 201) {
var jsonDecoeded = jsonDecode(response.body);
if (jsonDecoeded['status'] == 'success') {
var d = jsonDecoeded['data'][0];
var jwt = jsonDecoeded['jwt'];
// حفظ التوكن أولاً
// حفظ التوكن في FlutterSecureStorage فقط
if (jwt != null) {
box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
}
box.write(BoxName.emailDriver, (d['email']));
box.write(BoxName.driverID, (d['id']));
box.write(BoxName.isTest, '1');
box.write(BoxName.gender, (d['gender']));
box.write(BoxName.phoneVerified, d['is_verified'].toString());
final isVerifiedFlag =
(d['is_verified'].toString() == '1' ||
d['is_test'].toString() == '1' ||
d['status'].toString() == 'actives' ||
d['status'].toString() == 'active')
? '1'
: '0';
box.write(BoxName.phoneVerified, isVerifiedFlag);
box.write(BoxName.phoneDriver, (d['phone']));
box.write(BoxName.nameArabic, (d['name_arabic']));
box.write(BoxName.bankCodeDriver, (d['bankCode']));
box.write(BoxName.accountBankNumberDriver, d['accountBank']);
box.write(
BoxName.nameDriver,
'${(d['first_name'])}'
' ${(d['last_name'])}');
BoxName.nameDriver,
'${(d['first_name'])}'
' ${(d['last_name'])}',
);
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
Get.off(() => HomeCaptain());
} else {
mySnackbarError('Login failed'.tr);
Log.print('❌ Login failed message: ${jsonDecoeded['message']}');
mySnackbarError(
(jsonDecoeded['message'] ?? 'Login failed').toString().tr,
);
isloading = false;
update();
}
} else if (response.statusCode == 429) {
Log.print('❌ Too many attempts (429)');
mySnackbarError('Too many attempts. Please wait a minute.'.tr);
isloading = false;
update();
} else {
Log.print('❌ Server error response (Not 200/201/429)');
mySnackbarError('Server error'.tr);
isloading = false;
update();
}
} catch (e) {
Log.print('❌ Network/Exception error: $e');
mySnackbarError('Network error'.tr);
isloading = false;
update();
@@ -698,21 +982,26 @@ class LoginDriverController extends GetxController {
void loginByBoxData() async {
Get.to(() => HomeCaptain());
await CRUD().post(link: AppLink.addTokensDriver, payload: {
'token': box.read(BoxName.tokenDriver).toString(),
'captain_id': box.read(BoxName.driverID).toString()
});
await CRUD().post(
link: AppLink.addTokensDriver,
payload: {
'token': box.read(BoxName.tokenDriver).toString(),
'captain_id': box.read(BoxName.driverID).toString(),
},
);
CRUD().post(
link: "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
payload: {
'token': box.read(BoxName.tokenDriver),
'captain_id': box.read(BoxName.driverID).toString()
});
link: "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
payload: {
'token': box.read(BoxName.tokenDriver),
'captain_id': box.read(BoxName.driverID).toString(),
},
);
CRUD().post(
link: "${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
payload: {
'token': box.read(BoxName.tokenDriver),
'captain_id': box.read(BoxName.driverID).toString()
});
link: "${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
payload: {
'token': box.read(BoxName.tokenDriver),
'captain_id': box.read(BoxName.driverID).toString(),
},
);
}
}
@@ -4,8 +4,8 @@ import 'package:get/get.dart';
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:tripz_driver/constant/colors.dart';
import 'package:tripz_driver/controller/functions/llama_ai.dart';
import 'package:siro_driver/constant/colors.dart';
import 'package:siro_driver/controller/functions/llama_ai.dart';
// class CarRegistrationRecognizerController extends GetxController {
// @override
@@ -1,8 +1,8 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:tripz_driver/print.dart';
import 'package:tripz_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:siro_driver/print.dart';
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
@@ -10,6 +10,7 @@ import '../../../main.dart';
import '../../../views/widgets/error_snakbar.dart';
import '../../firebase/notification_service.dart';
import '../../functions/crud.dart';
import 'login_captin_controller.dart';
class OtpVerificationController extends GetxController {
final String phone;
@@ -95,6 +96,25 @@ class OtpVerificationController extends GetxController {
response != 'no_internet') {
Log.print('response (already decoded): ${response}');
// ═══════════════════════════════════════════════════════════
// التحقق نجح ⇒ السيرفر ربط بصمة هذا الجهاز وأصدر توكن driver.
// نحفظه فوراً: هو التوكن الوحيد الذي يملك صلاحيات بعد تغيير
// الجهاز، وبدونه تبقى كل الطلبات على 403.
// ═══════════════════════════════════════════════════════════
final issuedJwt = (response is Map && response['message'] is Map)
? response['message']['jwt']?.toString()
: null;
if (issuedJwt != null && issuedJwt.isNotEmpty) {
await storage.write(key: BoxName.jwt, value: issuedJwt);
LoginDriverController.needsDeviceVerification = false;
Log.print('🔑 تم حفظ توكن driver بعد تحقق تغيير الجهاز.');
} else {
// السيرفر لم يرجع توكناً — نطلب واحداً الآن بعد أن صارت
// البصمة مربوطة، وإلا دخل الكابتن بتوكن بلا صلاحيات.
LoginDriverController.needsDeviceVerification = false;
await Get.put(LoginDriverController()).getJWT(force: true);
}
// توجه إلى الصفحة التالية
await CRUD().post(
link: AppLink.addTokensDriver,
@@ -104,15 +124,19 @@ class OtpVerificationController extends GetxController {
'captain_id': box.read(BoxName.driverID).toString(),
});
await NotificationService.sendNotification(
target: ptoken.toString(),
title: 'token change'.tr,
body: 'token change'.tr,
isTopic: false,
tone: 'cancel',
driverList: [],
category: 'token change',
);
// إشعار الجهاز القديم — يُرسل فقط إذا عرفنا توكنه. عند دخول جهاز
// جديد لا نملك ptoken (تعذّر جلبه بلا صلاحيات) فنتخطّاه.
if (ptoken.isNotEmpty) {
await NotificationService.sendNotification(
target: ptoken,
title: 'token change'.tr,
body: 'token change'.tr,
isTopic: false,
tone: 'cancel',
driverList: [],
category: 'token change',
);
}
Get.offAll(() => HomeCaptain());
} else {
@@ -1,10 +1,11 @@
import 'package:get/get.dart';
import 'package:tripz_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:tripz_driver/controller/functions/crud.dart';
import 'package:tripz_driver/print.dart';
import 'package:tripz_driver/views/home/on_boarding_page.dart';
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
import 'package:tripz_driver/controller/functions/country_logic.dart';
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/print.dart';
import 'package:siro_driver/views/home/on_boarding_page.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/controller/functions/country_logic.dart';
import 'package:siro_driver/controller/functions/package_info.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
@@ -66,13 +67,18 @@ class PhoneAuthHelper {
try {
final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber);
Log.print('fixedPhone: $fixedPhone');
final fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ??
await DeviceHelper.getDeviceFingerprint();
final response = await CRUD().post(
link: _verifyOtpUrl,
payload: {
'phone_number': fixedPhone,
'token_code': otpCode,
'context': 'login',
'user_type': 'driver'
'user_type': 'driver',
// البصمة تُدمج داخل التوكن الذي يصدره السيرفر، وبدونها يُرفض
// كل طلب لاحق بـ 403 "Device verification required".
'fingerPrint': fingerPrint,
},
);
@@ -85,6 +91,14 @@ class PhoneAuthHelper {
box.write(BoxName.phoneDriver, phoneNumber);
box.write(BoxName.driverID, data['message']['driverID']);
// ✅ التحقق ينجح ⇒ السيرفر يصدر توكن driver كامل الصلاحيات.
// حفظه هنا هو ما يمنع بقاء توكن التسجيل مستعملاً بعد الدخول.
final issuedJwt = data['message']['jwt']?.toString();
if (issuedJwt != null && issuedJwt.isNotEmpty) {
await storage.write(key: BoxName.jwt, value: issuedJwt);
Log.print('🔑 تم حفظ توكن driver الصادر من التحقق بالـ OTP.');
}
if (isRegistered) {
// ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية
final driver = data['message']['driver'];
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/main.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/main.dart';
import '../../models/model/onboarding_model.dart';
import '../../views/auth/captin/login_captin.dart';
@@ -7,8 +7,8 @@ import 'package:image_picker/image_picker.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart';
import 'package:tripz_driver/constant/links.dart';
import 'package:tripz_driver/controller/firebase/notification_service.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/firebase/notification_service.dart';
import '../../../constant/box_name.dart';
import 'package:path_provider/path_provider.dart';
@@ -371,8 +371,7 @@ class RegistrationController extends GetxController {
final uri = Uri.parse(currentUrl);
final request = http.MultipartRequest('POST', uri);
final _jwt = box.read(BoxName.jwt);
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
final String _token = await storage.read(key: BoxName.jwt) ?? '';
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
String nonce = timestamp;
@@ -581,8 +580,8 @@ class RegistrationController extends GetxController {
try {
// ترويسات مشتركة
final _jwt = box.read(BoxName.jwt);
final bearer = 'Bearer ${_jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''}';
final String _jwtToken = await storage.read(key: BoxName.jwt) ?? '';
final bearer = 'Bearer $_jwtToken';
final hmac = '${box.read(BoxName.hmac)}';
String fingerPrint =
@@ -612,7 +611,7 @@ class RegistrationController extends GetxController {
_addField(fields, 'password', 'generated_password_or_token');
_addField(fields, 'status', 'yet');
String generatedEmail = 'driver_${box.read(BoxName.driverID) ?? DateTime.now().millisecondsSinceEpoch}@tripzmove.com';
String generatedEmail = 'driver_${box.read(BoxName.driverID) ?? DateTime.now().millisecondsSinceEpoch}@siromove.com';
_addField(fields, 'email', generatedEmail);
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
_addField(fields, 'country', box.read(BoxName.countryCode)?.toString() ?? 'Jordan');
@@ -764,7 +763,7 @@ class RegistrationController extends GetxController {
// isLoading.value = true;
// final uri = Uri.parse(
// 'https://tripz.xyz/tripz/auth/driver/register.php',
// 'https://intaleq.xyz/siro/auth/driver/register.php',
// );
// final client = http.Client();
@@ -12,14 +12,14 @@ import 'dart:io';
/// 6. Final Submission (register_driver_and_car.php → Gemini bulk AI → DB insert → FCM)
/// 7. Admin Review & Activation
///
/// Run: `dart run tripz_driver/lib/controller/auth/syria/registration_simulation.dart`
/// Run: `dart run siro_driver/lib/controller/auth/syria/registration_simulation.dart`
void main() async {
print('═══════════════════════════════════════════════════════');
print(' SYRIA DRIVER REGISTRATION SIMULATION');
print('═══════════════════════════════════════════════════════');
const host = 'api-syria.tripzmove.com';
const host = 'api-syria.siromove.com';
const protocol = 'https';
const driverId = 'DRV2026061912345678';
@@ -137,7 +137,7 @@ void main() async {
}
print(' ✅ AI extraction: $expectedExtraction');
print(' ✅ Image URL: $protocol://$host/tripz/auth/uploads/documents/driver_${docType}_$driverId.jpg');
print(' ✅ Image URL: $protocol://$host/siro/auth/uploads/documents/driver_${docType}_$driverId.jpg');
}
// ================================================================
@@ -163,7 +163,7 @@ void main() async {
print(' MIME validation: image/jpeg, image/png, image/webp');
print(' Max size: 5 MB');
final signedUrl = '$protocol://$host/tripz/secure_image.php'
final signedUrl = '$protocol://$host/siro/secure_image.php'
'?driver_id=$driverId'
'&doc_type=$docType'
'&ext=jpg'
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
class CarPlatformBridge {
CarPlatformBridge._();
static const _channel = MethodChannel('com.tripz.tripz_driver/car_navigation');
static const _channel = MethodChannel('com.siro.siro_driver/car_navigation');
static bool _isInitialized = false;
@@ -1,14 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:tripz_driver/controller/home/captin/home_captain_controller.dart';
import 'package:tripz_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
import 'package:tripz_driver/views/widgets/mydialoug.dart';
import 'package:tripz_driver/controller/voice_call_controller.dart';
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
import 'package:siro_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/views/widgets/mydialoug.dart';
import 'package:siro_driver/controller/voice_call_controller.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import '../../constant/box_name.dart';
import '../../constant/colors.dart';
@@ -233,7 +233,7 @@ class FirebaseMessagesController extends GetxController {
case 'token change':
case 'TOKEN_CHANGE':
box.remove(BoxName.jwt);
await storage.delete(key: BoxName.jwt);
break;
case 'face detect':
@@ -1,4 +1,4 @@
import 'package:tripz_driver/constant/currency.dart';
import 'package:siro_driver/constant/currency.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -15,8 +15,13 @@ import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart'; // للوصول لـ box
import '../../print.dart';
import '../../views/food_delivery/food_delivery_home_page.dart';
import '../../views/food_delivery/food_offer_page.dart';
import '../../views/food_delivery/food_task_details_page.dart';
import '../../views/home/Captin/driver_map_page.dart';
import '../../views/home/Captin/orderCaptin/order_request_page.dart';
import '../food_delivery/food_delivery_controller.dart';
import '../food_delivery/food_notification_service.dart';
import '../functions/crud.dart';
import '../home/captin/home_captain_controller.dart';
@@ -213,6 +218,14 @@ class NotificationController extends GetxController {
if (payload == null) return;
final payloadData = jsonDecode(payload) as Map<String, dynamic>;
// إشعارات وحدة التوصيل لها حمولة وأزرار خاصة بها — نفرزها قبل أي شيء
// لأن باقي هذه الدالة يفترض حمولة رحلة (مصفوفة add_ride.php).
if (payloadData.containsKey('food_event')) {
await _handleFoodNotificationResponse(response, payloadData);
return;
}
final rawData = payloadData['data'];
List<dynamic> listData = [];
@@ -248,6 +261,38 @@ class NotificationController extends GetxController {
}
}
// ── فرع وحدة التوصيل — مسار مستقل تماماً عن مسار الرحلات أعلاه ──────────
Future<void> _handleFoodNotificationResponse(
NotificationResponse response, Map<String, dynamic> payloadData) async {
final orderId = int.tryParse(payloadData['order_id']?.toString() ?? '') ?? 0;
if (orderId == 0) return;
await FoodNotificationService.instance.cancelOfferNotification();
// الكنترولر قد لا يكون مسجَّلاً (الضغط على الإشعار والتطبيق مغلق)، فنسجّله
// ليعيد بناء حالته من الخادم قبل عرض أي شاشة.
final controller = Get.isRegistered<FoodDeliveryController>()
? Get.find<FoodDeliveryController>()
: Get.put(FoodDeliveryController());
if (response.actionId == 'FOOD_ACCEPT') {
await controller.respondToOffer(orderId, true);
Get.to(() => FoodTaskDetailsPage(orderId: orderId));
return;
}
if (response.actionId == 'FOOD_REJECT') {
await controller.respondToOffer(orderId, false);
return;
}
if (payloadData['food_event'] == 'offer') {
Get.to(() => const FoodOfferPage(), fullscreenDialog: true);
} else {
Get.to(() => const FoodDeliveryHomePage());
}
}
// ==============================================================================
// 4. منطق القبول الآمن (Safe Accept Logic)
// ==============================================================================
@@ -1,7 +1,7 @@
import 'package:tripz_driver/constant/links.dart';
import 'package:tripz_driver/controller/functions/crud.dart';
import 'package:tripz_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:flutter/material.dart';
class OverlayContent1 extends StatelessWidget {
@@ -0,0 +1,388 @@
// food_delivery_controller.dart — حالة تبويب التوصيل (وضع التوصيل، العروض، المهام النشطة)
//
// مصادر الأحداث ثلاثة، مرتّبة بالأولوية:
// 1) سوكيت الطعام المستقل (FoodSocketService) — المسار اللحظي الأساسي.
// 2) polling لـ pending_offers.php — يعمل فقط حين ينقطع السوكيت (لا نُحمّل
// الخادم نداءً كل 4 ثوانٍ بينما القناة الحية شغّالة).
// 3) active.php — مصدر الحقيقة لحالة المهام، يُصحّح أي فارق بعد أي انقطاع.
import 'dart:async';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../../views/food_delivery/food_offer_page.dart';
import '../../views/widgets/error_snakbar.dart';
import '../voice_call_controller.dart';
import 'food_delivery_models.dart';
import 'food_delivery_service.dart';
import 'food_location_broadcaster.dart';
import 'food_notification_service.dart';
import 'food_overlay_bridge.dart';
import 'food_socket_service.dart';
class FoodDeliveryController extends GetxController {
bool isDeliveryModeEnabled = false;
bool isTogglingMode = false;
bool isSocketConnected = false;
List<FoodDeliveryOffer> pendingOffers = [];
List<FoodDeliveryTask> activeTasks = [];
bool isLoadingTasks = false;
int todayOrdersCount = 0;
int todayEarnings = 0;
final Set<int> _respondingOfferIds = {};
final Set<int> _busyTaskIds = {};
// معرّفات عروض عُرضت أو انتهت — يمنع تكرار فتح الشاشة حين يصل نفس العرض
// من السوكيت ومن polling معاً.
final Set<int> _handledOfferIds = {};
Timer? _pollTimer;
Timer? _countdownTimer;
final AudioPlayer _audioPlayer = AudioPlayer();
bool _isOfferScreenOpen = false;
@override
void onInit() {
super.onInit();
FoodNotificationService.instance.ensureChannels();
_bindSocketHandlers();
_bootstrap();
// نبضة خفيفة: المهام دائماً، والعروض فقط إذا كان السوكيت مقطوعاً
_pollTimer = Timer.periodic(const Duration(seconds: 8), (_) => _pollTick());
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) => _tickOffers());
}
Future<void> _bootstrap() async {
await refreshStatus();
await fetchActiveTasks();
}
// ── وضع التوصيل ────────────────────────────────────────────────────────
Future<void> refreshStatus() async {
final res = await FoodDeliveryService.getStatus();
if (!res.success || res.data == null) return;
final s = res.data!;
isDeliveryModeEnabled = s.deliveryModeEnabled;
box.write(BoxName.foodDeliveryModeEnabled, s.deliveryModeEnabled);
todayOrdersCount = s.todayOrdersCount;
todayEarnings = s.todayEarnings;
// الخادم هو مرجع الوضع — نوصل/نقطع السوكيت تبعاً له لا تبعاً لحالة الشاشة
if (isDeliveryModeEnabled) {
await FoodSocketService.instance.connect();
} else {
FoodSocketService.instance.disconnect();
pendingOffers.clear();
}
update();
}
Future<void> toggleDeliveryMode(bool enable) async {
isTogglingMode = true;
update();
final res = await FoodDeliveryService.toggleAvailability(enable);
isTogglingMode = false;
if (res.success) {
isDeliveryModeEnabled = enable;
// العزلة الخلفية تقرأ هذا المفتاح لتقرر فتح سوكيت الطعام أو إغلاقه،
// فيصل العرض حتى والتطبيق مغلق
box.write(BoxName.foodDeliveryModeEnabled, enable);
FlutterBackgroundService().invoke('foodModeChanged', {'enabled': enable});
if (enable) {
await FoodSocketService.instance.connect();
} else {
FoodSocketService.instance.disconnect();
pendingOffers.clear();
FoodNotificationService.instance.cancelOfferNotification();
}
} else {
mySnackbarWarning(res.message);
}
update();
}
// ── السوكيت ────────────────────────────────────────────────────────────
void _bindSocketHandlers() {
final socket = FoodSocketService.instance;
socket.onOffer = (payload) => _handleIncomingOffer(FoodDeliveryOffer.fromJson(payload));
socket.onOrderUpdate = _handleOrderUpdate;
socket.onIncomingCall = _handleIncomingCall;
socket.onConnectionChanged = (connected) {
isSocketConnected = connected;
// بعد أي إعادة اتصال نسحب الحالة من الخادم — السوكيت ناقل لا مخزن حالة
if (connected) {
fetchActiveTasks();
_fetchPendingOffers();
}
update();
};
}
void _handleOrderUpdate(Map<String, dynamic> payload) {
final status = payload['status']?.toString() ?? '';
final orderId = int.tryParse(payload['order_id']?.toString() ?? '') ?? 0;
if (status.startsWith('cancelled') && orderId != 0) {
FoodNotificationService.instance.showStatusNotification(
orderId: orderId,
title: 'أُلغي طلب التوصيل',
body: 'الطلب #$orderId أُلغي — لا حاجة لمتابعته',
);
mySnackbarWarning('أُلغي طلب التوصيل #$orderId');
}
fetchActiveTasks();
}
// ── المكالمة المقنّعة ───────────────────────────────────────────────────
// الزبون بادر بالاتصال: الحمولة لا تحمل رقماً ولا اسماً حقيقياً، فقط
// session_id ننضم به لنفس خادم الإشارات المستعمل في مكالمات الرحلات.
void _handleIncomingCall(Map<String, dynamic> payload) {
final sessionId = payload['session_id']?.toString() ?? '';
final orderId = payload['order_id']?.toString() ?? '';
if (sessionId.isEmpty || orderId.isEmpty) return;
// مسجَّل عالمياً بـ lazyPut(fenix) في main — والـ put احتياط لو أُزيل
final callController = _voiceCall();
callController.receiveCall(
sessionIdVal: sessionId,
remoteNameVal: payload['caller_name']?.toString() ?? 'زبون الطلب',
rideIdVal: 'food_$orderId',
);
}
/// السائق يتصل بالزبون — لا يرى رقمه ولا يُخزَّن عنده
Future<void> callCustomer(int orderId) async {
// مسجَّل عالمياً بـ lazyPut(fenix) في main — والـ put احتياط لو أُزيل
final callController = _voiceCall();
await callController.startCall(
rideIdVal: 'food_$orderId',
driverId: box.read(BoxName.driverID).toString(),
passengerId: '', // هوية الزبون لا تصل التطبيق — الخادم يحلّها من الطلب
remoteNameVal: 'زبون الطلب',
sessionEndpoint: '${AppLink.server}/food/courier/call_customer.php',
sessionPayload: {'order_id': orderId.toString()},
);
}
VoiceCallController _voiceCall() {
try {
return Get.find<VoiceCallController>();
} catch (_) {
return Get.put(VoiceCallController());
}
}
// ── العروض ─────────────────────────────────────────────────────────────
void _handleIncomingOffer(FoodDeliveryOffer offer) {
if (offer.orderId == 0 || offer.isExpired) return;
if (_handledOfferIds.contains(offer.orderId)) return;
if (pendingOffers.any((o) => o.orderId == offer.orderId)) return;
_handledOfferIds.add(offer.orderId);
pendingOffers.add(offer);
update();
_playOfferSound();
final isForeground = box.read(BoxName.isAppInForeground) ?? false;
if (isForeground == true) {
_openOfferScreen();
return;
}
// التطبيق في الخلفية: النافذة العائمة أولاً (أندرويد)، وإن تعذّرت —
// صلاحية ناقصة أو iOS — يبقى الإشعار ملء الشاشة هو المسار البديل.
_showBackgroundOffer(offer);
}
Future<void> _showBackgroundOffer(FoodDeliveryOffer offer) async {
final shown = await FoodOverlayBridge.show(offer);
if (!shown) {
FoodNotificationService.instance.showOfferNotification(offer);
}
}
void _openOfferScreen() {
if (_isOfferScreenOpen) return;
_isOfferScreenOpen = true;
Get.to(() => const FoodOfferPage(), fullscreenDialog: true)?.whenComplete(() {
_isOfferScreenOpen = false;
});
}
Future<void> _playOfferSound() async {
try {
await _audioPlayer.setAsset('assets/order1.wav');
await _audioPlayer.play();
} catch (e) {
Log.print('🍔 [FoodDelivery] تعذّر تشغيل نغمة العرض: $e');
}
}
// انتهاء المهلة يُدار محلياً للعرض فقط — الخادم يرفض أي قبول متأخر أصلاً
void _tickOffers() {
if (pendingOffers.isEmpty) return;
final before = pendingOffers.length;
pendingOffers.removeWhere((o) => o.isExpired);
if (pendingOffers.isEmpty && before > 0) {
FoodNotificationService.instance.cancelOfferNotification();
if (_isOfferScreenOpen && Get.currentRoute.contains('FoodOfferPage')) {
Get.back();
}
}
update();
}
Future<void> _pollTick() async {
// العروض عبر polling فقط عند انقطاع السوكيت — احتياط لا مسار أساسي
if (isDeliveryModeEnabled && !FoodSocketService.instance.isConnected) {
await _fetchPendingOffers();
}
await fetchActiveTasks(silent: true);
}
Future<void> _fetchPendingOffers() async {
final res = await FoodDeliveryService.getPendingOffers();
if (!res.success) return;
for (final offer in res.data ?? <FoodDeliveryOffer>[]) {
_handleIncomingOffer(offer);
}
}
Future<void> fetchActiveTasks({bool silent = false}) async {
if (!silent) {
isLoadingTasks = true;
update();
}
final res = await FoodDeliveryService.getActiveTasks();
isLoadingTasks = false;
if (res.success) activeTasks = res.data ?? [];
// البثّ نفسه يجري في LocationController (المصدر الوحيد للموقع) — نكتفي
// هنا بتسجيل المهام النشطة كي يستمر البثّ بعد إغلاق هذه الشاشة.
FoodLocationBroadcaster.setActiveOrders(activeTasks.map((t) => t.id).toList());
_syncRideAvailability();
update();
}
// ── مهمة واحدة في الوقت الواحد ──────────────────────────────────────────
// السائق أثناء التوصيل «مشغول» في نظام الرحلات تماماً كما لو كان في رحلة:
// نكتب statusDriverLocation='on' فينقله driver_socket من geo:drivers:available
// إلى geo:drivers:busy، فيتوقف توزيع الرحلات عليه — ويتوقف معه توزيع طلبات
// طعام إضافية، لأن مطابقة الطعام تتقاطع مع نفس المجموعة المتاحة.
// نعيده متاحاً بعد التسليم، وبشرطين: أن نكون نحن من شغَله، وألا تكون هناك
// رحلة جارية فعلاً (وإلا أفرغنا انشغالاً ليس لنا).
bool _markedBusyForDelivery = false;
void _syncRideAvailability() {
final hasDelivery = activeTasks.isNotEmpty;
if (hasDelivery && !_markedBusyForDelivery) {
_markedBusyForDelivery = true;
box.write(BoxName.statusDriverLocation, 'on');
Log.print('🍔 [FoodDelivery] السائق مشغول بتوصيل — أُوقف استقبال الرحلات');
return;
}
if (!hasDelivery && _markedBusyForDelivery) {
_markedBusyForDelivery = false;
final rideStatus = (box.read(BoxName.rideStatus) ?? '').toString();
final onRide = rideStatus == 'Begin' || rideStatus == 'Apply' || rideStatus == 'Arrived';
final blocked = box.read(BoxName.statusDriverLocation) == 'blocked';
if (!onRide && !blocked) {
box.write(BoxName.statusDriverLocation, 'off');
Log.print('🍔 [FoodDelivery] انتهى التوصيل — عاد استقبال الرحلات');
}
}
}
bool isRespondingToOffer(int orderId) => _respondingOfferIds.contains(orderId);
bool isTaskBusy(int orderId) => _busyTaskIds.contains(orderId);
Future<void> respondToOffer(int orderId, bool accept) async {
if (_respondingOfferIds.contains(orderId)) return;
_respondingOfferIds.add(orderId);
update();
final res = await FoodDeliveryService.respondToOffer(orderId, accept);
_respondingOfferIds.remove(orderId);
pendingOffers.removeWhere((o) => o.orderId == orderId);
FoodNotificationService.instance.cancelOfferNotification();
FoodOverlayBridge.hide();
if (!res.success) {
mySnackbarWarning(res.message);
} else if (accept) {
mySnackbarSuccess('تم قبول طلب التوصيل');
}
if (pendingOffers.isEmpty && _isOfferScreenOpen && Get.currentRoute.contains('FoodOfferPage')) {
Get.back();
}
update();
await fetchActiveTasks();
}
// ── خطوات المهمة ───────────────────────────────────────────────────────
Future<void> markPickedUp(int orderId) async {
if (_busyTaskIds.contains(orderId)) return;
_busyTaskIds.add(orderId);
update();
final res = await FoodDeliveryService.markPickedUp(orderId);
_busyTaskIds.remove(orderId);
if (res.success) {
mySnackbarSuccess('تم استلام الطلب من المطعم');
await fetchActiveTasks();
} else {
mySnackbarWarning(res.message);
}
update();
}
Future<void> markDelivered(int orderId) async {
if (_busyTaskIds.contains(orderId)) return;
_busyTaskIds.add(orderId);
update();
final res = await FoodDeliveryService.markDelivered(orderId);
_busyTaskIds.remove(orderId);
if (res.success) {
mySnackbarSuccess('تم تسليم الطلب بنجاح');
await fetchActiveTasks();
await refreshStatus(); // تحديث عدّاد أرباح اليوم فوراً بعد التسليم
} else {
mySnackbarWarning(res.message);
}
update();
}
@override
void onClose() {
_pollTimer?.cancel();
_countdownTimer?.cancel();
_audioPlayer.dispose();
// لا نقطع السوكيت هنا: وضع التوصيل قد يبقى مفعّلاً بعد إغلاق الشاشة،
// والقطع يتم فقط عند إطفاء الوضع صراحةً من التبديل.
super.onClose();
}
}
@@ -0,0 +1,323 @@
// food_delivery_models.dart — نماذج بيانات وحدة التوصيل (جهة السائق)
int _int(dynamic v) => int.tryParse(v?.toString() ?? '') ?? 0;
double? _doubleOrNull(dynamic v) => double.tryParse(v?.toString() ?? '');
double _double(dynamic v) => double.tryParse(v?.toString() ?? '') ?? 0;
DateTime? _date(dynamic v) => DateTime.tryParse(v?.toString() ?? '');
class FoodDeliveryTask {
final int id;
final String status; // courier_assigned | picked_up
final int merchantId;
final String merchantNameAr;
final double? merchantLat;
final double? merchantLng;
final String? merchantAddress;
final int deliveryFee;
final int itemsTotal;
final int serviceFee;
final int discount;
final int grandTotal;
final String paymentMethod; // wallet | cash
final int cashToCollect;
final int courierOwes;
final int itemsCount;
final String? customerNote;
final String? deliveryAddress;
final double deliveryLat;
final double deliveryLng;
final DateTime? createdAt;
final DateTime? readyAt;
final DateTime? courierAssignedAt;
final DateTime? pickedUpAt;
FoodDeliveryTask({
required this.id,
required this.status,
required this.merchantId,
required this.merchantNameAr,
required this.deliveryFee,
required this.deliveryLat,
required this.deliveryLng,
this.itemsTotal = 0,
this.serviceFee = 0,
this.discount = 0,
this.grandTotal = 0,
this.paymentMethod = 'wallet',
this.cashToCollect = 0,
this.courierOwes = 0,
this.itemsCount = 0,
this.customerNote,
this.merchantLat,
this.merchantLng,
this.merchantAddress,
this.deliveryAddress,
this.createdAt,
this.readyAt,
this.courierAssignedAt,
this.pickedUpAt,
});
bool get isPickedUp => status == 'picked_up';
bool get isCash => paymentMethod == 'cash';
factory FoodDeliveryTask.fromJson(Map<String, dynamic> j) => FoodDeliveryTask(
id: _int(j['id']),
status: j['status']?.toString() ?? '',
merchantId: _int(j['merchant_id']),
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
merchantLat: _doubleOrNull(j['merchant_lat']),
merchantLng: _doubleOrNull(j['merchant_lng']),
merchantAddress: j['merchant_address']?.toString(),
deliveryFee: _int(j['delivery_fee']),
itemsTotal: _int(j['items_total']),
serviceFee: _int(j['service_fee']),
discount: _int(j['discount']),
grandTotal: _int(j['grand_total']),
paymentMethod: j['payment_method']?.toString() ?? 'wallet',
cashToCollect: _int(j['cash_to_collect']),
courierOwes: _int(j['courier_owes']),
itemsCount: _int(j['items_count']),
customerNote: j['customer_note']?.toString(),
deliveryAddress: j['delivery_address']?.toString(),
deliveryLat: _double(j['delivery_lat']),
deliveryLng: _double(j['delivery_lng']),
createdAt: _date(j['created_at']),
readyAt: _date(j['ready_at']),
courierAssignedAt: _date(j['courier_assigned_at']),
pickedUpAt: _date(j['picked_up_at']),
);
}
// عرض توصيل وارد — يصل عبر سوكيت الطعام ('food_delivery_offer') وبنفس الحقول
// من courier/pending_offers.php عند انقطاع السوكيت (foodBuildCourierOfferPayload).
class FoodDeliveryOffer {
final int orderId;
final int merchantId;
final String merchantNameAr;
final String? merchantAddress;
final double? merchantLat;
final double? merchantLng;
final double deliveryLat;
final double deliveryLng;
final int deliveryFee;
final int itemsCount;
final String paymentMethod;
final int cashToCollect;
final int ttlSeconds;
final DateTime receivedAt;
final DateTime? offeredAt;
FoodDeliveryOffer({
required this.orderId,
required this.merchantId,
required this.merchantNameAr,
required this.deliveryFee,
this.merchantAddress,
this.merchantLat,
this.merchantLng,
this.deliveryLat = 0,
this.deliveryLng = 0,
this.itemsCount = 0,
this.paymentMethod = 'wallet',
this.cashToCollect = 0,
this.ttlSeconds = 20,
DateTime? receivedAt,
this.offeredAt,
}) : receivedAt = receivedAt ?? DateTime.now();
bool get isCash => paymentMethod == 'cash';
factory FoodDeliveryOffer.fromJson(Map<String, dynamic> j) => FoodDeliveryOffer(
orderId: _int(j['order_id']),
merchantId: _int(j['merchant_id']),
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
merchantAddress: j['merchant_address']?.toString(),
merchantLat: _doubleOrNull(j['merchant_lat']),
merchantLng: _doubleOrNull(j['merchant_lng']),
deliveryLat: _double(j['delivery_lat']),
deliveryLng: _double(j['delivery_lng']),
deliveryFee: _int(j['delivery_fee']),
itemsCount: _int(j['items_count']),
paymentMethod: j['payment_method']?.toString() ?? 'wallet',
cashToCollect: _int(j['cash_to_collect']),
ttlSeconds: _int(j['offer_ttl_seconds']) == 0 ? 20 : _int(j['offer_ttl_seconds']),
offeredAt: _date(j['offered_at']),
);
// العدّاد للعرض فقط — الخادم هو الحكم الفعلي ويرفض offer_respond بعد المهلة.
// نبدأ من offered_at إن جاءت (مسار polling)، وإلا من لحظة الاستلام (مسار السوكيت).
int get secondsRemaining {
final start = offeredAt ?? receivedAt;
final elapsed = DateTime.now().difference(start).inSeconds;
return (ttlSeconds - elapsed).clamp(0, ttlSeconds);
}
bool get isExpired => secondsRemaining <= 0;
}
// صنف داخل الطلب — يراه السائق ليتأكد من محتوى الكيس عند الاستلام من المطعم
class FoodOrderItem {
final String nameAr;
final int quantity;
final int unitPrice;
final int lineTotal;
FoodOrderItem({
required this.nameAr,
required this.quantity,
required this.unitPrice,
required this.lineTotal,
});
factory FoodOrderItem.fromJson(Map<String, dynamic> j) => FoodOrderItem(
nameAr: j['name_ar']?.toString() ?? '',
quantity: _int(j['quantity']),
unitPrice: _int(j['unit_price']),
lineTotal: _int(j['line_total']),
);
}
class FoodOrderDetails {
final FoodDeliveryTask task;
final List<FoodOrderItem> items;
final String? merchantPhone;
FoodOrderDetails({required this.task, required this.items, this.merchantPhone});
factory FoodOrderDetails.fromJson(Map<String, dynamic> j) => FoodOrderDetails(
task: FoodDeliveryTask.fromJson(j),
items: (j['items'] is List)
? (j['items'] as List)
.map((i) => FoodOrderItem.fromJson(Map<String, dynamic>.from(i)))
.toList()
: <FoodOrderItem>[],
merchantPhone: j['merchant_phone']?.toString(),
);
}
class FoodCourierStatus {
final bool deliveryModeEnabled;
final int activeOrdersCount;
final int todayOrdersCount;
final int todayEarnings;
FoodCourierStatus({
required this.deliveryModeEnabled,
this.activeOrdersCount = 0,
this.todayOrdersCount = 0,
this.todayEarnings = 0,
});
factory FoodCourierStatus.fromJson(Map<String, dynamic> j) => FoodCourierStatus(
deliveryModeEnabled: j['delivery_mode_enabled'] == true ||
j['delivery_mode_enabled']?.toString() == '1' ||
j['delivery_mode_enabled']?.toString() == 'true',
activeOrdersCount: _int(j['active_orders_count']),
todayOrdersCount: _int(j['today_orders_count']),
todayEarnings: _int(j['today_earnings']),
);
}
class FoodEarningsDay {
final String day;
final int ordersCount;
final int earnings;
FoodEarningsDay({required this.day, required this.ordersCount, required this.earnings});
factory FoodEarningsDay.fromJson(Map<String, dynamic> j) => FoodEarningsDay(
day: j['day']?.toString() ?? '',
ordersCount: _int(j['orders_count']),
earnings: _int(j['earnings']),
);
}
class FoodEarnings {
final int todayEarnings;
final int todayOrders;
final int weekEarnings;
final int weekOrders;
final int monthEarnings;
final int monthOrders;
final int totalEarnings;
final int totalOrders;
final int pendingCashOwed;
final List<FoodEarningsDay> daily;
FoodEarnings({
required this.todayEarnings,
required this.todayOrders,
required this.weekEarnings,
required this.weekOrders,
required this.monthEarnings,
required this.monthOrders,
required this.totalEarnings,
required this.totalOrders,
required this.pendingCashOwed,
required this.daily,
});
factory FoodEarnings.fromJson(Map<String, dynamic> j) => FoodEarnings(
todayEarnings: _int(j['today_earnings']),
todayOrders: _int(j['today_orders']),
weekEarnings: _int(j['week_earnings']),
weekOrders: _int(j['week_orders']),
monthEarnings: _int(j['month_earnings']),
monthOrders: _int(j['month_orders']),
totalEarnings: _int(j['total_earnings']),
totalOrders: _int(j['total_orders']),
pendingCashOwed: _int(j['pending_cash_owed']),
daily: (j['daily'] is List)
? (j['daily'] as List)
.map((d) => FoodEarningsDay.fromJson(Map<String, dynamic>.from(d)))
.toList()
: <FoodEarningsDay>[],
);
}
class FoodHistoryEntry {
final int id;
final String merchantNameAr;
final String? merchantAddress;
final int deliveryFee;
final int grandTotal;
final String paymentMethod;
final int itemsCount;
final int? rating;
final int? durationMinutes;
final DateTime? deliveredAt;
FoodHistoryEntry({
required this.id,
required this.merchantNameAr,
required this.deliveryFee,
this.merchantAddress,
this.grandTotal = 0,
this.paymentMethod = 'wallet',
this.itemsCount = 0,
this.rating,
this.durationMinutes,
this.deliveredAt,
});
factory FoodHistoryEntry.fromJson(Map<String, dynamic> j) => FoodHistoryEntry(
id: _int(j['id']),
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
merchantAddress: j['merchant_address']?.toString(),
deliveryFee: _int(j['delivery_fee']),
grandTotal: _int(j['grand_total']),
paymentMethod: j['payment_method']?.toString() ?? 'wallet',
itemsCount: _int(j['items_count']),
rating: j['rating'] == null ? null : _int(j['rating']),
durationMinutes: j['duration_minutes'] == null ? null : _int(j['duration_minutes']),
deliveredAt: _date(j['delivered_at']),
);
}
const int foodCurrencyDivisor = 1000;
String foodFormatPrice(int smallestUnit, {String currencySymbol = 'د.أ'}) {
final decimal = smallestUnit / foodCurrencyDivisor;
return '${decimal.toStringAsFixed(3)} $currencySymbol';
}
@@ -0,0 +1,157 @@
// food_delivery_service.dart — طبقة الاتصال بـ backend/food (جهة السائق/الموصّل)
import '../functions/crud.dart';
import '../../constant/links.dart';
import 'food_delivery_models.dart';
class FoodDeliveryApiResult<T> {
final bool success;
final T? data;
final String message;
FoodDeliveryApiResult(this.success, this.data, this.message);
}
class FoodDeliveryService {
static String get _base => '${AppLink.server}/food';
static Future<FoodDeliveryApiResult<bool>> toggleAvailability(bool enabled) async {
final res = await CRUD().post(
link: '$_base/courier/toggle_availability.php',
payload: {'enabled': enabled.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, enabled, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
/// حالة السائق عند فتح التبويب — وضع التوصيل مخزَّن في Redis على الخادم،
/// فلا يجوز افتراض "مغلق" محلياً وإلا ظهر التبديل مطفأً والعروض تصل فعلاً.
static Future<FoodDeliveryApiResult<FoodCourierStatus>> getStatus() async {
final res = await CRUD().post(link: '$_base/courier/status.php');
final msg = _payload(res);
if (msg != null) {
return FoodDeliveryApiResult(true, FoodCourierStatus.fromJson(msg), 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<List<FoodDeliveryTask>>> getActiveTasks() async {
final res = await CRUD().post(link: '$_base/courier/active.php');
final msg = _payload(res);
if (msg != null) {
return FoodDeliveryApiResult(true, _list(msg['orders'], FoodDeliveryTask.fromJson), 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<List<FoodDeliveryOffer>>> getPendingOffers() async {
final res = await CRUD().post(link: '$_base/courier/pending_offers.php');
final msg = _payload(res);
if (msg != null) {
return FoodDeliveryApiResult(true, _list(msg['offers'], FoodDeliveryOffer.fromJson), 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<FoodOrderDetails>> getOrderDetails(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/order_details.php',
payload: {'order_id': orderId.toString()},
);
final msg = _payload(res);
if (msg != null && msg['order'] is Map) {
return FoodDeliveryApiResult(
true, FoodOrderDetails.fromJson(Map<String, dynamic>.from(msg['order'])), 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<FoodEarnings>> getEarnings() async {
final res = await CRUD().post(link: '$_base/courier/earnings.php');
final msg = _payload(res);
if (msg != null) return FoodDeliveryApiResult(true, FoodEarnings.fromJson(msg), 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<List<FoodHistoryEntry>>> getHistory({
int limit = 20,
int offset = 0,
}) async {
final res = await CRUD().post(
link: '$_base/courier/history.php',
payload: {'limit': limit.toString(), 'offset': offset.toString()},
);
final msg = _payload(res);
if (msg != null) {
return FoodDeliveryApiResult(true, _list(msg['orders'], FoodHistoryEntry.fromJson), 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<String>> respondToOffer(int orderId, bool accept) async {
final res = await CRUD().post(
link: '$_base/courier/offer_respond.php',
payload: {'order_id': orderId.toString(), 'response': accept ? 'accept' : 'reject'},
);
final msg = _payload(res);
if (msg != null) {
return FoodDeliveryApiResult(true, msg['response']?.toString() ?? '', 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
/// بثّ موقع السائق للزبون أثناء المهمة. الخادم يرفضه بعد التسليم،
/// وينتهي آخر موقع من Redis تلقائياً — لا تتبّع خارج نافذة العمل.
static Future<void> sendLocation({
required int orderId,
required double lat,
required double lng,
double? heading,
}) async {
await CRUD().post(
link: '$_base/courier/location.php',
payload: {
'order_id': orderId.toString(),
'lat': lat.toString(),
'lng': lng.toString(),
if (heading != null) 'heading': heading.toStringAsFixed(0),
},
);
}
static Future<FoodDeliveryApiResult<void>> markPickedUp(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/picked_up.php',
payload: {'order_id': orderId.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<void>> markDelivered(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/delivered.php',
payload: {'order_id': orderId.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
// كل ردود backend/food تأتي بغلاف {status, message:{...}} — نستخرج المحتوى مرة واحدة
static Map<String, dynamic>? _payload(dynamic res) {
if (res is Map && res['status'] == 'success' && res['message'] is Map) {
return Map<String, dynamic>.from(res['message']);
}
return null;
}
static List<T> _list<T>(dynamic raw, T Function(Map<String, dynamic>) fromJson) {
if (raw is! List) return <T>[];
return raw.map((e) => fromJson(Map<String, dynamic>.from(e))).toList();
}
static String _errMsg(dynamic res) {
if (res == 'no_internet') return 'تحقق من اتصالك بالإنترنت';
if (res == 'token_expired') return 'انتهت الجلسة، حاول مجدداً';
if (res is Map && res['message'] is String) return res['message'];
return 'حدث خطأ، حاول مجدداً';
}
}
@@ -0,0 +1,79 @@
// food_location_broadcaster.dart — بثّ موقع السائق للزبون أثناء التوصيل
//
// لماذا هنا ولا في الخدمة الخلفية (background_service.dart):
// قاعدة معمارية مثبّتة في المشروع تمنع أي تدفّق GPS ثانٍ في العزلة الخلفية —
// `LocationController` هو المصدر الوحيد، وهو يبقى حياً في الخلفية أصلاً عبر
// enableBackgroundMode. فتح تدفّق ثانٍ هناك يعني مستمعَي GPS متوازيين واستنزاف
// بطارية وتجميد أجهزة (موثّق في driver_lifecycle.md). فالبثّ يركب على النبضة
// القائمة في LocationController بدل إنشاء مصدر جديد — وهو ما يجعله يستمر بعد
// إغلاق شاشة التوصيل، وهو المطلوب فعلاً.
//
// المهام النشطة تُحفظ في التخزين المحلي، فلا يحتاج هذا الملف كنترولر التوصيل
// ولا يموت بموته.
import 'dart:convert';
import '../../constant/box_name.dart';
import '../../main.dart';
import '../../print.dart';
import 'food_delivery_service.dart';
class FoodLocationBroadcaster {
FoodLocationBroadcaster._();
static const Duration _minInterval = Duration(seconds: 7);
static DateTime? _lastSentAt;
static bool _inFlight = false;
/// تُخزَّن من كنترولر التوصيل عند كل تحديث للمهام
static void setActiveOrders(List<int> orderIds) {
box.write(BoxName.foodActiveOrders, jsonEncode(orderIds));
}
static List<int> get activeOrders {
try {
final raw = box.read(BoxName.foodActiveOrders);
if (raw == null) return const [];
return (jsonDecode(raw.toString()) as List)
.map((e) => int.tryParse(e.toString()) ?? 0)
.where((e) => e != 0)
.toList();
} catch (_) {
return const [];
}
}
static void clear() => box.remove(BoxName.foodActiveOrders);
/// يُنادى من LocationController على كل تحديث موقع — يخرج فوراً إن لم تكن
/// هناك مهمة توصيل، فلا يكلّف شيئاً في الحالة الغالبة.
static Future<void> maybeSend({
required double lat,
required double lng,
double? heading,
}) async {
final orders = activeOrders;
if (orders.isEmpty) return;
if (lat == 0 && lng == 0) return;
if (_inFlight) return;
final now = DateTime.now();
if (_lastSentAt != null && now.difference(_lastSentAt!) < _minInterval) return;
_lastSentAt = now;
_inFlight = true;
try {
for (final orderId in orders) {
await FoodDeliveryService.sendLocation(
orderId: orderId,
lat: lat,
lng: lng,
heading: heading,
);
}
} catch (e) {
Log.print('🍔 [FoodLocation] تعذّر بثّ الموقع: $e');
} finally {
_inFlight = false;
}
}
}
@@ -0,0 +1,62 @@
// food_navigation.dart — تشغيل ملاحة سيرو الداخلية لمهام التوصيل
//
// لماذا النسخة نفسها (بلا tag) لا نسخة مستقلة للتوصيل:
// خطّ تحديث الموقع المركزي في LocationController يُغذّي نسخة واحدة فقط
// (Get.find<NavigationController> بلا وسم). نسخة موسومة للتوصيل ستُحرم من
// تحديثات الموقع فلا تعمل الملاحة أصلاً، وتغذيتها يستلزم تعديل مسار الموقع
// وهو المسار الأخطر في التطبيق. فنستعمل النسخة المشتركة، ونعالج التضارب
// الوحيد الممكن (سائق في ملاحة رحلة ثم يفتح مهمة توصيل) بسؤاله صراحةً
// بدل استبدال مساره صامتاً.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../views/widgets/error_snakbar.dart';
import '../home/navigation/navigation_controller.dart';
import '../home/navigation/navigation_view.dart';
class FoodNavigation {
/// يفتح ملاحة سيرو إلى [lat]/[lng]. يعيد false إذا ألغى السائق الاستبدال.
static Future<bool> startInApp({
required double lat,
required double lng,
required String title,
}) async {
final controller = Get.isRegistered<NavigationController>()
? Get.find<NavigationController>()
: Get.put(NavigationController());
if (controller.isNavigating) {
final replace = await Get.dialog<bool>(
AlertDialog(
title: const Text('ملاحة نشطة'),
content: Text('لديك ملاحة جارية. هل تستبدلها بالوجهة الجديدة؟\n$title'),
actions: [
TextButton(onPressed: () => Get.back(result: false), child: const Text('إبقاء الحالية')),
TextButton(onPressed: () => Get.back(result: true), child: const Text('استبدال')),
],
),
);
if (replace != true) return false;
}
Get.to(() => const NavigationView());
await controller.startNavigationTo(LatLng(lat, lng), infoWindowTitle: title);
return true;
}
/// خيار ثانوي لمن يفضّل جوجل — نفس ما يتيحه تدفّق الرحلات.
/// لا نمرّر في الرابط سوى الإحداثيات: لا اسم زبون ولا رقم طلب.
static Future<void> startExternal(double lat, double lng) async {
final nav = Uri.parse('google.navigation:q=$lat,$lng');
if (await canLaunchUrl(nav)) {
await launchUrl(nav);
return;
}
final web = Uri.parse('https://www.google.com/maps/dir/?api=1&destination=$lat,$lng');
if (!await launchUrl(web, mode: LaunchMode.externalApplication)) {
mySnackbarWarning('تعذّر فتح تطبيق الخرائط');
}
}
}
@@ -0,0 +1,156 @@
// food_notification_service.dart — إشعارات وحدة التوصيل، منفصلة كلياً عن إشعارات الرحلات
//
// الفصل مقصود على ثلاثة مستويات:
// 1) قناتان أندرويد خاصتان (food_delivery_offer_channel / food_delivery_status_channel)
// غير 'high_importance_channel' الخاص بالرحلات — فيقدر السائق يكتم أو يغيّر
// نغمة التوصيل من إعدادات النظام دون أن يمسّ تنبيه الرحلات إطلاقاً.
// 2) معرّفات إشعارات مستقلة (7001/7002) فلا يستبدل إشعار توصيل إشعارَ رحلة.
// 3) أزرار وحمولة خاصة ('FOOD_ACCEPT'/'FOOD_REJECT' + {"food_event": ...})
// يلتقطها فرع مستقل في handleNotificationResponse.
import 'dart:convert';
import 'dart:ui';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'food_delivery_models.dart';
class FoodNotificationService {
FoodNotificationService._();
static final FoodNotificationService instance = FoodNotificationService._();
// نفس نسخة الإضافة المستخدمة في NotificationController (singleton داخل الحزمة)،
// لكن بقنوات ومعرّفات خاصة بنا — لا نستدعي initialize هنا إطلاقاً حتى لا
// نستبدل معالِج النقر العام الذي هيّأه تطبيق الرحلات عند الإقلاع.
final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin();
static const String offerChannelId = 'food_delivery_offer_channel';
static const String statusChannelId = 'food_delivery_status_channel';
static const int offerNotificationId = 7001;
static const int statusNotificationId = 7002;
bool _channelsReady = false;
Future<void> ensureChannels() async {
if (_channelsReady) return;
final android = _plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
if (android == null) {
_channelsReady = true; // iOS — لا قنوات
return;
}
const offerChannel = AndroidNotificationChannel(
offerChannelId,
'عروض التوصيل',
description: 'تنبيه وصول عرض توصيل طلب طعام جديد',
importance: Importance.max,
playSound: true,
sound: RawResourceAndroidNotificationSound('order1'),
enableVibration: true,
);
const statusChannel = AndroidNotificationChannel(
statusChannelId,
'تحديثات طلبات التوصيل',
description: 'تغيّر حالة طلب توصيل قائم (جاهز، ملغى، إلخ)',
importance: Importance.high,
playSound: true,
);
await android.createNotificationChannel(offerChannel);
await android.createNotificationChannel(statusChannel);
_channelsReady = true;
}
/// إشعار عرض توصيل — ملء الشاشة (fullScreenIntent) كإشعار المكالمة، لأن مهلة
/// العرض 20 ثانية ولا تحتمل أن يمرّ السائق على شريط الإشعارات لاحقاً.
Future<void> showOfferNotification(FoodDeliveryOffer offer) async {
await ensureChannels();
final body = '${offer.merchantNameAr}\n'
'💰 أجرة التوصيل: ${foodFormatPrice(offer.deliveryFee)}'
'${offer.itemsCount > 0 ? ' | 🧾 ${offer.itemsCount} صنف' : ''}'
'${offer.cashToCollect > 0 ? '\n💵 تحصيل نقدي: ${foodFormatPrice(offer.cashToCollect)}' : ''}';
final androidDetails = AndroidNotificationDetails(
offerChannelId,
'عروض التوصيل',
importance: Importance.max,
priority: Priority.max,
fullScreenIntent: true,
category: AndroidNotificationCategory.call,
visibility: NotificationVisibility.public,
timeoutAfter: 20000, // يختفي مع انتهاء مهلة العرض نفسها
sound: const RawResourceAndroidNotificationSound('order1'),
audioAttributesUsage: AudioAttributesUsage.alarm,
color: const Color(0xFFFF6B35),
styleInformation: BigTextStyleInformation(
body,
contentTitle: '🍔 عرض توصيل جديد',
summaryText: foodFormatPrice(offer.deliveryFee),
),
actions: const <AndroidNotificationAction>[
AndroidNotificationAction('FOOD_ACCEPT', '✅ قبول',
showsUserInterface: true, titleColor: Color(0xFF4CAF50)),
AndroidNotificationAction('FOOD_REJECT', '❌ رفض',
cancelNotification: true, titleColor: Color(0xFFE53935)),
],
);
const iosDetails = DarwinNotificationDetails(
sound: 'order1.wav',
presentAlert: true,
presentBadge: true,
presentSound: true,
categoryIdentifier: 'FOOD_OFFER_CATEGORY',
interruptionLevel: InterruptionLevel.timeSensitive,
);
await _plugin.show(
id: offerNotificationId,
title: '🍔 عرض توصيل جديد',
body: '${offer.merchantNameAr} — ${foodFormatPrice(offer.deliveryFee)}',
notificationDetails:
NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: jsonEncode({
'food_event': 'offer',
'order_id': offer.orderId,
}),
);
}
Future<void> cancelOfferNotification() async {
await _plugin.cancel(id: offerNotificationId);
}
/// تحديث حالة مهمة قائمة (مثلاً إلغاء من النظام) — قناة أهدأ، بلا ملء شاشة
Future<void> showStatusNotification({
required int orderId,
required String title,
required String body,
}) async {
await ensureChannels();
const androidDetails = AndroidNotificationDetails(
statusChannelId,
'تحديثات طلبات التوصيل',
importance: Importance.high,
priority: Priority.high,
color: Color(0xFFFF6B35),
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
);
await _plugin.show(
id: statusNotificationId,
title: title,
body: body,
notificationDetails:
const NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: jsonEncode({'food_event': 'status', 'order_id': orderId}),
);
}
}
@@ -0,0 +1,92 @@
// food_overlay_bridge.dart — نافذة عرض التوصيل العائمة (أندرويد) ونتيجتها
//
// لماذا جسر مستقل عن كنترولر التوصيل:
// العرض قد يصل والتطبيق في الخلفية وشاشة التوصيل مغلقة — بل وقد يصل من العزلة
// الخلفية حيث لا وجود لأي كنترولر. فالاشتراك في نتيجة النافذة يجب أن يكون
// عالمياً ويعيش طوال عمر التطبيق، ويردّ على الخادم بنفسه بلا انتظار شاشة.
import 'dart:async';
import 'package:get/get.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
import '../../print.dart';
import '../../views/food_delivery/food_task_details_page.dart';
import '../../views/widgets/error_snakbar.dart';
import 'food_delivery_models.dart';
import 'food_delivery_service.dart';
class FoodOverlayBridge {
FoodOverlayBridge._();
static StreamSubscription<String>? _acceptSub;
static StreamSubscription<String>? _rejectSub;
/// تُنادى مرة واحدة عند إقلاع التطبيق
static void attach() {
if (!TripOverlayPlugin.isSupported) return; // أندرويد فقط
if (_acceptSub != null) return;
TripOverlayPlugin.initialize();
_acceptSub = TripOverlayPlugin.onFoodOrderAccepted.listen(_onAccepted);
_rejectSub = TripOverlayPlugin.onFoodOrderRejected.listen(_onRejected);
Log.print('🍔 [FoodOverlay] جاهز لاستقبال نتائج النافذة العائمة');
}
static Future<void> _onAccepted(String orderIdRaw) async {
final orderId = int.tryParse(orderIdRaw) ?? 0;
if (orderId == 0) return;
Log.print('🍔 [FoodOverlay] قبول من النافذة — طلب #$orderId');
// القبول يتم هنا لا في الشاشة: النافذة تُغلق فوراً والخادم لا ينتظر
// فتح واجهة. لو تأخّرنا حتى تُبنى الشاشة قد تنتهي مهلة العرض.
final res = await FoodDeliveryService.respondToOffer(orderId, true);
if (res.success) {
mySnackbarSuccess('تم قبول طلب التوصيل');
Get.to(() => FoodTaskDetailsPage(orderId: orderId));
} else {
mySnackbarWarning(res.message);
}
}
static Future<void> _onRejected(String orderIdRaw) async {
final orderId = int.tryParse(orderIdRaw) ?? 0;
if (orderId == 0) return;
// الرفض الصريح يُبلَّغ الخادم ليعرض الطلب على سائق آخر فوراً بدل انتظار
// انتهاء المهلة. انتهاء المهلة يصل بنفس الحدث، والخادم يتعامل مع الحالتين
// بأمان (يرفض إن لم يعد العرض قائماً).
Log.print('🍔 [FoodOverlay] رفض/انتهاء مهلة — طلب #$orderId');
await FoodDeliveryService.respondToOffer(orderId, false);
}
/// عرض النافذة لعرض توصيل وارد — تُنادى حين يكون التطبيق في الخلفية
static Future<bool> show(FoodDeliveryOffer offer, {String? distanceText}) {
return TripOverlayPlugin.showFoodOverlay(
FoodOrderOverlayData(
orderId: offer.orderId.toString(),
merchantName: offer.merchantNameAr,
merchantAddress: offer.merchantAddress ?? '',
// التنسيق يتم هنا لا في Kotlin — العملة والوحدة الصغرى منطق تطبيق
deliveryFeeText: foodFormatPrice(offer.deliveryFee),
cashToCollectText:
offer.cashToCollect > 0 ? foodFormatPrice(offer.cashToCollect) : '',
distanceText: distanceText ?? '',
itemsCount: offer.itemsCount,
isCash: offer.isCash,
),
autoCloseSeconds: offer.secondsRemaining.clamp(5, 20),
);
}
static Future<void> hide() => TripOverlayPlugin.hideOverlay();
static void detach() {
_acceptSub?.cancel();
_rejectSub?.cancel();
_acceptSub = null;
_rejectSub = null;
}
}
@@ -0,0 +1,169 @@
// food_socket_service.dart — قناة سوكيت مستقلة تماماً لوحدة التوصيل
//
// لماذا سوكيت منفصل ولا نركب على سوكيت الرحلات (LocationController.socket)؟
// • خادمان منفصلان فعلاً: food_socket.php (4040) مقابل driver_socket (2020)،
// وغرفة السائق هناك courier_food_{id} بمصادقة JWT مختلفة الاستعلام.
// • عزل الأعطال: سقوط سوكيت الطعام لا يقطع توزيع الرحلات، وهو المسار الحرج.
// • دورة حياة مختلفة: نتصل فقط أثناء «وضع التوصيل» وننفصل عند إطفائه،
// بينما سوكيت الرحلات يبقى حياً طوال مناوبة السائق.
import 'dart:async';
import 'dart:io';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
typedef FoodOfferHandler = void Function(Map<String, dynamic> payload);
typedef FoodOrderUpdateHandler = void Function(Map<String, dynamic> payload);
class FoodSocketService {
FoodSocketService._();
static final FoodSocketService instance = FoodSocketService._();
IO.Socket? _socket;
bool _isConnecting = false;
Timer? _heartbeat;
FoodOfferHandler? onOffer;
FoodOrderUpdateHandler? onOrderUpdate;
FoodOrderUpdateHandler? onIncomingCall;
void Function(bool connected)? onConnectionChanged;
bool get isConnected => _socket?.connected == true;
Future<void> connect() async {
if (isConnected || _isConnecting) return;
final driverId = box.read(BoxName.driverID)?.toString() ?? '';
final token = box.read(BoxName.tokenDriver)?.toString() ?? '';
if (driverId.isEmpty || token.isEmpty) {
Log.print('🍔 [FoodSocket] لا يوجد driverID/token — تخطّي الاتصال');
return;
}
_isConnecting = true;
_disposeSocket();
try {
_socket = IO.io(
AppLink.foodSocketUrl,
IO.OptionBuilder()
.setTransports(['websocket'])
// food_socket.php يتحقق: role=driver ويطابق user_id داخل الـ JWT مع id
.setQuery({
'role': 'driver',
'id': driverId,
'jwt': token,
'platform': Platform.isIOS ? 'ios' : 'android',
})
.enableForceNew()
.enableReconnection()
.setReconnectionDelay(2000)
.build(),
);
_bindListeners();
_socket!.connect();
} catch (e) {
_isConnecting = false;
Log.print('❌ [FoodSocket] فشل التهيئة: $e');
}
}
void _bindListeners() {
final s = _socket;
if (s == null) return;
s.onConnect((_) {
_isConnecting = false;
Log.print('✅ [FoodSocket] متصل — غرفة courier_food');
onConnectionChanged?.call(true);
_startHeartbeat();
});
s.onDisconnect((_) {
Log.print('⚠️ [FoodSocket] انقطع الاتصال');
onConnectionChanged?.call(false);
_stopHeartbeat();
});
s.onConnectError((e) {
_isConnecting = false;
Log.print('❌ [FoodSocket] خطأ اتصال: $e');
onConnectionChanged?.call(false);
});
s.onError((e) => Log.print('❌ [FoodSocket] خطأ: $e'));
// عرض توصيل جديد — الحمولة كاملة من foodBuildCourierOfferPayload،
// فشاشة العرض تُبنى فوراً بلا نداء HTTP إضافي (المهلة 20 ثانية فقط).
s.on('food_delivery_offer', (data) {
final map = _asMap(data);
if (map == null) return;
Log.print('🍔 [FoodSocket] عرض توصيل #${map['order_id']}');
onOffer?.call(map);
});
// مكالمة واردة من الزبون عبر القناة المقنّعة — لا رقم هاتف في الحمولة،
// فقط session_id ينضم به الطرفان لخادم الإشارات.
s.on('food_incoming_call', (data) {
final map = _asMap(data);
if (map == null) return;
Log.print('🍔 [FoodSocket] مكالمة واردة على الطلب #${map['order_id']}');
onIncomingCall?.call(map);
});
// تحديث حالة طلب قائم (المطعم ألغى، النظام ألغى، إلخ)
s.on('food_order_update', (data) {
final map = _asMap(data);
if (map == null) return;
Log.print('🍔 [FoodSocket] تحديث طلب #${map['order_id']} → ${map['status']}');
onOrderUpdate?.call(map);
});
}
// السوكيت ناقل إشعار لا مخزن حالة — أي حمولة غير مفهومة تُهمَل بصمت
// ويصحّح التطبيق نفسه من courier/active.php.
Map<String, dynamic>? _asMap(dynamic data) {
try {
if (data is Map) return Map<String, dynamic>.from(data);
if (data is List && data.isNotEmpty && data.first is Map) {
return Map<String, dynamic>.from(data.first as Map);
}
} catch (e) {
Log.print('❌ [FoodSocket] حمولة غير صالحة: $e');
}
return null;
}
void _startHeartbeat() {
_stopHeartbeat();
_heartbeat = Timer.periodic(const Duration(seconds: 25), (_) {
if (isConnected) _socket!.emit('heartbeat', {});
});
}
void _stopHeartbeat() {
_heartbeat?.cancel();
_heartbeat = null;
}
void _disposeSocket() {
if (_socket == null) return;
_socket!.clearListeners();
_socket!.dispose();
_socket = null;
}
void disconnect() {
_stopHeartbeat();
_isConnecting = false;
_socket?.disconnect();
_disposeSocket();
onConnectionChanged?.call(false);
Log.print('🍔 [FoodSocket] تم قطع الاتصال (وضع التوصيل مطفأ)');
}
}
@@ -67,8 +67,8 @@ class AppUpdateController extends GetxController {
void _showStoreUpdateDialog() {
final String storeUrl = Platform.isAndroid
? 'https://play.google.com/store/apps/details?id=com.tripz_driver'
: 'https://apps.apple.com/jo/app/tripz-driver/id6482995159';
? AppLink.driverAppStoreAndroid
: AppLink.driverAppStoreIOS;
Get.defaultDialog(
title: "تحديث جديد متوفر".tr,
@@ -11,6 +11,7 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
import 'package:get_storage/get_storage.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
import '../firebase/local_notification.dart';
const String notificationChannelId = 'driver_service_channel';
@@ -136,11 +137,172 @@ Future<bool> onStart(ServiceInstance service) async {
payload: jsonEncode(data));
}
});
// ══════════════════════════════════════════════════════════════════
// 🚫 الرحلة أخذها سائق آخر
//
// ‏نافذة الأوفرلي تعيش في عزلة منفصلة بلا سوكِت خاص بها، فلا تعلم
// ‏بشيء من نفسها. وهذه العزلة الخلفية هي الوحيدة التي تسمع الحدث
// ‏والتطبيق مغلق. بدون هذا المستمع تبقى النافذة معروضة وعدّادها
// ‏يعمل على رحلة لم تعد موجودة، ويضغط السائق "قبول" فيجدها مأخوذة.
//
// ‏نغلق النافذتين معاً: نافذة فلاتر (مسار السوكِت) والنافذة الأصلية
// ‏(مسار FCM) — لا نعرف أيّهما معروضة الآن.
// ══════════════════════════════════════════════════════════════════
socket.on('ride_taken', (data) async {
try {
await GetStorage.init();
final box = GetStorage();
final String myDriverId = box.read(BoxName.driverID).toString();
final String whoTookIt = data?['taken_by_driver_id']?.toString() ?? '';
// ‏لا نغلق على من فاز بها — تطبيقه في طريقه لفتح شاشة الرحلة.
if (whoTookIt == myDriverId) return;
bool closedAny = false;
if (Platform.isAndroid) {
if (await Overlay.FlutterOverlayWindow.isActive()) {
await Overlay.FlutterOverlayWindow.closeOverlay();
closedAny = true;
}
if (await TripOverlayPlugin.isOverlayActive()) {
await TripOverlayPlugin.hideOverlay();
closedAny = true;
}
}
// ‏الإشعار فقط إن كان هناك ما يُغلق فعلاً — وإلا أزعجنا سائقاً
// ‏لم يُعرض عليه شيء أصلاً (الحدث يُبثّ للجميع).
if (closedAny) {
flutterLocalNotificationsPlugin.show(
id: 1003,
title: "الطلب لم يعد متاحاً",
body: "قام سائق آخر بقبول هذه الرحلة",
notificationDetails: const NotificationDetails(
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: false,
presentSound: false,
),
),
payload: '');
}
} catch (e) {
print("⚠️ ride_taken handler error: $e");
}
});
}
// ══════════════════════════════════════════════════════════════════════
// سوكيت الطعام في العزلة الخلفية — مستقل تماماً عن سوكيت الرحلات أعلاه
// (خادم وبورت وغرفة مختلفة). بدونه لا يصل عرض توصيل والتطبيق مغلق، لأن
// FoodSocketService يعيش في العزلة الرئيسية التي يوقفها أندرويد.
// نفتحه فقط إن كان وضع التوصيل مفعّلاً — لا اتصال دائم بلا داعٍ.
// ══════════════════════════════════════════════════════════════════════
IO.Socket? foodSocket;
void connectFoodSocket() {
if (foodSocket != null || driverId.isEmpty) return;
foodSocket = IO.io(
AppLink.foodSocketUrl,
IO.OptionBuilder()
.setTransports(['websocket'])
.setQuery({
'role': 'driver',
'id': driverId,
'jwt': token,
'platform': Platform.isIOS ? 'ios' : 'android',
})
.enableReconnection()
.setReconnectionDelay(3000)
.build());
foodSocket!.onConnect((_) => print('🍔 Background: food socket connected'));
foodSocket!.on('food_delivery_offer', (data) async {
print('🍔 Background: food delivery offer received');
await GetStorage.init();
final box = GetStorage();
// الواجهة أمام السائق تتكفّل بالعرض إن كان التطبيق مفتوحاً
final bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
if (isAppInForeground) return;
Map<String, dynamic>? payload;
if (data is Map) {
payload = Map<String, dynamic>.from(data);
} else if (data is List && data.isNotEmpty && data.first is Map) {
payload = Map<String, dynamic>.from(data.first as Map);
}
if (payload == null) return;
if (Platform.isAndroid) {
final shown = await TripOverlayPlugin.showFoodOverlay(
FoodOrderOverlayData(
orderId: payload['order_id'].toString(),
merchantName: payload['merchant_name_ar']?.toString() ?? 'مطعم',
merchantAddress: payload['merchant_address']?.toString() ?? '',
deliveryFeeText: _formatFoodPrice(payload['delivery_fee']),
cashToCollectText: _formatFoodPrice(payload['cash_to_collect']),
distanceText: '',
itemsCount: int.tryParse(payload['items_count']?.toString() ?? '') ?? 0,
isCash: payload['payment_method']?.toString() == 'cash',
),
autoCloseSeconds: 20,
);
if (shown) return;
}
// iOS أو تعذّرت النافذة — إشعار على قناة التوصيل الخاصة
flutterLocalNotificationsPlugin.show(
id: 7001,
title: '🍔 عرض توصيل جديد',
body: '${payload['merchant_name_ar'] ?? 'مطعم'} — '
'${_formatFoodPrice(payload['delivery_fee'])}',
notificationDetails: const NotificationDetails(
android: AndroidNotificationDetails(
'food_delivery_offer_channel',
'عروض التوصيل',
importance: Importance.max,
priority: Priority.max,
fullScreenIntent: true,
category: AndroidNotificationCategory.call,
),
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: jsonEncode({'food_event': 'offer', 'order_id': payload['order_id']}),
);
});
}
if (box.read(BoxName.foodDeliveryModeEnabled) == true) {
connectFoodSocket();
}
// تفعيل/إطفاء وضع التوصيل من الواجهة ينعكس هنا بلا إعادة تشغيل الخدمة
service.on('foodModeChanged').listen((event) {
final enabled = event?['enabled'] == true;
if (enabled) {
connectFoodSocket();
} else {
foodSocket?.clearListeners();
foodSocket?.dispose();
foodSocket = null;
}
});
service.on('stopService').listen((event) {
socket?.clearListeners();
socket?.dispose();
foodSocket?.clearListeners();
foodSocket?.dispose();
service.stopSelf();
});
@@ -185,6 +347,14 @@ Future<bool> onStart(ServiceInstance service) async {
return true;
}
// نفس قاعدة foodFormatPrice في نماذج الطعام — مكرّرة هنا عمداً لأن العزلة
// الخلفية يجب أن تبقى خفيفة الاعتماديات ولا تسحب طبقة النماذج كاملة.
String _formatFoodPrice(dynamic smallestUnit) {
final v = int.tryParse(smallestUnit?.toString() ?? '') ?? 0;
if (v == 0) return '';
return '${(v / 1000).toStringAsFixed(3)} د.أ';
}
class BackgroundServiceHelper {
static Future<void> initialize() async {
final service = FlutterBackgroundService();
@@ -5,9 +5,9 @@ import 'dart:io';
import 'package:camera/camera.dart';
import 'package:get/get.dart';
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/constant/links.dart';
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http;
@@ -1,10 +1,10 @@
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/constant/country_polygons.dart';
import 'package:tripz_driver/constant/city_polygons.dart';
import 'package:tripz_driver/main.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/country_polygons.dart';
import 'package:siro_driver/constant/city_polygons.dart';
import 'package:siro_driver/main.dart';
import '../../print.dart';
class CountryLogic {
+101 -73
View File
@@ -1,17 +1,17 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:tripz_driver/controller/functions/encrypt_decrypt.dart';
import 'package:tripz_driver/controller/functions/network/net_guard.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/constant/links.dart';
import 'package:tripz_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:tripz_driver/main.dart';
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
import 'package:siro_driver/controller/functions/network/net_guard.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:siro_driver/main.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:tripz_driver/env/env.dart';
import 'package:tripz_driver/print.dart';
import 'package:tripz_driver/controller/functions/package_info.dart';
import 'package:siro_driver/env/env.dart';
import 'package:siro_driver/print.dart';
import 'package:siro_driver/controller/functions/package_info.dart';
import '../../constant/api_key.dart';
import '../../views/widgets/error_snakbar.dart';
@@ -22,38 +22,48 @@ import 'ssl_pinning.dart';
class CRUD {
final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
// ── فحص صلاحية JWT بدون مكتبات خارجية ──────────────────────
static bool _isJwtValid(String? token) {
if (token == null || token.isEmpty) return false;
try {
final parts = token.split('.');
if (parts.length != 3) return false;
// فك تشفير الـ payload (الجزء الثاني)
String payload = parts[1];
// إضافة padding للـ base64
switch (payload.length % 4) {
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
}
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
if (exp == null) return false;
// نعتبر التوكن منتهي قبل 30 ثانية من انتهاء الصلاحية (buffer)
return DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
} catch (_) {
final decoded = LoginDriverController.decodeJwtPayload(token);
if (decoded == null) return false;
final exp = decoded['exp'];
if (exp is! int) return false;
// نعتبر التوكن منتهي قبل 30 ثانية من انتهاء الصلاحية (buffer)
if (DateTime.now().millisecondsSinceEpoch >= (exp * 1000 - 30000)) {
return false;
}
// توكن التسجيل صالح زمنياً لكن الباك إند يرفضه بـ 403 على أي endpoint
// محمي. ما دام عندنا driverID فالتسجيل انتهى ويلزمنا توكن driver.
if (LoginDriverController.isRegistrationToken(decoded) &&
(box.read(BoxName.driverID)?.toString().isNotEmpty ?? false)) {
return false;
}
return true;
}
/// الباك إند يردّ 403 بهذه الرسالة تحديداً عندما يكون التوكن من نوع
/// registration على endpoint محمي — وهي الحالة الوحيدة من حالات الـ 403
/// التي يحلّها تجديد التوكن (بقيّتها: بصمة جهاز / HMAC / سلامة التطبيق).
static bool _isUnauthorizedTokenType(String body) =>
body.contains('Token not authorized for this action');
/// endpoints عامة لا يتحقق فيها الباك إند من التوكن (طلب/تحقق OTP،
/// تسجيل الدخول، التسجيل، تسجيل الأخطاء). حجب الطلب عنها عند غياب
/// التوكن يقفل بالضبط المسار الذي يستعيد به التطبيق توكناً صالحاً —
/// وهو ما كان يترك جهازاً جديداً عالقاً بلا مخرج.
static bool _isPublicEndpoint(String link) =>
link.contains('/auth/otp/') ||
link.contains('login') ||
link.contains('register') ||
link.contains('errorApp.php');
static Future<void> addError(
String error, String details, String where) async {
try {
@@ -107,6 +117,18 @@ class CRUD {
}
}
// ═══════════════════════════════════════════════════════════════
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
// ═══════════════════════════════════════════════════════════════
Future<String> _ensureJwt() async {
String token = await _getJwt();
if (_isJwtValid(token)) return token;
final ok = await Get.put(LoginDriverController()).getJWT();
if (!ok) return '';
return await _getJwt();
}
// ═══════════════════════════════════════════════════════════════
// _makeRequest — دالة مركزية لكل الطلبات
// ───────────────────────────────────────────────────────────────
@@ -119,6 +141,7 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
bool allowRefresh = true,
}) async {
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
const totalTimeout = Duration(seconds: 60);
@@ -186,19 +209,34 @@ class CRUD {
}
}
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
if (sc == 401) {
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES-$requestId] 429 rate limited — $link');
return 'rate_limited';
}
// 401 (منتهي) أو 403 بتوكن تسجيل (بلا صلاحيات) →
// تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
if (sc == 401 || (sc == 403 && _isUnauthorizedTokenType(body))) {
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
if (isNonCritical || !allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginDriverController()).getJWT();
if (!refreshed) return 'token_expired';
final newToken = await _getJwt();
if (newToken.isEmpty) return 'token_expired';
// إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
final retryHeaders = Map<String, String>.from(headers)
..['Authorization'] = 'Bearer $newToken';
return await _makeRequest(
link: link,
payload: payload,
headers: retryHeaders,
allowRefresh: false,
);
}
// 5xx
@@ -218,17 +256,10 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
String token = await _ensureJwt();
if (token.isEmpty && !_isPublicEndpoint(link)) {
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
return 'token_expired';
}
final headers = {
@@ -247,19 +278,11 @@ class CRUD {
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
bool allowRefresh = true,
}) async {
try {
// فحص صلاحية التوكن قبل الإرسال
String token = await _getJwt();
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
}
final String token = await _ensureJwt();
var url = Uri.parse(link);
var response = await _client.post(
@@ -279,16 +302,21 @@ class CRUD {
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') return response.body;
return jsonData['status'];
} else if (response.statusCode == 401) {
if (!_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
} else if (response.statusCode == 401 ||
(response.statusCode == 403 &&
_isUnauthorizedTokenType(response.body))) {
// تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
// (allowRefresh: false تمنع أي تكرار إضافي) — نفس سلوك _makeRequest.
if (!allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginDriverController()).getJWT();
if (!refreshed) return 'token_expired';
return await get(
link: link,
payload: payload,
allowRefresh: false,
);
} else if (response.statusCode >= 500) {
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
url.toString());
@@ -1,8 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:tripz_driver/constant/api_key.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/main.dart';
import 'package:siro_driver/constant/api_key.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/main.dart';
import 'package:http/http.dart' as http;
import '../../constant/links.dart';
@@ -4,18 +4,18 @@ import 'dart:io';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:tripz_driver/constant/box_name.dart';
import 'package:tripz_driver/constant/info.dart';
import 'package:tripz_driver/constant/links.dart';
import 'package:tripz_driver/constant/style.dart';
import 'package:tripz_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:tripz_driver/controller/firebase/firbase_messge.dart';
import 'package:tripz_driver/controller/firebase/local_notification.dart';
import 'package:tripz_driver/controller/functions/crud.dart';
import 'package:tripz_driver/env/env.dart';
import 'package:tripz_driver/main.dart';
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
import 'package:tripz_driver/views/widgets/mydialoug.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/info.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/constant/style.dart';
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:siro_driver/controller/firebase/firbase_messge.dart';
import 'package:siro_driver/controller/firebase/local_notification.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/env/env.dart';
import 'package:siro_driver/main.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/views/widgets/mydialoug.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
@@ -479,8 +479,7 @@ class AI extends GetxController {
bool isLoadingVehicleFrontSy = false;
bool isLoadingVehicleBackSy = false;
Future<void> sendToAI(String type, {required File imageFile}) async {
final _jwt = box.read(BoxName.jwt);
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
final String _token = await storage.read(key: BoxName.jwt) ?? '';
final headers = {
'Authorization': 'Bearer $_token',
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',

Some files were not shown because too many files have changed in this diff Show More