Update codebase
@@ -0,0 +1,2 @@
|
|||||||
|
#Sun Jun 21 02:56:57 EET 2026
|
||||||
|
gradle.version=8.13
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#Sun Jun 21 02:56:49 EET 2026
|
||||||
|
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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" }
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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" "$@"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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")
|
||||||
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 561 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 149 KiB |
@@ -129,4 +129,18 @@ class BoxName {
|
|||||||
static const String isBusMode = 'isBusMode';
|
static const String isBusMode = 'isBusMode';
|
||||||
static const String busModeTripId = 'busModeTripId';
|
static const String busModeTripId = 'busModeTripId';
|
||||||
static const String busModeRouteId = 'busModeRouteId';
|
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) {
|
static String getRoutingApiUrl(String countryName) {
|
||||||
switch (countryName) {
|
switch (countryName) {
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://routec.tripz.xyz/route-jo';
|
return 'https://routec.intaleq.xyz/route-jo';
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://routec.tripz.xyz/route';
|
return 'https://routec.intaleq.xyz/route';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://routec.tripz.xyz/route-eg';
|
return 'https://routec.intaleq.xyz/route-eg';
|
||||||
default:
|
default:
|
||||||
// الافتراضي في حالة لم يقع الموقع ضمن أي من المضلعات
|
// الافتراضي في حالة لم يقع الموقع ضمن أي من المضلعات
|
||||||
return 'https://routec.tripz.xyz/route';
|
return 'https://routec.intaleq.xyz/route';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class CountryPolygons {
|
|||||||
if (_isPointInPolygon(point, syriaBoundary)) return "Syria";
|
if (_isPointInPolygon(point, syriaBoundary)) return "Syria";
|
||||||
if (_isPointInPolygon(point, egyptBoundary)) return "Egypt";
|
if (_isPointInPolygon(point, egyptBoundary)) return "Egypt";
|
||||||
|
|
||||||
return "unsupported";
|
return "unsupported";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// خوارزمية Ray Casting للتحقق من وقوع نقطة داخل مضلع
|
/// خوارزمية Ray Casting للتحقق من وقوع نقطة داخل مضلع
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
class CurrencyHelper {
|
class CurrencyHelper {
|
||||||
|
|||||||
@@ -1,44 +1,59 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'colors.dart';
|
||||||
|
|
||||||
|
/// كان هذا نظام ألوان **ثانياً منافساً** لـ [AppColor]: نيفي `#0A0E21` وأزرق
|
||||||
|
/// `#3D5AFE` بدل نيفي العلامة `#1A2340` والـ Periwinkle `#8C9CF8`. النتيجة أن
|
||||||
|
/// صفحات المحفظة والأرباح والتحديات كانت تبدو من تطبيق آخر.
|
||||||
|
///
|
||||||
|
/// أُبقي الصنف باسمه حتى لا تتكسّر عشرات مواضع الاستدعاء، لكنه صار **واجهة
|
||||||
|
/// رقيقة فوق ألوان العلامة**. الجديد يُكتب بـ `SiroUi` / `Theme.of(context)`.
|
||||||
class FinanceDesignSystem {
|
class FinanceDesignSystem {
|
||||||
// --- Colors ---
|
// --- Colors ---
|
||||||
static Color get primaryDark => Get.isDarkMode ? const Color(0xFFE0E0E0) : const Color(0xFF0A0E21);
|
static Color get primaryDark =>
|
||||||
static Color get accentBlue => const Color(0xFF3D5AFE);
|
Get.isDarkMode ? const Color(0xFFF1F5F9) : AppColor.primaryColor;
|
||||||
static Color get successGreen => const Color(0xFF00C853);
|
static Color get accentBlue =>
|
||||||
static Color get dangerRed => const Color(0xFFD50000);
|
Get.isDarkMode ? AppColor.secondaryColorStatic : AppColor.primaryColor;
|
||||||
static Color get backgroundColor => Get.isDarkMode ? const Color(0xFF0A0E21) : const Color(0xFFF6F8FA);
|
static Color get successGreen => AppColor.greenColor;
|
||||||
static Color get cardColor => Get.isDarkMode ? const Color(0xFF1E1E2E) : const Color(0xFFFFFFFF);
|
static Color get dangerRed => AppColor.redColor;
|
||||||
static Color get textSecondary => Get.isDarkMode ? Colors.white70 : const Color(0xFF757575);
|
static Color get backgroundColor =>
|
||||||
static Color get textMuted => Get.isDarkMode ? Colors.white38 : const Color(0xFFBDBDBD);
|
Get.isDarkMode ? const Color(0xFF0F1424) : const Color(0xFFF6F8FA);
|
||||||
static Color get borderColor => Get.isDarkMode ? Colors.white10 : Colors.grey.withOpacity(0.1);
|
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 ---
|
// --- Shadows ---
|
||||||
static BoxShadow get softShadow => BoxShadow(
|
static BoxShadow get softShadow => BoxShadow(
|
||||||
color: Colors.black.withOpacity(Get.isDarkMode ? 0.3 : 0.05),
|
color: Colors.black.withValues(alpha: Get.isDarkMode ? 0.35 : 0.05),
|
||||||
blurRadius: 10,
|
blurRadius: 18,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 6),
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Gradients ---
|
// --- Gradients ---
|
||||||
|
/// تدرّج العلامة: نيفي ← Periwinkle. يُستعمل في بطاقات الرصيد والرؤوس.
|
||||||
static LinearGradient get balanceGradient => LinearGradient(
|
static LinearGradient get balanceGradient => LinearGradient(
|
||||||
colors: Get.isDarkMode
|
colors: Get.isDarkMode
|
||||||
? [const Color(0xFF1E1E2E), const Color(0xFF2A2A3E)]
|
? [const Color(0xFF1E2745), const Color(0xFF2E3A63)]
|
||||||
: [const Color(0xFF0A0E21), const Color(0xFF1A237E)],
|
: [AppColor.primaryColor, const Color(0xFF3A4674)],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const LinearGradient dangerGradient = LinearGradient(
|
static const LinearGradient dangerGradient = LinearGradient(
|
||||||
colors: [Color(0xFFD50000), Color(0xFFFF5252)],
|
colors: [AppColor.redColor, Color(0xFFF87171)],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Radius ---
|
// --- Radius ---
|
||||||
static const double mainRadius = 16.0;
|
static const double mainRadius = 16.0;
|
||||||
static const double cardRadius = 20.0;
|
static const double cardRadius = 18.0;
|
||||||
static const double buttonRadius = 12.0;
|
static const double buttonRadius = 14.0;
|
||||||
|
|
||||||
// --- Spacing ---
|
// --- Spacing ---
|
||||||
static const double horizontalPadding = 16.0;
|
static const double horizontalPadding = 16.0;
|
||||||
@@ -46,19 +61,19 @@ class FinanceDesignSystem {
|
|||||||
|
|
||||||
// --- Text Styles ---
|
// --- Text Styles ---
|
||||||
static TextStyle get balanceStyle => const TextStyle(
|
static TextStyle get balanceStyle => const TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get headingStyle => TextStyle(
|
static TextStyle get headingStyle => TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: primaryDark,
|
color: primaryDark,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get subHeadingStyle => TextStyle(
|
static TextStyle get subHeadingStyle => TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
color: textSecondary,
|
color: textSecondary,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
class AppInformation {
|
class AppInformation {
|
||||||
static const String companyName = 'Tripz';
|
static const String companyName = 'Siro';
|
||||||
static const String appName = 'Tripz DRIVER';
|
static const String appName = 'Siro DRIVER';
|
||||||
static const String appVersion = 'Tripz DRIVER';
|
static const String appVersion = 'Siro DRIVER';
|
||||||
static const String phoneNumber = '';
|
static const String phoneNumber = '';
|
||||||
static const String linkedInProfile =
|
static const String linkedInProfile =
|
||||||
'https://www.linkedin.com/in/hamza-ayed/';
|
'https://www.linkedin.com/in/hamza-ayed/';
|
||||||
static const String website = 'https://tripzapp.com';
|
static const String website = 'https://siromove.com';
|
||||||
static const String email = 'support@tripzapp.com';
|
static const String email = 'support@siromove.com';
|
||||||
static const String complaintPrompt =
|
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';
|
'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';
|
static const String addd = 'BlBlNl';
|
||||||
@@ -16,7 +16,7 @@ class AppInformation {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
@@ -71,11 +71,11 @@ class AppInformation {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>1. Introduction and Acceptance</h2>
|
<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>
|
<h2>2. Definitions</h2>
|
||||||
<ul>
|
<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>"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>"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>
|
<li><strong>"Services":</strong> The connection between Passengers and Drivers facilitated by our App.</li>
|
||||||
@@ -136,14 +136,14 @@ class AppInformation {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>5. Disclaimer of Liability</h2>
|
<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>
|
<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>
|
<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>
|
<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>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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -155,7 +155,7 @@ class AppInformation {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>تريبز - سياسة الخصوصية وشروط الاستخدام</title>
|
<title>سيرو - سياسة الخصوصية وشروط الاستخدام</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
@@ -211,11 +211,11 @@ class AppInformation {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>1. المقدمة والقبول</h2>
|
<h2>1. المقدمة والقبول</h2>
|
||||||
<p>عبر تحميل أو تسجيل أو استخدام تطبيق "تريبز" ("التطبيق")، فإنك توافق على الالتزام بسياسة الخصوصية وشروط الاستخدام هذه. إذا كنت لا توافق، يجب عليك التوقف فورًا عن استخدام التطبيق. استمرارك في الاستخدام يُعد قبولاً لهذه الشروط وأي تحديثات مستقبلية لها.</p>
|
<p>عبر تحميل أو تسجيل أو استخدام تطبيق "سيرو" ("التطبيق")، فإنك توافق على الالتزام بسياسة الخصوصية وشروط الاستخدام هذه. إذا كنت لا توافق، يجب عليك التوقف فورًا عن استخدام التطبيق. استمرارك في الاستخدام يُعد قبولاً لهذه الشروط وأي تحديثات مستقبلية لها.</p>
|
||||||
|
|
||||||
<h2>2. التعريفات</h2>
|
<h2>2. التعريفات</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>"تريبز"، "نحن":</strong> تشير إلى شركة تريبز لنقل الركاب، دمشق – سوريا (مالك ومشغل التطبيق)، التي توفر المنصة التقنية.</li>
|
<li><strong>"سيرو"، "نحن":</strong> تشير إلى شركة سيرو لنقل الركاب، دمشق – سوريا (مالك ومشغل التطبيق)، التي توفر المنصة التقنية.</li>
|
||||||
<li><strong>"السائق":</strong> مقدم خدمة مستقل يستخدم التطبيق لتقديم خدمات النقل.</li>
|
<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>
|
</ul>
|
||||||
|
|
||||||
<h2>5. إخلاء المسؤولية</h2>
|
<h2>5. إخلاء المسؤولية</h2>
|
||||||
<p>يتم تقديم التطبيق "كما هو". "تريبز" هي منصة وسيطة وليست مسؤولة عن تصرفات السائقين أو الركاب، أو الحوادث، أو التأخير، أو أي نزاعات بين المستخدمين. مسؤوليتنا محدودة إلى أقصى حد يسمح به القانون.</p>
|
<p>يتم تقديم التطبيق "كما هو". "سيرو" هي منصة وسيطة وليست مسؤولة عن تصرفات السائقين أو الركاب، أو الحوادث، أو التأخير، أو أي نزاعات بين المستخدمين. مسؤوليتنا محدودة إلى أقصى حد يسمح به القانون.</p>
|
||||||
|
|
||||||
<h2>6. تحديثات السياسة</h2>
|
<h2>6. تحديثات السياسة</h2>
|
||||||
<p>قد نقوم بتحديث هذه الشروط. في حال إجراء تغييرات جوهرية، سنقوم بإعلامك داخل التطبيق. سيُطلب منك مراجعة الشروط الجديدة وقبولها لمواصلة استخدام الخدمات، لضمان أن موافقتك فعالة ومبنية على معرفة.</p>
|
<p>قد نقوم بتحديث هذه الشروط. في حال إجراء تغييرات جوهرية، سنقوم بإعلامك داخل التطبيق. سيُطلب منك مراجعة الشروط الجديدة وقبولها لمواصلة استخدام الخدمات، لضمان أن موافقتك فعالة ومبنية على معرفة.</p>
|
||||||
|
|
||||||
<h2>7. حذف الحساب والتواصل</h2>
|
<h2>7. حذف الحساب والتواصل</h2>
|
||||||
<p>لديك الحق في طلب حذف حسابك وبياناتك الشخصية. للقيام بذلك، أو لأي استفسارات أخرى، يرجى التواصل معنا. سنرد على طلبات الحذف في غضون 30 يومًا.</p>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// import 'package:tripz_driver/env/env.dart';
|
// import 'package:siro_driver/env/env.dart';
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import 'box_name.dart';
|
import 'box_name.dart';
|
||||||
@@ -9,35 +9,48 @@ class AppLink {
|
|||||||
static String get paymentServer {
|
static String get paymentServer {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://wallet-syria.tripzmove.com/v1/main';
|
return 'https://wallet-syria.siromove.com/v1/main';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://wallet-egypt.tripzmove.com/v1/main';
|
return 'https://wallet-egypt.siromove.com/v1/main';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://wallettripz.tripz.xyz/v2/main';
|
return 'https://jordan-siro.intaleqapp.com/v2/main';
|
||||||
default:
|
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 {
|
static String get inviteRedirectUrl {
|
||||||
if (currentCountry == 'Syria') {
|
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 {
|
static String get location {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://api-syria.tripzmove.com/tripz_v3/ride/location';
|
return 'https://api-syria.siromove.com/siro_v3/ride/location';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://api-egypt.tripzmove.com/tripz_v3/ride/location';
|
return 'https://api-egypt.siromove.com/siro_v3/ride/location';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://jordan-tripz.tripzapp.com/backend/ride/location';
|
return 'https://jordan-siro.intaleqapp.com/backend/ride/location';
|
||||||
default:
|
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 {
|
static String get locationServerSide {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://location-syria.tripzmove.com/tripz/ride/location';
|
return 'https://location-syria.siromove.com/siro/ride/location';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://location-egypt.tripzmove.com/tripz/ride/location';
|
return 'https://location-egypt.siromove.com/siro/ride/location';
|
||||||
case 'Jordan':
|
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:
|
default:
|
||||||
return 'https://location-jordan.tripzmove.com/tripz/ride/location';
|
return 'https://location-jordan.siromove.com/siro/ride/location';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String get locationSocketUrl {
|
static String get locationSocketUrl {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://location-syria.tripzmove.com';
|
return 'https://location-syria.siromove.com';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://location-egypt.tripzmove.com';
|
return 'https://location-egypt.siromove.com';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://jordan-tripz.tripzapp.com:2020';
|
return 'https://jordan-siro.intaleqapp.com:2020';
|
||||||
default:
|
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 {
|
static String get mapSaasRoute {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://map-syria.tripzmove.com/api/maps/route';
|
return 'https://map-syria.siromove.com/api/maps/route';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://map-egypt.tripzmove.com/api/maps/route';
|
return 'https://map-egypt.siromove.com/api/maps/route';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://map-saas.tripzapp.com/api/maps/route';
|
return 'https://map-saas.intaleqapp.com/api/maps/route';
|
||||||
default:
|
default:
|
||||||
return 'https://map-saas.tripzapp.com/api/maps/route';
|
return 'https://map-saas.intaleqapp.com/api/maps/route';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String get mapSaasPlaces {
|
static String get mapSaasPlaces {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://map-syria.tripzmove.com/api/geocoding/places';
|
return 'https://map-syria.siromove.com/api/geocoding/places';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://map-egypt.tripzmove.com/api/geocoding/places';
|
return 'https://map-egypt.siromove.com/api/geocoding/places';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://map-saas.tripzapp.com/api/geocoding/places';
|
return 'https://map-saas.intaleqapp.com/api/geocoding/places';
|
||||||
default:
|
default:
|
||||||
return 'https://map-saas.tripzapp.com/api/geocoding/places';
|
return 'https://map-saas.intaleqapp.com/api/geocoding/places';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String get routeApiBaseUrl {
|
static String get routeApiBaseUrl {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return "https://routes-syria.tripzmove.com/route/v1/driving";
|
return "https://routes-syria.siromove.com/route/v1/driving";
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return "https://routes-egypt.tripzmove.com/route/v1/driving";
|
return "https://routes-egypt.siromove.com/route/v1/driving";
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return "https://routesjo.tripz.xyz/route/v1/driving";
|
return "https://routesjo.intaleq.xyz/route/v1/driving";
|
||||||
default:
|
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 {
|
static String get endPoint {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://api-syria.tripzmove.com/tripz';
|
return 'https://api-syria.siromove.com/siro';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://api-egypt.tripzmove.com/tripz';
|
return 'https://api-egypt.siromove.com/siro';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://jordan-tripz.tripzapp.com/backend';
|
return 'https://jordan-siro.intaleqapp.com/backend';
|
||||||
default:
|
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;
|
static String get server => endPoint;
|
||||||
|
|
||||||
///=================ride==========================///
|
///=================ride==========================///
|
||||||
///https://jordan-tripz.tripzapp.com/backend
|
///https://jordan-siro.intaleqapp.com/backend
|
||||||
static String get ride => '$server/ride';
|
static String get ride => '$server/ride';
|
||||||
|
|
||||||
/// this ride server is for socket communication not for rest api calls
|
/// 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 {
|
static String get rideServer {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://ride-syria.tripzmove.com/tripz/ride';
|
return 'https://ride-syria.siromove.com/siro/ride';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://ride-egypt.tripzmove.com/tripz/ride';
|
return 'https://ride-egypt.siromove.com/siro/ride';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://jordan-tripz.tripzapp.com/backend';
|
return 'https://jordan-siro.intaleqapp.com/backend';
|
||||||
default:
|
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 =>
|
static String get getPredictiveDemand =>
|
||||||
"$server/api/demand/getPredictiveDemandZones.php";
|
"$server/api/demand/getPredictiveDemandZones.php";
|
||||||
|
|
||||||
///mapOSM = 'https://routesy.tripz.xyz'
|
///mapOSM = 'https://routesy.intaleq.xyz'
|
||||||
static String get mapOSM {
|
static String get mapOSM {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://routes-syria.tripzmove.com';
|
return 'https://routes-syria.siromove.com';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://routes-egypt.tripzmove.com';
|
return 'https://routes-egypt.siromove.com';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://routesjo.tripz.xyz';
|
return 'https://routesjo.intaleq.xyz';
|
||||||
default:
|
default:
|
||||||
return 'https://routes.tripzmove.com';
|
return 'https://routes.siromove.com';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +278,13 @@ class AppLink {
|
|||||||
static String get getDriverDetails =>
|
static String get getDriverDetails =>
|
||||||
"$seferCairoServer/ride/driverWallet/getDriverDetails.php";
|
"$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 =================
|
// ================= Gamification Endpoints =================
|
||||||
static String get getWeeklyAggregate =>
|
static String get getWeeklyAggregate =>
|
||||||
"$endPoint/ride/gamification/getWeeklyAggregate.php";
|
"$endPoint/ride/gamification/getWeeklyAggregate.php";
|
||||||
@@ -396,13 +433,13 @@ class AppLink {
|
|||||||
static String get reverseGeocoding {
|
static String get reverseGeocoding {
|
||||||
switch (currentCountry) {
|
switch (currentCountry) {
|
||||||
case 'Syria':
|
case 'Syria':
|
||||||
return 'https://map-syria.tripzmove.com/api/geocoding/reverse';
|
return 'https://map-syria.siromove.com/api/geocoding/reverse';
|
||||||
case 'Egypt':
|
case 'Egypt':
|
||||||
return 'https://map-egypt.tripzmove.com/api/geocoding/reverse';
|
return 'https://map-egypt.siromove.com/api/geocoding/reverse';
|
||||||
case 'Jordan':
|
case 'Jordan':
|
||||||
return 'https://map-saas.tripzapp.com/api/geocoding/reverse';
|
return 'https://map-saas.intaleqapp.com/api/geocoding/reverse';
|
||||||
default:
|
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";
|
static String get updateTips => "$ride/tips/update.php";
|
||||||
|
|
||||||
//-----------------Help Center------------------
|
//-----------------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 gethelpCenter => "$ride/helpCenter/get.php";
|
||||||
static String get getByIdhelpCenter => "$ride/helpCenter/getById.php";
|
static String get getByIdhelpCenter => "$ride/helpCenter/getById.php";
|
||||||
static String get updatehelpCenter => "$ride/helpCenter/update.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 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 addRateToDriver => "$ride/rate/addRateToDriver.php";
|
||||||
static String get addRateApp => "$ride/rate/add_rate_app.php";
|
static String get addRateApp => "$ride/rate/add_rate_app.php";
|
||||||
static String get sendEmailRateingApp => "$ride/rate/sendEmailRateingApp.php";
|
static String get sendEmailRateingApp => "$ride/rate/sendEmailRateingApp.php";
|
||||||
@@ -645,4 +685,16 @@ class AppLink {
|
|||||||
if (clean.startsWith('01') || clean.startsWith('1')) return 'Egypt';
|
if (clean.startsWith('01') || clean.startsWith('1')) return 'Egypt';
|
||||||
return '';
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ List<String> syrianDriverMessages = [
|
|||||||
"رزقتك جاهزة: 💸 لا تخلّي الزباين يستنّوا، افتح التطبيق وخليك متأهّب.",
|
"رزقتك جاهزة: 💸 لا تخلّي الزباين يستنّوا، افتح التطبيق وخليك متأهّب.",
|
||||||
|
|
||||||
// --- التقدير والشكر ---
|
// --- التقدير والشكر ---
|
||||||
"شكراً إلك: 🙏 إنت مو بس سائق، إنت أساس نجاح تطبيق تريبز.",
|
"شكراً إلك: 🙏 إنت مو بس سائق، إنت أساس نجاح تطبيق سيرو.",
|
||||||
"نفتخر فيك: 🌟 الكباتن متلك هنن يلي رافعين اسمنا بالعالي.",
|
"نفتخر فيك: 🌟 الكباتن متلك هنن يلي رافعين اسمنا بالعالي.",
|
||||||
"أداء ممتاز: 👍 استمر بنفس الروح الحلوة، زباينك مبسوطين منك.",
|
"أداء ممتاز: 👍 استمر بنفس الروح الحلوة، زباينك مبسوطين منك.",
|
||||||
"نجاح مشترك: 🗺️ كل توصيلة بتعملها بتكبر فيها شركتنا وانت كمان.",
|
"نجاح مشترك: 🗺️ كل توصيلة بتعملها بتكبر فيها شركتنا وانت كمان.",
|
||||||
|
|
||||||
// --- التحفيز والتشجيع ---
|
// --- التحفيز والتشجيع ---
|
||||||
"صباح النشاط: ☀️ بلّش نهارك بطاقة إيجابية وتريبز لتكسب أكتر.",
|
"صباح النشاط: ☀️ بلّش نهارك بطاقة إيجابية وسيرو لتكسب أكتر.",
|
||||||
"كل مشوار فرصة: 🏁 لا توقف، الطريق لإلك والنجاح ناطرك.",
|
"كل مشوار فرصة: 🏁 لا توقف، الطريق لإلك والنجاح ناطرك.",
|
||||||
"يوم مربح: 💼 السوق ناشط اليوم، لا تفوّت الفرصة!",
|
"يوم مربح: 💼 السوق ناشط اليوم، لا تفوّت الفرصة!",
|
||||||
"جاهز للطلب الجاي: 🔔 الزبون الجاي ممكن يكون أوفر ممتاز، خليك مستعد.",
|
"جاهز للطلب الجاي: 🔔 الزبون الجاي ممكن يكون أوفر ممتاز، خليك مستعد.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
|
|
||||||
class RechargeTier {
|
class RechargeTier {
|
||||||
final double amount;
|
final double amount;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.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:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
@@ -7,15 +7,15 @@ import 'package:flutter_contacts/contact.dart';
|
|||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/constant/links.dart';
|
import 'package:siro_driver/constant/links.dart';
|
||||||
import 'package:tripz_driver/controller/functions/crud.dart';
|
import 'package:siro_driver/controller/functions/crud.dart';
|
||||||
import 'package:tripz_driver/controller/home/payment/captain_wallet_controller.dart';
|
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||||
import 'package:tripz_driver/print.dart';
|
import 'package:siro_driver/print.dart';
|
||||||
import 'package:tripz_driver/controller/functions/country_logic.dart';
|
import 'package:siro_driver/controller/functions/country_logic.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
|
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:tripz_driver/views/widgets/mydialoug.dart';
|
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
import '../../firebase/local_notification.dart';
|
import '../../firebase/local_notification.dart';
|
||||||
@@ -43,10 +43,10 @@ class InviteController extends GetxController {
|
|||||||
Future<void> shareDriverCode() async {
|
Future<void> shareDriverCode() async {
|
||||||
if (driverCouponCode != null) {
|
if (driverCouponCode != null) {
|
||||||
final String shareText =
|
final String shareText =
|
||||||
'''Join Tripz as a driver using my referral code!
|
'''Join Siro as a driver using my referral code!
|
||||||
Use code: $driverCouponCode
|
Use code: $driverCouponCode
|
||||||
Download the Tripz Driver app now and earn rewards:
|
Download the Siro Captain app now and earn rewards:
|
||||||
https://tripzmove.com/invite.php?code=$driverCouponCode&app=driver
|
${AppLink.driverDownloadLinks}
|
||||||
''';
|
''';
|
||||||
await Share.share(shareText);
|
await Share.share(shareText);
|
||||||
}
|
}
|
||||||
@@ -54,10 +54,10 @@ https://tripzmove.com/invite.php?code=$driverCouponCode&app=driver
|
|||||||
|
|
||||||
Future<void> sharePassengerCode() async {
|
Future<void> sharePassengerCode() async {
|
||||||
if (couponCode != null) {
|
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
|
Use my referral code: $couponCode
|
||||||
Download the Tripz app now and enjoy your ride:
|
Download the Siro app now and enjoy your ride:
|
||||||
https://tripzmove.com/invite.php?code=$couponCode&app=rider
|
https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||||
''';
|
''';
|
||||||
await Share.share(shareText);
|
await Share.share(shareText);
|
||||||
}
|
}
|
||||||
@@ -66,62 +66,53 @@ https://tripzmove.com/invite.php?code=$couponCode&app=rider
|
|||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
// **MODIFIED**: Sync contacts automatically on controller initialization.
|
// لا نقرأ دفتر الهاتف هنا. القراءة تحدث فقط حين يطلب الكابتن
|
||||||
syncContactsToServerOnce();
|
// اختيار جهة اتصال — انظر loadDeviceContacts أدناه.
|
||||||
// fetchDriverStats();
|
// fetchDriverStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEW LOGIC: ONE-TIME CONTACTS SYNC ---
|
// --- قراءة جهات الاتصال محلياً، بطلب المستخدم وحده ---
|
||||||
|
|
||||||
/// **NEW**: Syncs all phone contacts to the server, but only runs once per user.
|
/// كان هنا `syncContactsToServerOnce`: يقرأ دفتر هاتف الكابتن كاملاً
|
||||||
Future<void> syncContactsToServerOnce() async {
|
/// تلقائياً عند فتح شاشة الدعوة، ويرفع **كل** اسم ورقم إلى
|
||||||
final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}';
|
/// `ride/egyptPhones/syrianAdd.php` — طلب لكل جهة اتصال.
|
||||||
|
///
|
||||||
// 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.");
|
/// ١) سياسة Google Play: رفع دفتر العناوين جملةً يتطلب إفصاحاً بارزاً
|
||||||
return;
|
/// وموافقة صريحة، وإذن النظام وحده لا يكفي. والتطبيق موقوف أصلاً.
|
||||||
}
|
///
|
||||||
|
/// ٢) البيانات ليست بيانات الكابتن ليتبرّع بها: هي أسماء وأرقام أطراف
|
||||||
|
/// ثالثة لم تُستأذن ولا تعلم بوجود سيرو.
|
||||||
|
///
|
||||||
|
/// ٣) لا ميزة تعتمد عليها. الدعوة تعمل بالمشاركة أو باختيار جهة واحدة
|
||||||
|
/// عمداً، وكلاهما لا يحتاج نسخة من الدفتر على السيرفر.
|
||||||
|
///
|
||||||
|
/// ما بقي هنا قراءة محلية بحتة لعرض قائمة يختار منها الكابتن — لا
|
||||||
|
/// شيء يغادر الجهاز.
|
||||||
|
Future<void> loadDeviceContacts() async {
|
||||||
|
if (contactMaps.isNotEmpty) return; // مُحمّلة سلفاً في هذه الجلسة
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. Request permission and fetch all contacts.
|
|
||||||
if (await FlutterContacts.requestPermission(readonly: true)) {
|
if (await FlutterContacts.requestPermission(readonly: true)) {
|
||||||
// mySnackbarSuccess('Starting contacts sync in background...'.tr);
|
|
||||||
final List<Contact> allContacts =
|
final List<Contact> allContacts =
|
||||||
await FlutterContacts.getContacts(withProperties: true);
|
await FlutterContacts.getContacts(withProperties: true);
|
||||||
// **FIX**: Assign fetched contacts to the class variable.
|
|
||||||
contacts = allContacts;
|
contacts = allContacts;
|
||||||
contactMaps.value = contacts.map((contact) {
|
contactMaps.value = allContacts
|
||||||
return {
|
.where((c) => c.phones.isNotEmpty)
|
||||||
'name': contact.displayName,
|
.map((contact) => {
|
||||||
'phones':
|
'name': contact.displayName,
|
||||||
contact.phones.map((phone) => phone.normalizedNumber).toList(),
|
'phones': contact.phones
|
||||||
'emails': contact.emails.map((email) => email.address).toList(),
|
.map((phone) => phone.normalizedNumber)
|
||||||
};
|
.toList(),
|
||||||
}).toList();
|
'emails':
|
||||||
|
contact.emails.map((email) => email.address).toList(),
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
update();
|
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) {
|
} 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;
|
var d = response;
|
||||||
mySnackbarSuccess('Invite sent successfully'.tr);
|
mySnackbarSuccess('Invite sent successfully'.tr);
|
||||||
String inviteCode = d['message']['inviteCode'].toString();
|
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'
|
'${"Use this code in registration".tr}\n'
|
||||||
'${"To get a gift for both".tr}\n\n'
|
'${"To get a gift for both".tr}\n\n'
|
||||||
'${"The period of this code is 24 hours".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);
|
mySnackbarSuccess('Invite sent successfully'.tr);
|
||||||
String inviteCode = d['message']['inviteCode'].toString();
|
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'
|
'${"Use this code in registration".tr}\n\n'
|
||||||
'${"To get a gift for both".tr}\n\n'
|
'${"To get a gift for both".tr}\n\n'
|
||||||
'${"The period of this code is 24 hours".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 'dart:math';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:tripz_driver/views/auth/syria/registration_view.dart';
|
import 'package:siro_driver/views/auth/syria/registration_view.dart';
|
||||||
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
|
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||||
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
|
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/constant/links.dart';
|
import 'package:siro_driver/constant/links.dart';
|
||||||
import 'package:tripz_driver/controller/functions/crud.dart';
|
import 'package:siro_driver/controller/functions/crud.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import 'package:tripz_driver/views/home/Captin/home_captain/home_captin.dart';
|
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||||
import 'package:location/location.dart';
|
import 'package:location/location.dart';
|
||||||
|
|
||||||
import '../../../constant/api_key.dart';
|
import '../../../constant/api_key.dart';
|
||||||
@@ -55,7 +55,7 @@ class LoginDriverController extends GetxController {
|
|||||||
void togglePasswordVisibility() {
|
void togglePasswordVisibility() {
|
||||||
isPasswordHidden = !isPasswordHidden;
|
isPasswordHidden = !isPasswordHidden;
|
||||||
update([
|
update([
|
||||||
'passwordVisibility'
|
'passwordVisibility',
|
||||||
]); // Use a unique ID to only update the password field
|
]); // Use a unique ID to only update the password field
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +110,9 @@ class LoginDriverController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var res = await CRUD().post(
|
var res = await CRUD().post(
|
||||||
link: AppLink.isPhoneVerified,
|
link: AppLink.isPhoneVerified,
|
||||||
payload: {'phone_number': box.read(BoxName.phoneDriver)});
|
payload: {'phone_number': box.read(BoxName.phoneDriver)},
|
||||||
|
);
|
||||||
|
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
// Get.offAll(() => SyrianCardAI());
|
// Get.offAll(() => SyrianCardAI());
|
||||||
@@ -197,13 +198,14 @@ class LoginDriverController extends GetxController {
|
|||||||
|
|
||||||
Log.print('Security check passed');
|
Log.print('Security check passed');
|
||||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||||
var fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ??
|
var fingerPrint =
|
||||||
|
box.read(BoxName.deviceFingerprint)?.toString() ??
|
||||||
await DeviceHelper.getDeviceFingerprint();
|
await DeviceHelper.getDeviceFingerprint();
|
||||||
var payload = {
|
var payload = {
|
||||||
'id': box.read(BoxName.driverID),
|
'id': box.read(BoxName.driverID),
|
||||||
'password': AK.passnpassenger,
|
'password': AK.passnpassenger,
|
||||||
'aud': '${AK.allowedWallet}$dev',
|
'aud': '${AK.allowedWallet}$dev',
|
||||||
'fingerPrint': fingerPrint
|
'fingerPrint': fingerPrint,
|
||||||
};
|
};
|
||||||
var response1 = await http.post(
|
var response1 = await http.post(
|
||||||
Uri.parse(AppLink.loginJwtWalletDriver),
|
Uri.parse(AppLink.loginJwtWalletDriver),
|
||||||
@@ -228,104 +230,148 @@ class LoginDriverController extends GetxController {
|
|||||||
return '';
|
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();
|
await EncryptionHelper.initialize();
|
||||||
|
|
||||||
// 1. Check secure storage first to avoid redundant API calls
|
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||||
String? secureJwt = await storage.read(key: BoxName.jwt);
|
final driverId = box.read(BoxName.driverID)?.toString() ?? '';
|
||||||
if (secureJwt != null && secureJwt.isNotEmpty) {
|
final isRegistering = driverId.isEmpty;
|
||||||
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 (_) {}
|
|
||||||
|
|
||||||
if (isTokenValid) {
|
String? secureJwt = await storage.read(key: BoxName.jwt);
|
||||||
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
|
final cached = decodeJwtPayload(secureJwt);
|
||||||
return;
|
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';
|
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
|
||||||
Log.print(
|
Log.print(
|
||||||
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
|
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.',
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
Log.print('response0: ${response0.body}');
|
return false;
|
||||||
Log.print('request: ${response0.request}');
|
}
|
||||||
if (response0.statusCode == 200) {
|
|
||||||
final decodedResponse1 = jsonDecode(response0.body);
|
|
||||||
Log.print('decodedResponse1: ${decodedResponse1}');
|
|
||||||
|
|
||||||
String? jwt;
|
if (isRegistering &&
|
||||||
if (decodedResponse1['message'] is Map &&
|
box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
||||||
decodedResponse1['message']['jwt'] != null) {
|
return _getRegistrationJwt();
|
||||||
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 {}
|
|
||||||
} else {
|
} else {
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
|
|
||||||
|
if (isRegistering) {
|
||||||
|
return _onJwtFailure('renew: لا يوجد driverID');
|
||||||
|
}
|
||||||
|
|
||||||
var payload = {
|
var payload = {
|
||||||
'id': box.read(BoxName.driverID),
|
'id': box.read(BoxName.driverID),
|
||||||
'password': box.read(BoxName.emailDriver),
|
|
||||||
'aud': '${AK.allowed}$dev',
|
'aud': '${AK.allowed}$dev',
|
||||||
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
|
'fingerPrint':
|
||||||
|
box.read(BoxName.deviceFingerprint) ??
|
||||||
await DeviceHelper.getDeviceFingerprint(),
|
await DeviceHelper.getDeviceFingerprint(),
|
||||||
};
|
};
|
||||||
// print(payload);
|
|
||||||
var response1 = await http.post(
|
var response1 = await http
|
||||||
Uri.parse(AppLink.loginJwtDriver),
|
.post(Uri.parse(AppLink.loginJwtDriver), body: payload)
|
||||||
body: payload,
|
.timeout(const Duration(seconds: 30));
|
||||||
);
|
|
||||||
Log.print('response1.request: ${response1.request}');
|
if (response1.statusCode == 429) {
|
||||||
Log.print('response1.body: ${response1.body}');
|
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) {
|
if (response1.statusCode == 200) {
|
||||||
final decodedResponse1 = jsonDecode(response1.body);
|
final decodedResponse1 = jsonDecode(response1.body);
|
||||||
// Log.print('decodedResponse1: ${decodedResponse1}');
|
|
||||||
|
|
||||||
String? jwt;
|
String? jwt;
|
||||||
if (decodedResponse1['message'] is Map &&
|
if (decodedResponse1['message'] is Map &&
|
||||||
decodedResponse1['message']['jwt'] != null) {
|
decodedResponse1['message']['jwt'] != null) {
|
||||||
@@ -335,15 +381,98 @@ class LoginDriverController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
// await box.write(BoxName.jwt, c(jwt));
|
|
||||||
await storage.write(key: BoxName.jwt, value: 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 {
|
Future<void> getLocationPermission() async {
|
||||||
var status = await Permission.locationAlways.status;
|
var status = await Permission.locationAlways.status;
|
||||||
if (!status.isGranted) {
|
if (!status.isGranted) {
|
||||||
@@ -357,9 +486,7 @@ class LoginDriverController extends GetxController {
|
|||||||
Future updateInvitationCodeFromRegister() async {
|
Future updateInvitationCodeFromRegister() async {
|
||||||
var res = await CRUD().post(
|
var res = await CRUD().post(
|
||||||
link: AppLink.updateDriverInvitationDirectly,
|
link: AppLink.updateDriverInvitationDirectly,
|
||||||
payload: {
|
payload: {"inviterDriverPhone": box.read(BoxName.phoneDriver).toString()},
|
||||||
"inviterDriverPhone": box.read(BoxName.phoneDriver).toString(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
Log.print('invite: ${res}');
|
Log.print('invite: ${res}');
|
||||||
|
|
||||||
@@ -371,7 +498,11 @@ class LoginDriverController extends GetxController {
|
|||||||
update();
|
update();
|
||||||
box.write(BoxName.isInstall, '1');
|
box.write(BoxName.isInstall, '1');
|
||||||
NotificationController().showNotification(
|
NotificationController().showNotification(
|
||||||
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
"Code approved".tr,
|
||||||
|
"Code approved".tr,
|
||||||
|
'tone2',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NotificationService.sendNotification(
|
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 {
|
loginDriver(String driverID, email) async {
|
||||||
isloading = true;
|
isloading = true;
|
||||||
update();
|
update();
|
||||||
@@ -396,8 +544,47 @@ class LoginDriverController extends GetxController {
|
|||||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||||
// await getJWT();
|
// await getJWT();
|
||||||
var res = await CRUD().get(
|
var res = await CRUD().get(
|
||||||
link: AppLink.loginFromGoogleCaptin, payload: {'driver_id': driverID});
|
link: AppLink.loginFromGoogleCaptin,
|
||||||
|
payload: {'driver_id': driverID},
|
||||||
|
);
|
||||||
Log.print('loginDriver: ${res}');
|
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') {
|
if (res == 'failure') {
|
||||||
await isPhoneVerified();
|
await isPhoneVerified();
|
||||||
isloading = false; // <--- أضفت هذا أيضاً
|
isloading = false; // <--- أضفت هذا أيضاً
|
||||||
@@ -405,17 +592,46 @@ class LoginDriverController extends GetxController {
|
|||||||
return false;
|
return false;
|
||||||
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
||||||
} else {
|
} 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];
|
var d = jsonDecoeded['data'][0];
|
||||||
if (jsonDecoeded.isNotEmpty) {
|
if (jsonDecoeded.isNotEmpty) {
|
||||||
if (jsonDecoeded['status'] == 'success' &&
|
final driverStatus = d['status']?.toString() ?? '';
|
||||||
d['is_verified'].toString() == '1') {
|
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.emailDriver, d['email']);
|
||||||
box.write(BoxName.firstTimeLoadKey, 'false');
|
box.write(BoxName.firstTimeLoadKey, 'false');
|
||||||
box.write(BoxName.driverID, (d['id']));
|
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.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.phoneDriver, (d['phone']));
|
||||||
box.write(BoxName.is_claimed, d['is_claimed']);
|
box.write(BoxName.is_claimed, d['is_claimed']);
|
||||||
box.write(BoxName.isInstall, d['isInstall']);
|
box.write(BoxName.isInstall, d['isInstall']);
|
||||||
@@ -426,9 +642,10 @@ class LoginDriverController extends GetxController {
|
|||||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||||
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
||||||
box.write(
|
box.write(
|
||||||
BoxName.nameDriver,
|
BoxName.nameDriver,
|
||||||
'${(d['first_name'])}'
|
'${(d['first_name'])}'
|
||||||
' ${(d['last_name'])}');
|
' ${(d['last_name'])}',
|
||||||
|
);
|
||||||
if (((d['model']).toString().contains('دراجه') ||
|
if (((d['model']).toString().contains('دراجه') ||
|
||||||
d['make'].toString().contains('دراجه '))) {
|
d['make'].toString().contains('دراجه '))) {
|
||||||
if ((d['gender']).toString() == 'Male') {
|
if ((d['gender']).toString() == 'Male') {
|
||||||
@@ -449,15 +666,39 @@ class LoginDriverController extends GetxController {
|
|||||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
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...');
|
Log.print('🔑 Getting access token after login...');
|
||||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||||
// await storage.write(
|
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
||||||
// key: BoxName.fingerPrint, value: fingerPrint.toString());
|
final upgraded = await getJWT(force: true);
|
||||||
// await getJWT();
|
Log.print(
|
||||||
// Log.print('🔑 Access token obtained.');
|
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 ||
|
if (box.read(BoxName.isInstall) == null ||
|
||||||
box.read(BoxName.isInstall).toString() == '0') {
|
box.read(BoxName.isInstall).toString() == '0') {
|
||||||
updateInvitationCodeFromRegister();
|
updateInvitationCodeFromRegister();
|
||||||
@@ -467,14 +708,13 @@ class LoginDriverController extends GetxController {
|
|||||||
final driverStatus = d['status'].toString();
|
final driverStatus = d['status'].toString();
|
||||||
if (driverStatus != 'yet' && driverStatus != 'pending_review') {
|
if (driverStatus != 'yet' && driverStatus != 'pending_review') {
|
||||||
var token = await CRUD().get(
|
var token = await CRUD().get(
|
||||||
link: AppLink.getDriverToken,
|
link: AppLink.getDriverToken,
|
||||||
payload: {
|
payload: {'captain_id': (box.read(BoxName.driverID)).toString()},
|
||||||
'captain_id': (box.read(BoxName.driverID)).toString()
|
);
|
||||||
});
|
|
||||||
// print(jsonDecode(token)['data'][0]['token'].toString());
|
// print(jsonDecode(token)['data'][0]['token'].toString());
|
||||||
// print(box.read(BoxName.tokenDriver).toString());
|
// print(box.read(BoxName.tokenDriver).toString());
|
||||||
// if (box.read(BoxName.emailDriver).toString() !=
|
// if (box.read(BoxName.emailDriver).toString() !=
|
||||||
// '963992952235@tripzapp.com') {
|
// '963992952235@intaleqapp.com') {
|
||||||
if (token != 'failure') {
|
if (token != 'failure') {
|
||||||
var serverData = jsonDecode(token);
|
var serverData = jsonDecode(token);
|
||||||
final serverToken = serverData['data'][0]['token'].toString();
|
final serverToken = serverData['data'][0]['token'].toString();
|
||||||
@@ -483,9 +723,19 @@ class LoginDriverController extends GetxController {
|
|||||||
final serverFp = serverData['data'][0]['fingerPrint'].toString();
|
final serverFp = serverData['data'][0]['fingerPrint'].toString();
|
||||||
final tokenMismatch =
|
final tokenMismatch =
|
||||||
storedToken.isNotEmpty && serverToken != storedToken;
|
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(
|
Log.print(
|
||||||
'🔍 [FP_COMPARE] serverFp: $serverFp | localFp: ${fingerPrint.toString()} | tokenMismatch: $tokenMismatch | serverToken: $serverToken | storedToken: $storedToken');
|
'🔍 [FP_COMPARE] serverFp: $serverFp | localFp: ${fingerPrint.toString()} | fingerprintMismatch: $fingerprintMismatch | tokenMismatch: $tokenMismatch | serverToken: $serverToken | storedToken: $storedToken',
|
||||||
if (tokenMismatch) {
|
);
|
||||||
|
if (fingerprintMismatch || tokenMismatch) {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
title: 'Device Change Detected'.tr,
|
title: 'Device Change Detected'.tr,
|
||||||
@@ -504,8 +754,9 @@ class LoginDriverController extends GetxController {
|
|||||||
phone: d['phone'].toString(),
|
phone: d['phone'].toString(),
|
||||||
deviceToken: fingerPrint.toString(),
|
deviceToken: fingerPrint.toString(),
|
||||||
token: token.toString(),
|
token: token.toString(),
|
||||||
ptoken:
|
ptoken: jsonDecode(
|
||||||
jsonDecode(token)['data'][0]['token'].toString(),
|
token,
|
||||||
|
)['data'][0]['token'].toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -523,7 +774,8 @@ class LoginDriverController extends GetxController {
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
Get.offAll(
|
Get.offAll(
|
||||||
() => DriverVerificationScreen()); // افترض أن هذا الكلاس موجود
|
() => DriverVerificationScreen(),
|
||||||
|
); // افترض أن هذا الكلاس موجود
|
||||||
isloading = false; // <--- أضفت هذا
|
isloading = false; // <--- أضفت هذا
|
||||||
update(); // <--- أضفت هذا
|
update(); // <--- أضفت هذا
|
||||||
return false;
|
return false;
|
||||||
@@ -552,7 +804,9 @@ class LoginDriverController extends GetxController {
|
|||||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||||
|
|
||||||
var res = await CRUD().get(
|
var res = await CRUD().get(
|
||||||
link: AppLink.loginFromGoogleCaptin, payload: {'driver_id': driverID});
|
link: AppLink.loginFromGoogleCaptin,
|
||||||
|
payload: {'driver_id': driverID},
|
||||||
|
);
|
||||||
|
|
||||||
// print('res is $res');
|
// print('res is $res');
|
||||||
// if (res == 'failure') {
|
// if (res == 'failure') {
|
||||||
@@ -583,9 +837,10 @@ class LoginDriverController extends GetxController {
|
|||||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||||
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
||||||
box.write(
|
box.write(
|
||||||
BoxName.nameDriver,
|
BoxName.nameDriver,
|
||||||
'${(d['first_name'])}'
|
'${(d['first_name'])}'
|
||||||
' ${(d['last_name'])}');
|
' ${(d['last_name'])}',
|
||||||
|
);
|
||||||
if (((d['model']).toString().contains('دراجه') ||
|
if (((d['model']).toString().contains('دراجه') ||
|
||||||
d['make'].toString().contains('دراجه '))) {
|
d['make'].toString().contains('دراجه '))) {
|
||||||
if ((d['gender']).toString() == 'Male') {
|
if ((d['gender']).toString() == 'Male') {
|
||||||
@@ -645,51 +900,80 @@ class LoginDriverController extends GetxController {
|
|||||||
'aud': AppInformation.appName,
|
'aud': AppInformation.appName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Log.print(
|
||||||
|
'🚀 loginUsingCredentialsWithoutGoogle Request URL: ${AppLink.loginUsingCredentialsWithoutGoogle}',
|
||||||
|
);
|
||||||
|
Log.print('📦 loginUsingCredentialsWithoutGoogle Payload: $payload');
|
||||||
|
|
||||||
var response = await http.post(
|
var response = await http.post(
|
||||||
Uri.parse(AppLink.loginUsingCredentialsWithoutGoogle),
|
Uri.parse(AppLink.loginUsingCredentialsWithoutGoogle),
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Log.print(
|
||||||
|
'✅ loginUsingCredentialsWithoutGoogle Status Code: ${response.statusCode}',
|
||||||
|
);
|
||||||
|
Log.print(
|
||||||
|
'📄 loginUsingCredentialsWithoutGoogle Response Body: ${response.body}',
|
||||||
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
var jsonDecoeded = jsonDecode(response.body);
|
var jsonDecoeded = jsonDecode(response.body);
|
||||||
if (jsonDecoeded['status'] == 'success') {
|
if (jsonDecoeded['status'] == 'success') {
|
||||||
var d = jsonDecoeded['data'][0];
|
var d = jsonDecoeded['data'][0];
|
||||||
var jwt = jsonDecoeded['jwt'];
|
var jwt = jsonDecoeded['jwt'];
|
||||||
|
|
||||||
// حفظ التوكن أولاً
|
// حفظ التوكن في FlutterSecureStorage فقط
|
||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
box.write(BoxName.jwt, c(jwt));
|
await storage.write(key: BoxName.jwt, value: jwt);
|
||||||
await storage.write(key: BoxName.jwt, value: c(jwt));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
box.write(BoxName.emailDriver, (d['email']));
|
box.write(BoxName.emailDriver, (d['email']));
|
||||||
box.write(BoxName.driverID, (d['id']));
|
box.write(BoxName.driverID, (d['id']));
|
||||||
box.write(BoxName.isTest, '1');
|
box.write(BoxName.isTest, '1');
|
||||||
box.write(BoxName.gender, (d['gender']));
|
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.phoneDriver, (d['phone']));
|
||||||
box.write(BoxName.nameArabic, (d['name_arabic']));
|
box.write(BoxName.nameArabic, (d['name_arabic']));
|
||||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||||
box.write(BoxName.accountBankNumberDriver, d['accountBank']);
|
box.write(BoxName.accountBankNumberDriver, d['accountBank']);
|
||||||
box.write(
|
box.write(
|
||||||
BoxName.nameDriver,
|
BoxName.nameDriver,
|
||||||
'${(d['first_name'])}'
|
'${(d['first_name'])}'
|
||||||
' ${(d['last_name'])}');
|
' ${(d['last_name'])}',
|
||||||
|
);
|
||||||
|
|
||||||
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
||||||
|
|
||||||
Get.off(() => HomeCaptain());
|
Get.off(() => HomeCaptain());
|
||||||
} else {
|
} else {
|
||||||
mySnackbarError('Login failed'.tr);
|
Log.print('❌ Login failed message: ${jsonDecoeded['message']}');
|
||||||
|
mySnackbarError(
|
||||||
|
(jsonDecoeded['message'] ?? 'Login failed').toString().tr,
|
||||||
|
);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
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 {
|
} else {
|
||||||
|
Log.print('❌ Server error response (Not 200/201/429)');
|
||||||
mySnackbarError('Server error'.tr);
|
mySnackbarError('Server error'.tr);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Log.print('❌ Network/Exception error: $e');
|
||||||
mySnackbarError('Network error'.tr);
|
mySnackbarError('Network error'.tr);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
update();
|
||||||
@@ -698,21 +982,26 @@ class LoginDriverController extends GetxController {
|
|||||||
|
|
||||||
void loginByBoxData() async {
|
void loginByBoxData() async {
|
||||||
Get.to(() => HomeCaptain());
|
Get.to(() => HomeCaptain());
|
||||||
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
await CRUD().post(
|
||||||
'token': box.read(BoxName.tokenDriver).toString(),
|
link: AppLink.addTokensDriver,
|
||||||
'captain_id': box.read(BoxName.driverID).toString()
|
payload: {
|
||||||
});
|
'token': box.read(BoxName.tokenDriver).toString(),
|
||||||
|
'captain_id': box.read(BoxName.driverID).toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
CRUD().post(
|
CRUD().post(
|
||||||
link: "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
|
link: "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
|
||||||
payload: {
|
payload: {
|
||||||
'token': box.read(BoxName.tokenDriver),
|
'token': box.read(BoxName.tokenDriver),
|
||||||
'captain_id': box.read(BoxName.driverID).toString()
|
'captain_id': box.read(BoxName.driverID).toString(),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
CRUD().post(
|
CRUD().post(
|
||||||
link: "${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
|
link: "${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
|
||||||
payload: {
|
payload: {
|
||||||
'token': box.read(BoxName.tokenDriver),
|
'token': box.read(BoxName.tokenDriver),
|
||||||
'captain_id': box.read(BoxName.driverID).toString()
|
'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:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
|
||||||
import 'package:image_cropper/image_cropper.dart';
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:tripz_driver/constant/colors.dart';
|
import 'package:siro_driver/constant/colors.dart';
|
||||||
import 'package:tripz_driver/controller/functions/llama_ai.dart';
|
import 'package:siro_driver/controller/functions/llama_ai.dart';
|
||||||
|
|
||||||
// class CarRegistrationRecognizerController extends GetxController {
|
// class CarRegistrationRecognizerController extends GetxController {
|
||||||
// @override
|
// @override
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tripz_driver/print.dart';
|
import 'package:siro_driver/print.dart';
|
||||||
import 'package:tripz_driver/views/home/Captin/home_captain/home_captin.dart';
|
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/links.dart';
|
import '../../../constant/links.dart';
|
||||||
@@ -10,6 +10,7 @@ import '../../../main.dart';
|
|||||||
import '../../../views/widgets/error_snakbar.dart';
|
import '../../../views/widgets/error_snakbar.dart';
|
||||||
import '../../firebase/notification_service.dart';
|
import '../../firebase/notification_service.dart';
|
||||||
import '../../functions/crud.dart';
|
import '../../functions/crud.dart';
|
||||||
|
import 'login_captin_controller.dart';
|
||||||
|
|
||||||
class OtpVerificationController extends GetxController {
|
class OtpVerificationController extends GetxController {
|
||||||
final String phone;
|
final String phone;
|
||||||
@@ -95,6 +96,25 @@ class OtpVerificationController extends GetxController {
|
|||||||
response != 'no_internet') {
|
response != 'no_internet') {
|
||||||
Log.print('response (already decoded): ${response}');
|
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(
|
await CRUD().post(
|
||||||
link: AppLink.addTokensDriver,
|
link: AppLink.addTokensDriver,
|
||||||
@@ -104,15 +124,19 @@ class OtpVerificationController extends GetxController {
|
|||||||
'captain_id': box.read(BoxName.driverID).toString(),
|
'captain_id': box.read(BoxName.driverID).toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await NotificationService.sendNotification(
|
// إشعار الجهاز القديم — يُرسل فقط إذا عرفنا توكنه. عند دخول جهاز
|
||||||
target: ptoken.toString(),
|
// جديد لا نملك ptoken (تعذّر جلبه بلا صلاحيات) فنتخطّاه.
|
||||||
title: 'token change'.tr,
|
if (ptoken.isNotEmpty) {
|
||||||
body: 'token change'.tr,
|
await NotificationService.sendNotification(
|
||||||
isTopic: false,
|
target: ptoken,
|
||||||
tone: 'cancel',
|
title: 'token change'.tr,
|
||||||
driverList: [],
|
body: 'token change'.tr,
|
||||||
category: 'token change',
|
isTopic: false,
|
||||||
);
|
tone: 'cancel',
|
||||||
|
driverList: [],
|
||||||
|
category: 'token change',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Get.offAll(() => HomeCaptain());
|
Get.offAll(() => HomeCaptain());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tripz_driver/controller/auth/captin/login_captin_controller.dart';
|
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||||
import 'package:tripz_driver/controller/functions/crud.dart';
|
import 'package:siro_driver/controller/functions/crud.dart';
|
||||||
import 'package:tripz_driver/print.dart';
|
import 'package:siro_driver/print.dart';
|
||||||
import 'package:tripz_driver/views/home/on_boarding_page.dart';
|
import 'package:siro_driver/views/home/on_boarding_page.dart';
|
||||||
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
|
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:tripz_driver/controller/functions/country_logic.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/box_name.dart';
|
||||||
import '../../../constant/links.dart';
|
import '../../../constant/links.dart';
|
||||||
@@ -66,13 +67,18 @@ class PhoneAuthHelper {
|
|||||||
try {
|
try {
|
||||||
final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber);
|
final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber);
|
||||||
Log.print('fixedPhone: $fixedPhone');
|
Log.print('fixedPhone: $fixedPhone');
|
||||||
|
final fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ??
|
||||||
|
await DeviceHelper.getDeviceFingerprint();
|
||||||
final response = await CRUD().post(
|
final response = await CRUD().post(
|
||||||
link: _verifyOtpUrl,
|
link: _verifyOtpUrl,
|
||||||
payload: {
|
payload: {
|
||||||
'phone_number': fixedPhone,
|
'phone_number': fixedPhone,
|
||||||
'token_code': otpCode,
|
'token_code': otpCode,
|
||||||
'context': 'login',
|
'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.phoneDriver, phoneNumber);
|
||||||
box.write(BoxName.driverID, data['message']['driverID']);
|
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) {
|
if (isRegistered) {
|
||||||
// ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية
|
// ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية
|
||||||
final driver = data['message']['driver'];
|
final driver = data['message']['driver'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
|
|
||||||
import '../../models/model/onboarding_model.dart';
|
import '../../models/model/onboarding_model.dart';
|
||||||
import '../../views/auth/captin/login_captin.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_cropper/image_cropper.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:tripz_driver/constant/links.dart';
|
import 'package:siro_driver/constant/links.dart';
|
||||||
import 'package:tripz_driver/controller/firebase/notification_service.dart';
|
import 'package:siro_driver/controller/firebase/notification_service.dart';
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
@@ -371,8 +371,7 @@ class RegistrationController extends GetxController {
|
|||||||
final uri = Uri.parse(currentUrl);
|
final uri = Uri.parse(currentUrl);
|
||||||
final request = http.MultipartRequest('POST', uri);
|
final request = http.MultipartRequest('POST', uri);
|
||||||
|
|
||||||
final _jwt = box.read(BoxName.jwt);
|
final String _token = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
|
|
||||||
|
|
||||||
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
String nonce = timestamp;
|
String nonce = timestamp;
|
||||||
@@ -581,8 +580,8 @@ class RegistrationController extends GetxController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// ترويسات مشتركة
|
// ترويسات مشتركة
|
||||||
final _jwt = box.read(BoxName.jwt);
|
final String _jwtToken = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final bearer = 'Bearer ${_jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''}';
|
final bearer = 'Bearer $_jwtToken';
|
||||||
final hmac = '${box.read(BoxName.hmac)}';
|
final hmac = '${box.read(BoxName.hmac)}';
|
||||||
|
|
||||||
String fingerPrint =
|
String fingerPrint =
|
||||||
@@ -612,7 +611,7 @@ class RegistrationController extends GetxController {
|
|||||||
_addField(fields, 'password', 'generated_password_or_token');
|
_addField(fields, 'password', 'generated_password_or_token');
|
||||||
_addField(fields, 'status', 'yet');
|
_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, 'email', generatedEmail);
|
||||||
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
|
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
|
||||||
_addField(fields, 'country', box.read(BoxName.countryCode)?.toString() ?? 'Jordan');
|
_addField(fields, 'country', box.read(BoxName.countryCode)?.toString() ?? 'Jordan');
|
||||||
@@ -764,7 +763,7 @@ class RegistrationController extends GetxController {
|
|||||||
// isLoading.value = true;
|
// isLoading.value = true;
|
||||||
|
|
||||||
// final uri = Uri.parse(
|
// final uri = Uri.parse(
|
||||||
// 'https://tripz.xyz/tripz/auth/driver/register.php',
|
// 'https://intaleq.xyz/siro/auth/driver/register.php',
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// final client = http.Client();
|
// 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)
|
/// 6. Final Submission (register_driver_and_car.php → Gemini bulk AI → DB insert → FCM)
|
||||||
/// 7. Admin Review & Activation
|
/// 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 {
|
void main() async {
|
||||||
print('═══════════════════════════════════════════════════════');
|
print('═══════════════════════════════════════════════════════');
|
||||||
print(' SYRIA DRIVER REGISTRATION SIMULATION');
|
print(' SYRIA DRIVER REGISTRATION SIMULATION');
|
||||||
print('═══════════════════════════════════════════════════════');
|
print('═══════════════════════════════════════════════════════');
|
||||||
|
|
||||||
const host = 'api-syria.tripzmove.com';
|
const host = 'api-syria.siromove.com';
|
||||||
const protocol = 'https';
|
const protocol = 'https';
|
||||||
const driverId = 'DRV2026061912345678';
|
const driverId = 'DRV2026061912345678';
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
print(' ✅ AI extraction: $expectedExtraction');
|
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(' MIME validation: image/jpeg, image/png, image/webp');
|
||||||
print(' Max size: 5 MB');
|
print(' Max size: 5 MB');
|
||||||
|
|
||||||
final signedUrl = '$protocol://$host/tripz/secure_image.php'
|
final signedUrl = '$protocol://$host/siro/secure_image.php'
|
||||||
'?driver_id=$driverId'
|
'?driver_id=$driverId'
|
||||||
'&doc_type=$docType'
|
'&doc_type=$docType'
|
||||||
'&ext=jpg'
|
'&ext=jpg'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
class CarPlatformBridge {
|
class CarPlatformBridge {
|
||||||
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;
|
static bool _isInitialized = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:tripz_driver/controller/home/captin/home_captain_controller.dart';
|
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||||
import 'package:tripz_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
|
import 'package:siro_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
|
||||||
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
|
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:tripz_driver/views/widgets/mydialoug.dart';
|
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||||
import 'package:tripz_driver/controller/voice_call_controller.dart';
|
import 'package:siro_driver/controller/voice_call_controller.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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/box_name.dart';
|
||||||
import '../../constant/colors.dart';
|
import '../../constant/colors.dart';
|
||||||
@@ -233,7 +233,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
|
|
||||||
case 'token change':
|
case 'token change':
|
||||||
case 'TOKEN_CHANGE':
|
case 'TOKEN_CHANGE':
|
||||||
box.remove(BoxName.jwt);
|
await storage.delete(key: BoxName.jwt);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'face detect':
|
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:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
@@ -15,8 +15,13 @@ import '../../constant/box_name.dart';
|
|||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
import '../../main.dart'; // للوصول لـ box
|
import '../../main.dart'; // للوصول لـ box
|
||||||
import '../../print.dart';
|
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/driver_map_page.dart';
|
||||||
import '../../views/home/Captin/orderCaptin/order_request_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 '../functions/crud.dart';
|
||||||
import '../home/captin/home_captain_controller.dart';
|
import '../home/captin/home_captain_controller.dart';
|
||||||
|
|
||||||
@@ -213,6 +218,14 @@ class NotificationController extends GetxController {
|
|||||||
if (payload == null) return;
|
if (payload == null) return;
|
||||||
|
|
||||||
final payloadData = jsonDecode(payload) as Map<String, dynamic>;
|
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'];
|
final rawData = payloadData['data'];
|
||||||
|
|
||||||
List<dynamic> listData = [];
|
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)
|
// 4. منطق القبول الآمن (Safe Accept Logic)
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:tripz_driver/constant/links.dart';
|
import 'package:siro_driver/constant/links.dart';
|
||||||
import 'package:tripz_driver/controller/functions/crud.dart';
|
import 'package:siro_driver/controller/functions/crud.dart';
|
||||||
import 'package:tripz_driver/views/home/Captin/home_captain/home_captin.dart';
|
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||||
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
|
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class OverlayContent1 extends StatelessWidget {
|
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() {
|
void _showStoreUpdateDialog() {
|
||||||
final String storeUrl = Platform.isAndroid
|
final String storeUrl = Platform.isAndroid
|
||||||
? 'https://play.google.com/store/apps/details?id=com.tripz_driver'
|
? AppLink.driverAppStoreAndroid
|
||||||
: 'https://apps.apple.com/jo/app/tripz-driver/id6482995159';
|
: AppLink.driverAppStoreIOS;
|
||||||
|
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: "تحديث جديد متوفر".tr,
|
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 'package:get_storage/get_storage.dart';
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
|
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
|
||||||
import '../firebase/local_notification.dart';
|
import '../firebase/local_notification.dart';
|
||||||
|
|
||||||
const String notificationChannelId = 'driver_service_channel';
|
const String notificationChannelId = 'driver_service_channel';
|
||||||
@@ -136,11 +137,172 @@ Future<bool> onStart(ServiceInstance service) async {
|
|||||||
payload: jsonEncode(data));
|
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) {
|
service.on('stopService').listen((event) {
|
||||||
socket?.clearListeners();
|
socket?.clearListeners();
|
||||||
socket?.dispose();
|
socket?.dispose();
|
||||||
|
foodSocket?.clearListeners();
|
||||||
|
foodSocket?.dispose();
|
||||||
service.stopSelf();
|
service.stopSelf();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,6 +347,14 @@ Future<bool> onStart(ServiceInstance service) async {
|
|||||||
return true;
|
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 {
|
class BackgroundServiceHelper {
|
||||||
static Future<void> initialize() async {
|
static Future<void> initialize() async {
|
||||||
final service = FlutterBackgroundService();
|
final service = FlutterBackgroundService();
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import 'dart:io';
|
|||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
|
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/constant/links.dart';
|
import 'package:siro_driver/constant/links.dart';
|
||||||
import 'package:tripz_driver/views/widgets/elevated_btn.dart';
|
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/constant/country_polygons.dart';
|
import 'package:siro_driver/constant/country_polygons.dart';
|
||||||
import 'package:tripz_driver/constant/city_polygons.dart';
|
import 'package:siro_driver/constant/city_polygons.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
|
|
||||||
class CountryLogic {
|
class CountryLogic {
|
||||||
@@ -84,7 +84,7 @@ class CountryLogic {
|
|||||||
/// Helper to format phone using the current country in box.
|
/// Helper to format phone using the current country in box.
|
||||||
static String formatCurrentCountryPhone(String phone) {
|
static String formatCurrentCountryPhone(String phone) {
|
||||||
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
|
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
|
||||||
|
|
||||||
// 1. Explicit International Code Detection
|
// 1. Explicit International Code Detection
|
||||||
if (cleanPhone.startsWith('00963')) {
|
if (cleanPhone.startsWith('00963')) {
|
||||||
cleanPhone = cleanPhone.replaceFirst('00963', '963');
|
cleanPhone = cleanPhone.replaceFirst('00963', '963');
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:tripz_driver/controller/functions/encrypt_decrypt.dart';
|
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
|
||||||
import 'package:tripz_driver/controller/functions/network/net_guard.dart';
|
import 'package:siro_driver/controller/functions/network/net_guard.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/constant/links.dart';
|
import 'package:siro_driver/constant/links.dart';
|
||||||
import 'package:tripz_driver/controller/auth/captin/login_captin_controller.dart';
|
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:tripz_driver/env/env.dart';
|
import 'package:siro_driver/env/env.dart';
|
||||||
import 'package:tripz_driver/print.dart';
|
import 'package:siro_driver/print.dart';
|
||||||
import 'package:tripz_driver/controller/functions/package_info.dart';
|
import 'package:siro_driver/controller/functions/package_info.dart';
|
||||||
|
|
||||||
import '../../constant/api_key.dart';
|
import '../../constant/api_key.dart';
|
||||||
import '../../views/widgets/error_snakbar.dart';
|
import '../../views/widgets/error_snakbar.dart';
|
||||||
@@ -22,38 +22,48 @@ import 'ssl_pinning.dart';
|
|||||||
class CRUD {
|
class CRUD {
|
||||||
final NetGuard _netGuard = NetGuard();
|
final NetGuard _netGuard = NetGuard();
|
||||||
final _client = SslPinning.createPinnedClient();
|
final _client = SslPinning.createPinnedClient();
|
||||||
static bool _isRefreshingJWT = false;
|
|
||||||
static String _lastErrorSignature = '';
|
static String _lastErrorSignature = '';
|
||||||
static DateTime _lastErrorTimestamp = DateTime(2000);
|
static DateTime _lastErrorTimestamp = DateTime(2000);
|
||||||
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
||||||
|
|
||||||
// ── فحص صلاحية JWT بدون مكتبات خارجية ──────────────────────
|
// ── فحص صلاحية JWT بدون مكتبات خارجية ──────────────────────
|
||||||
static bool _isJwtValid(String? token) {
|
static bool _isJwtValid(String? token) {
|
||||||
if (token == null || token.isEmpty) return false;
|
final decoded = LoginDriverController.decodeJwtPayload(token);
|
||||||
try {
|
if (decoded == null) return false;
|
||||||
final parts = token.split('.');
|
|
||||||
if (parts.length != 3) return false;
|
final exp = decoded['exp'];
|
||||||
// فك تشفير الـ payload (الجزء الثاني)
|
if (exp is! int) return false;
|
||||||
String payload = parts[1];
|
// نعتبر التوكن منتهي قبل 30 ثانية من انتهاء الصلاحية (buffer)
|
||||||
// إضافة padding للـ base64
|
if (DateTime.now().millisecondsSinceEpoch >= (exp * 1000 - 30000)) {
|
||||||
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 (_) {
|
|
||||||
return false;
|
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(
|
static Future<void> addError(
|
||||||
String error, String details, String where) async {
|
String error, String details, String where) async {
|
||||||
try {
|
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 — دالة مركزية لكل الطلبات
|
// _makeRequest — دالة مركزية لكل الطلبات
|
||||||
// ───────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────
|
||||||
@@ -119,6 +141,7 @@ class CRUD {
|
|||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
required Map<String, String> headers,
|
required Map<String, String> headers,
|
||||||
|
bool allowRefresh = true,
|
||||||
}) async {
|
}) async {
|
||||||
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
|
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
|
||||||
const totalTimeout = Duration(seconds: 60);
|
const totalTimeout = Duration(seconds: 60);
|
||||||
@@ -186,19 +209,34 @@ class CRUD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
|
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
|
||||||
if (sc == 401) {
|
if (sc == 429) {
|
||||||
|
Log.print('🛑 [RES-$requestId] 429 rate limited — $link');
|
||||||
|
return 'rate_limited';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401 (منتهي) أو 403 بتوكن تسجيل (بلا صلاحيات) →
|
||||||
|
// تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
|
||||||
|
if (sc == 401 || (sc == 403 && _isUnauthorizedTokenType(body))) {
|
||||||
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
|
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
|
||||||
final isNonCritical = link.contains('errorApp.php');
|
final isNonCritical = link.contains('errorApp.php');
|
||||||
if (!_isRefreshingJWT && !isNonCritical) {
|
if (isNonCritical || !allowRefresh) return 'token_expired';
|
||||||
_isRefreshingJWT = true;
|
|
||||||
try {
|
final refreshed = await Get.put(LoginDriverController()).getJWT();
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
if (!refreshed) return 'token_expired';
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
final newToken = await _getJwt();
|
||||||
}
|
if (newToken.isEmpty) return 'token_expired';
|
||||||
}
|
|
||||||
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
|
// 5xx
|
||||||
@@ -218,17 +256,10 @@ class CRUD {
|
|||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
String token = await _getJwt();
|
String token = await _ensureJwt();
|
||||||
|
if (token.isEmpty && !_isPublicEndpoint(link)) {
|
||||||
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
|
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
|
||||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
return 'token_expired';
|
||||||
_isRefreshingJWT = true;
|
|
||||||
try {
|
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
|
||||||
token = await _getJwt();
|
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
@@ -247,19 +278,11 @@ class CRUD {
|
|||||||
Future<dynamic> get({
|
Future<dynamic> get({
|
||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
|
bool allowRefresh = true,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// فحص صلاحية التوكن قبل الإرسال
|
// فحص صلاحية التوكن قبل الإرسال
|
||||||
String token = await _getJwt();
|
final String token = await _ensureJwt();
|
||||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
|
||||||
_isRefreshingJWT = true;
|
|
||||||
try {
|
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
|
||||||
token = await _getJwt();
|
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = Uri.parse(link);
|
var url = Uri.parse(link);
|
||||||
var response = await _client.post(
|
var response = await _client.post(
|
||||||
@@ -279,16 +302,21 @@ class CRUD {
|
|||||||
var jsonData = jsonDecode(response.body);
|
var jsonData = jsonDecode(response.body);
|
||||||
if (jsonData['status'] == 'success') return response.body;
|
if (jsonData['status'] == 'success') return response.body;
|
||||||
return jsonData['status'];
|
return jsonData['status'];
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401 ||
|
||||||
if (!_isRefreshingJWT) {
|
(response.statusCode == 403 &&
|
||||||
_isRefreshingJWT = true;
|
_isUnauthorizedTokenType(response.body))) {
|
||||||
try {
|
// تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
// (allowRefresh: false تمنع أي تكرار إضافي) — نفس سلوك _makeRequest.
|
||||||
} finally {
|
if (!allowRefresh) return 'token_expired';
|
||||||
_isRefreshingJWT = false;
|
|
||||||
}
|
final refreshed = await Get.put(LoginDriverController()).getJWT();
|
||||||
}
|
if (!refreshed) return 'token_expired';
|
||||||
return 'token_expired';
|
|
||||||
|
return await get(
|
||||||
|
link: link,
|
||||||
|
payload: payload,
|
||||||
|
allowRefresh: false,
|
||||||
|
);
|
||||||
} else if (response.statusCode >= 500) {
|
} else if (response.statusCode >= 500) {
|
||||||
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
|
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
|
||||||
url.toString());
|
url.toString());
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:tripz_driver/constant/api_key.dart';
|
import 'package:siro_driver/constant/api_key.dart';
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import 'dart:io';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
|
|
||||||
import 'package:tripz_driver/constant/box_name.dart';
|
import 'package:siro_driver/constant/box_name.dart';
|
||||||
import 'package:tripz_driver/constant/info.dart';
|
import 'package:siro_driver/constant/info.dart';
|
||||||
import 'package:tripz_driver/constant/links.dart';
|
import 'package:siro_driver/constant/links.dart';
|
||||||
import 'package:tripz_driver/constant/style.dart';
|
import 'package:siro_driver/constant/style.dart';
|
||||||
import 'package:tripz_driver/controller/auth/captin/login_captin_controller.dart';
|
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||||
import 'package:tripz_driver/controller/firebase/firbase_messge.dart';
|
import 'package:siro_driver/controller/firebase/firbase_messge.dart';
|
||||||
import 'package:tripz_driver/controller/firebase/local_notification.dart';
|
import 'package:siro_driver/controller/firebase/local_notification.dart';
|
||||||
import 'package:tripz_driver/controller/functions/crud.dart';
|
import 'package:siro_driver/controller/functions/crud.dart';
|
||||||
import 'package:tripz_driver/env/env.dart';
|
import 'package:siro_driver/env/env.dart';
|
||||||
import 'package:tripz_driver/main.dart';
|
import 'package:siro_driver/main.dart';
|
||||||
import 'package:tripz_driver/views/widgets/error_snakbar.dart';
|
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:tripz_driver/views/widgets/mydialoug.dart';
|
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_cropper/image_cropper.dart';
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
@@ -479,8 +479,7 @@ class AI extends GetxController {
|
|||||||
bool isLoadingVehicleFrontSy = false;
|
bool isLoadingVehicleFrontSy = false;
|
||||||
bool isLoadingVehicleBackSy = false;
|
bool isLoadingVehicleBackSy = false;
|
||||||
Future<void> sendToAI(String type, {required File imageFile}) async {
|
Future<void> sendToAI(String type, {required File imageFile}) async {
|
||||||
final _jwt = box.read(BoxName.jwt);
|
final String _token = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Authorization': 'Bearer $_token',
|
'Authorization': 'Bearer $_token',
|
||||||
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||||
|
|||||||