Compare commits

...
33 Commits
Author SHA1 Message Date
Hamza-Ayed 0ff7d04f9a Update codebase 2026-08-11 00:11:23 +03:00
Hamza-Ayed 6726fcd917 Update codebase 2026-08-09 17:45:22 +03:00
Hamza-Ayed b64debaa88 Update codebase 2026-08-09 16:56:13 +03:00
Hamza-Ayed 95e2e4f35d Update codebase 2026-08-05 14:12:41 +03:00
Hamza-Ayed 58928cd258 Update codebase 2026-08-05 05:35:16 +03:00
Hamza-AyedandClaude Opus 5 6ea213629c fix(apps): سقوط أصلي عند تحميل الخريطة — ستايل map-saas بلا sprite
## العطل
libc++abi: terminating due to uncaught exception of type std::domain_error
بلا أي استثناء دارتي — سقوط أصلي بحت بعد «Using remote style URL» مباشرة.

## السبب
فحص الستايل البعيد مباشرةً:
  /api/maps/style.json?theme=light → 68 طبقة، منها 12 طبقة symbol تستعمل
  icon-image، و **لا sprite معرَّف في الملف إطلاقاً**.
MapLibre الأصلي يرمي std::domain_error عند طبقة أيقونات بلا sprite تحلّها.

⚠️ الستايل المرفق بالحزمة نفسها مصابٌ بالعيب ذاته (sprite غائب + 12 طبقة)،
فلا الافتراضي ولا البعيد صالحان. ستايلها الليلي وحده سليم.

## العلاج
التحوّل إلى assets/style.json و assets/style_dark.json المنقولين من سيرو
الميداني: بهما sprite صحيح، وبلاطاتهما من tiles.intaleqapp.com نفسها،
ويقلعان بلا انتظار شبكة — وهو ما يعنيه «الشكل حتمي من أول إقلاع» (docs/26 §1).
نُسخ الملفان لتطبيق السائق أيضاً.

## الإصلاح الجذري (خادم، خارج هذا الكوميت)
إضافة sprite لستايل map-saas البعيد أو إزالة طبقات الأيقونات منه.

docs/23 §13 وُسّع بالقاعدة وسببها.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 01:51:03 +03:00
Hamza-Ayed da5cfab38e Update codebase 2026-08-05 01:48:11 +03:00
Hamza-AyedandClaude Opus 5 e2e663dd61 fix(apps): إسقاط التطبيق عند توليد الخريطة — النقاط صارت طبقة فلاتر
## العطل
Unhandled Exception: This Annotation Manager has not been initialized
  → didUpdateWidget → diffCircles → addCircle
  → libc++abi: std::domain_error → SIGABRT

## السبب البنيوي
MapLibre يُنشئ مديري التعليقات (circle/symbol/line) **عند تحميل الستايل
وحده**، ويهدمهم عند كل إعادة تحميل — وتبديل الوضع الليلي إعادةُ تحميل.
وحارسٌ زمنيّ لا يكفي: didUpdateWidget في الحزمة يستدعي diffCircles بمجرّد
وجود المتحكّم، وهو موجود قبل تحميل الستايل وأثناء إعادة تحميله. أي أن أي
نافذة زمنية نتركها ستُصاب عاجلاً أو آجلاً.

## العلاج — إزالة صنف العطل لا ترقيعه
كل ما يُرسم فوق الخريطة صار **طبقة فلاتر** في Stack: نقطة البداية والنهاية
(MapPin) والأسماء (MapLabel) وأيقونة السائق، بمواضع من getScreenCoordinate.
لا annotation manager، فلا عطل — ومعها نرث الخط العربي والاتجاه والثيم.

Polyline وحده بقي annotation (لا بديل لرسم المسار)، محروساً بعَلَم _styleReady
يُصفَّر عند كل تغيير styleUrl.

## أداء
onCameraMove يُطلق عشرات المرات في الثانية وكل مزامنة ثلاثة نداءات منصّة —
أُضيف حارس _syncing يمنع تكدّسها.

## توثيق
docs/23 §13 قاعدة جديدة: ممنوع markers/circles/polygons مع IntaleqMap.
كُتبت كي لا تتكرر في أي شاشة خريطة قادمة.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 01:43:51 +03:00
Hamza-AyedandClaude Opus 5 ca766a2d4e feat(apps): الدراور على نمط سيرو — المكافآت والدعوات والمساعدة
جُرد دراور سيرو: 14 مدخلاً للراكب (map_menu_widget) و23 للسائق
(drawer_captain). بُني ما له نقطة فعلية على NestJS، والباقي مذكور صراحةً.

## مبنيّ
- **المكافآت** (features/rewards): GET /rewards/referrals و /rewards/coupons —
  كلاهما مُتحقَّق حيّاً. كود الدعوة يُنسخ ويُشارك، وعدّادات دعوتَهم/قيد
  التأكيد/مدفوعة. يقابل Promos + Invite Driver في سيرو
- **شارك التطبيق · قيّم التطبيق · سياسة الخصوصية · تطبيق الطرف الآخر** —
  أفعال محليّة بلا نقطة خادم (share_plus + url_launcher)
- **المساعدة** وُسّعت: أسئلة شائعة قابلة للطيّ · واتساب الدعم ·
  **فحص توافق الجهاز** (نمط سيرو) يفحص ما يُعطّل التطبيق فعلاً: خدمة الموقع
  وإذنها وإذن الإشعارات — أسباب حقيقية لشكوى «التطبيق لا يعمل»
- روابط المتجر والخصوصية والدعم صارت قيم بناء لكل مستأجر في BuildConfig

## غير مبنيّ — ولا نقطة خادم له
الشكاوى (R3 في الباكلوج) · الإحصاءات · التحدّيات ولوحة المتصدّرين · مركز
الصيانة · جدولي · التأمين الصحي (مؤجَّل بقرار المالك) · الطعام والمواصلات
(خارج النطاق، docs/39 §1). بناء واجهة بلا خادم يعني شاشة تكذب على المستخدم.

## سجلّ الإشعارات
سيرو يعرض قائمة إشعارات؛ الخادم يوفّر POST /notifications/token فقط بلا نقطة
سرد. لم تُبنَ شاشة قائمة على فراغ.

flutter analyze نظيف في التطبيقين.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 01:34:23 +03:00
Hamza-AyedandClaude Opus 5 6a33cc7241 fix(rider): البحث عن مكان والمحفظة الحقيقية وأسماء الخريطة
## البحث لم يكن يستجيب — غلافان مختلفان
مثبت بـcurl: /geocoding/search يرجّع {"results":[…]} بينما /geocoding/reverse
يرجّع مصفوفة عارية. افترضتُ كليهما مصفوفة، فكان البحث يفشل صامتاً — ولهذا
«تحديد من الخريطة» يعمل (يستعمل reverse) والبحث لا. أُضيفت رسالة «لا نتائج»
كي لا يبقى الفراغ غامضاً، وأيقونة لكل صنف مكان.

## المحفظة صارت دفعاً حقيقياً لا كاش-إن
كنت أستعمل POST /wallet/topup — وهو بوصف الخادم كاش-إن تجريبي يزيد الرصيد
بلا دفع. المسار الصحيح موجود أصلاً: POST /payments/charge مع provider
وpurpose، وسجلّ بوابات فيه cash · paymob · syriatel · mtn ومحوّل فاتورة
لـ cliq · shamcash · zaincash.

كليك وشام كاش وزين كاش تعمل بنمط **الفاتورة** — نفس ما يفعله سيرو حرفياً
(create_cliq_invoice.php): الخادم يولّد مرجعاً ويعرض حساب الاستلام، ويحوّل
المستخدم، والتسوية تصل لاحقاً بمطابقة رسالة المزوّد. لا بوابة تحويل حيّة.
- InvoiceSheet يعرض المرجع وحساب الاستلام بزرّي نسخ — الرقم يُحوَّل في تطبيق
  بنك آخر فالنسخ ليس رفاهية
- يُقال صراحةً أن الرصيد لا يزيد الآن، قبل الضغط لا بعده
- حساب استلام غير مضبوط للمستأجر يُعرض كخطأ لا كتعليمات ناقصة
- أسماء المزوّدين صُحّحت لتطابق PaymentGatewayRegistry حرفياً (syriatel لا
  syriatel_cash…) — أي اسم آخر يرجّع 400
- qi_card حُذف: لا محوّل له على الخادم
- السائق: chargeCredit بنيّة credit_topup للرصيد التشغيلي

## الخريطة وواجهة «إلى أين»
- أسماء البداية والنهاية تُرسم فوق الخريطة (نمط سيرو) كطبقة فلاتر لا marker
- البطاقة المطوية صارت حقل بحث بارزاً مع الموقع الحالي فوقه
- حقل البحث يفتح مركّزاً مع زرّ مسح

## أداة
tool/sync_from_rider.sh كان يستثني ملفَّي ARB فيُسقط كل مفتاح مشترك جديد
ويكسر بناء السائق — تكرّر ثلاث مرات. صارا يُنسخان ثم تُفرض نصوص السائق فوقهما.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 01:26:05 +03:00
Hamza-AyedandClaude Opus 5 e22cc5c7e4 feat(apps): ثيم سيرو ومفتاح الدولة والعراق
## الثيم مقرَّب من سيرو (قرار المالك 2026-08-05)
- البذرة صارت كحليّ سيرو #1A2340 بدل الأزرق العام، والثانوي #8C9CF8
- fromSeed وحدها تنتج من بذرة داكنة أسطحاً باهتة، فثُبّت الأساسي والثانوي
  والأسطح صراحةً وتُرك الباقي للمشتقّ — شكل سيرو مع تباين M3
- الأدوار الدلالية بأرقام سيرو الميدانية: نجاح #22C55E · تحذير #F59E0B ·
  خطأ #EF4444، وبطاقة الليلي #2C2C2E

## مفتاح الدولة عاد
core/ui/phone_field.dart — النمط المجرَّب في سيرو، لكن بلا intl_phone_field:
تلك الحزمة تحمل 240 دولة ونحتاج أربعاً. CountryCode صريح ومقصور على أسواقنا.
- الاختيار من ورقة سفلية لا قائمة منسدلة: أهداف لمس أكبر وأربع دول لا مئتان
- الترتيب يبدأ بحزمة دولة المستأجر — الأردنيّ يرى الأردن أولاً بلا بحث
- الرقم يُرسل كاملاً بالمفتاح بلا + (962790000001) كما يقبله الخادم
- التحقّق صار بطول الدولة لا برقم ثابت: jo/sy تسع خانات، eg/iq عشر

## العراق
- CountryCode.iraq (+964) في قائمة الهاتف
- قنوات دفعه: زين كاش · كي كارد · نقد
⚠️ الباك إند لا يعرف حزمة `iq`: default-tariffs.ts فيه jo/sy/eg فقط، فمستأجر
عراقي يُزوَّد بلا أي تعرفة. موثَّق في الكود — وهو شغل خادم يسبق أي إطلاق عراقي.

flutter analyze نظيف في التطبيقين.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 01:08:18 +03:00
Hamza-AyedandClaude Opus 5 3a8caf7b27 fix(rider): تعطيل أنواع الرحلات بلا تعرفة نشطة
فحص حيّ كشف أن `GET /ride-types` يرجّع ستّة أنواع للمستأجر siro بينما ثلاثة
فقط لها تعرفة نشطة:
  economy 1.55 · comfort 1.85 · electric 1.75  ✅
  family_van · women · scooter                 ❌ 404

أي أن الراكب يرى ستّة خيارات، وثلاثة منها لو اختارها يفشل طلب الرحلة بـ
«No pricing available» بعد أن يكون قد أكمل تحديد وجهته.

المعالجة في التطبيق: النوع بلا تعرفة يظهر **معطّلاً مكتوباً عليه «غير متاح»**
لا مخفياً — إخفاؤه يترك المستخدم يتساءل أين ذهب الخيار، وإظهاره قابلاً
للاختيار يوصله إلى فشل بعد خطوتين. وإن كان المختار بلا تعرفة يُنتقل تلقائياً
لأول نوع مسعَّر، فلا يبقى زرّ التأكيد معطّلاً بلا سبب ظاهر.

هذا علاج للعَرَض؛ السبب بيانات على الخادم: كتالوج ride-types والتعرفات غير
متزامنين.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:39:30 +03:00
Hamza-AyedandClaude Opus 5 8976821a93 fix(apps): إصلاحات من أول تجربة حقيقية على الجهاز
## تعذّر حساب الأجرة — الخطأ في التطبيق لا في جدول الكازان
الخادم يبذر التعرفة بـ city: 'default' (tenants.service.ts:156) ويستعمل
dto.city ?? 'default' عند إنشاء الرحلة، وكنت أرسل city: '' فيرجّع 404
«No active tariff for /economy». تحقّق حيّ:
  city='default' → total 1.55 JOD ✅   city='' → 404 ❌   city='amman' → 404 ❌
لا تغيير مطلوب على السيرفر. أُضيف AppConfig.defaultCity موثّقاً بالسبب.

## التسعير مرة واحدة لكل الأنواع
كان نداء شبكة عند كل ضغطة نوع. صار _priceAll يسعّر كل الأنواع بالتوازي بعد
اكتمال المسار — المسافة والمدة ثابتتان والفرق في التعرفة وحدها. الراكب يرى
سعر الاقتصادي والمريح معاً تحت كل خيار ويقارن بنظرة، وتبديل النوع بلا نداء.

## الكاميرا لا تتبع الموقع
سببان: الموقع يصل قبل إنشاء الخريطة فيضيع التحريك، ولا فحص للإذن/الخدمة قبل
getCurrentPosition فيُبتلع الخطأ صامتاً.
- _onCreated يلحق بما فات عند جهوز الخريطة
- LocationService.ensureReady يفحص الخدمة والإذن أولاً
- didChangeAppLifecycleState يعيد المحاولة عند العودة من إعدادات النظام
- حالة noLocation تُعرض للمستخدم مع زر إعادة بدل خريطة صامتة

## الخريطة والتنقّل
- إطار المسار من حدوده الفعلية مع حشوة سفلية بمقدار ما تغطّيه الورقة
- علامتا البداية والنهاية دائرتان ملوّنتان تطابقان ألوان الخط الزمني
- زر رجوع في ورقة التأكيد + زر إعادة حساب عند فشل التسعيرة: كان المستخدم
  يعلق في الشاشة بلا مخرج

## المحفظة — على أي أساس يُشحن
POST /wallet/topup شحنٌ تجريبي بلا مزوّد دفع خلفه (الخادم نفسه يصفه كذلك).
أُضيف PaymentChannel مشتقّاً من countryPack المستأجر لا من لغة الجهاز:
jo → CliQ · sy → سيرياتيل/MTN/شام كاش · eg → PayMob. ويُعرض تنبيه صريح بأن
الشحن بلا تحصيل فعلي — إخفاؤه يجعل المستخدم يظنّ أنه دفع. وسحب السائق صار
يستعمل القناة المختارة بدل 'cliq' المكتوبة في الكود.

flutter analyze نظيف في التطبيقين.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:36:02 +03:00
Hamza-Ayed 29f375b122 Update codebase 2026-08-05 00:28:04 +03:00
Hamza-Ayed 36833b039d Update codebase 2026-08-05 00:07:11 +03:00
Hamza-AyedandClaude Opus 5 bb2c0de478 feat(apps): تسجيل السائق والدردشة والكوبون — إكمال التغطية
فحص آليّ لنقاط docs/38 مقابل الكود كشف ثغرات لم تظهر بالقراءة.

## تسجيل السائق — أخطر ثغرة
لم يكن موجوداً أصلاً: سائق جديد يسجّل دخوله ثم يقف. بُنيت features/onboarding
كاملة — تقديم ← ملف ← مركبة ← وثائق ← انتظار الاعتماد.
- الخطوة تُشتقّ من حالة الخادم لا من تقدّم محلّي: سائق يعيد تثبيت التطبيق
  يعود إلى حيث وقف لا إلى البداية
- الاعتماد يقع على الخادم تلقائياً حين تكتمل الوثائق؛ التطبيق لا يعتمد أحداً
- عند الاعتماد خروج إجباري: الدور يتغيّر على الخادم والتوكن القديم يحمل
  القديم، فتفشل نقاط السائق بـ403 (مصيدة docs/38 §5)
- بوابة توجيه: مستخدم دوره ليس driver يُحجز في /onboarding
- الوثائق تُصوَّر بالكاميرا لا من المعرض: أصعب تزويراً

## الدردشة والكوبون
- Features.chat كان مفعّلاً بلا ميزة. features/chat باستطلاع كل خمس ثوان —
  قائمة أحداث الخادم لا تتضمّن الرسائل (docs/38 §9)، فالاستطلاع قيد خادم لا
  اختيار تصميمي
- الكوبون: حقل في ورقة التأكيد خلف طبقتَي الميزات، يُقيَّم على الخادم

## Features.calls أُطفئ صراحةً
الخادم يدعم WebRTC والتطبيق لا. عَلَم مفعّل بلا ميزة كذبٌ على القارئ التالي.

## بنية
- ApiClient.upload للرفع متعدّد الأجزاء
- AuthFailure → ApiFailure في core/api: يخدم المصادقة والملف والتسجيل
- tool/sync_from_rider.sh: المزامنة اليدوية بين التوأمين انكسرت أربع مرات،
  فصارت سكربتاً واحداً يعرّف ما يملكه كل تطبيق

flutter analyze نظيف · الراكب 101 ملف/7,809 سطر · السائق 110 ملف/8,116 سطر.
فحوص آلية: صفر استيراد بين ميزتين · صفر عَلَم مفعّل بلا ميزة · كل نقاط العقد
المخصّصة للتطبيقين مستهلكة.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:04:28 +03:00
Hamza-AyedandClaude Opus 5 096acae74e feat(apps): الدراور وطبقتا الميزات والإشعارات — المرحلة 6
## طبقتا الميزات مُطبَّقتان معاً
AppDrawer يفحص: Features.wallet && context.tenantHas('wallet')
- Features.x ثابت const: يقرّر ما يُبنى في الـbinary. إطفاؤه يحذف الكود
  فلا يُستخرج (docs/23 §5)
- tenantHas من GET /tenant/config/:slug: يقرّر ما يُعرض ممّا بُني حسب اشتراك
  المستأجر (docs/38 §10)
هذا هو ربط lite/pro/max عملياً. TenantCubit يعرض النسخة المخزّنة فوراً ثم
يحدّثها: إقلاع بلا إنترنت يعرض الميزات المعروفة آخر مرة لا شاشة فارغة.

## الشاشات
الدراور · تعديل الحساب · سجلّ الرحلات · الإعدادات (مظهر + لغة) · المساعدة.
الرقم في تعديل الحساب معطّل مع سبب معروض لا مخفي: هو هوية الحساب على الخادم
وتغييره يعني حساباً آخر (docs/38 §1).

## الإشعارات
PushService معزول — الملف الوحيد الذي يعرف Firebase. التسجيل بعد إثبات
الجلسة عبر خطّاف في SessionCubit، ومع اشتراك onTokenRefresh: بدونه تتوقّف
الإشعارات بصمت بعد أسابيع. الإعداد الأصلي كان جاهزاً مسبقاً.

## تصحيحات بنيوية كشفها فحص آلي
docs/23 §1 يمنع أن تستورد ميزة من أخرى — وكانت خمس مخالفات:
1. features/history أُدمجت في features/trip — السجلّ جزء من ميزة الرحلة
2. AppDrawer صعد إلى core/ui — هيكل تطبيق لا ميزة
3. AuthFailure → core/api/api_failure.dart، ومترجمه → core/ui/failure_text.dart
4. AuthHeader → core/ui/form_header.dart
5. ProfileRepository مستقلّ عن AuthRepository: تعديل الملف ليس مصادقة

flutter analyze نظيف · الراكب 98 ملف/7,497 سطر · السائق 95 ملف/6,931 سطر.
فحوص آلية نظيفة: صفر استيراد بين ميزتين · core يستورد features من di و
router وحدهما · Firebase و intaleq_maps و geolocator و socket_io كلٌّ معزول.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:52:27 +03:00
Hamza-AyedandClaude Opus 5 66e28acc58 feat(apps): التقييم والمحفظتان ومسار الرحلة — المرحلة 5
## التقييم مفروض لا اختياري
SessionCubit يفحص GET /trips/rating/pending بعد إثبات الجلسة، ووجود رحلة
معلّقة يحجز التطبيق كله عند /rating حتى تُحسم. دور المستخدم في الرحلة يأتي
من الخادم لا من نوع التطبيق — نفس الرقم قد يكون له حسابان (docs/38 §1).
التقييم المزدوج يرجّع 400، وهو نجاح من منظور المستخدم: الرحلة مُقيَّمة فعلاً
فلا يُحبس في شاشة بلا مخرج.

الجلسة تحتاج معرفة التقييم المعلّق، و core لا يجوز أن يستورد ميزة التقييم —
فأُضيفت الطريقة لواجهة SessionSource ونفّذها AuthRepository.

## core/format/money.dart
كل المبالغ تصل نصوصاً ("0.000")؛ التحويل والعرض من نقطة واحدة. عدد المنازل
من العملة (JOD ثلاث)، والعملة من إعداد المستأجر لا من ثابت في التطبيق.

## المحفظتان لا تُوحَّدان
- الراكب WalletPage: رصيد + شحن = التزام على المنصّة
- السائق EarningsPage: أرباح + سحب = إيراد له
شكلهما متقارب وحسابهما مختلف؛ دمجهما خطأ محاسبي لا اختصار (docs/39 §4).

## السحب بخطوتين
request يرسل رمزاً بلا خصم، و confirm هو ما يخصم ويحجز. شاشة سيرو كانت خطوة
واحدة ولم تُقلَّد. الرمز الخاطئ لا يُرجع المستخدم لخطوة المبلغ.

الرصيد التشغيلي معروض منفصلاً عن الأرباح: منه تُخصم العمولة ونفاده يوقف
وصول الطلبات — خلطه بالأرباح يخفي سبب توقّف العمل عن السائق.

FareSummary يعرض رقم كلّ طرف: price_for_passenger للراكب و price_for_driver
للسائق — حقلان منفصلان والفرق عمولة (docs/38 §4).

الإلغاء بتأكيد صريح مع سبب؛ السبب لا يقبله عقد الخادم بعد لكن السؤال يخفض
الإلغاء العرضي ويجهّز الحقل.

flutter analyze نظيف · الراكب 86 ملف/6,280 سطر · السائق 83 ملف/5,818 سطر.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:42:07 +03:00
Hamza-AyedandClaude Opus 5 1156299d09 feat(apps): الخريطة وطلب الرحلة ودورة السائق — المرحلة 4
## حسم تعارض الخرائط (docs/38 §7)
لصالح القرار الأحدث 2026-07-20: كل الخرائط — البلاطات والبحث والعكسي
والمسار — مباشرة إلى map-saas بترويسة x-api-key، بلا مرور بباك إند تريبز.
نقاط /maps/* في الباك إند لا تُستعمل، وإحداها (geocode) معطوبة على المنشور.
العقد تُحقّق حيّاً بـcurl قبل كتابة سطر.

## النواة المشتركة
- core/api/antlaq_api.dart: عميل منفصل بلا AuthInterceptor — توكن تريبز لا
  شأن لخادم الخرائط به، وإرساله إليه تسريب بلا مقابل
- core/realtime: Socket.IO واحد (trip:update · driver:location · trip:offer
  · trip:offer_taken)
- core/location: نقطة الموقع الوحيدة بمرشّح 25 متراً
- features/trip/data: سبعة نماذج + MapsRepository + TripRepository متطابق
  حرفياً بين التوأمين

## الراكب
RideCubit بستّ مراحل + RidePage وثمانية ويدجت: الخريطة · دبّوس المنتصف ·
المخطّط · الخط الزمني · اختيار النوع · التأكيد · البحث عن سائق · الرحلة.

## السائق
DutyCubit بأربع مراحل + DutyPage: مفتاح الاتصال · العرض بمؤقّت · الرحلة.

## قرارات تحمي الأداء والصحّة
- GeoPoint مستقلّ عن حزمة الخريطة؛ intaleq_maps مستوردة في ملف واحد فقط
  لكل تطبيق. تبديل محرّك الخريطة لا يلمس منطقاً
- الخريطة خارج BlocBuilder الورقة ولها BlocSelector خاص: نبضة موقع كل 25م
  لا تعيد بناء الشاشة
- مهلة بحث محليّة 90ث + استطلاع كل 5ث: الخادم قد لا يُطلق expired/no_drivers
  أبداً (ثغرة R1)، وانتظار حدث قد لا يصل = شاشة بحث أبدية
- offeredDrivers == 0 جواب نهائي فوري بلا انتظار المهلة
- الأجرة لا تُعرض إلا إن كانت صالحة: /tariff/quote يرجّع 200 بقيم null صامتة
- موقع السائق بمسارين عمداً: REST لمطابقة Redis والسوكت لخريطة الراكب

flutter analyze نظيف · الراكب 70 ملف/5,294 سطر · السائق 67 ملف/4,771 سطر.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:33:56 +03:00
Hamza-AyedandClaude Opus 5 c52338f3bb feat(apps): نظام التصميم والمصادقة الكاملة — المرحلتان 2 و3
## تصحيح بنيوي أولاً
بنية المرحلة 1 كانت تخالف docs/23 §1 المُلزِم. أُعيدت للشجرة المفروضة
حرفياً: app.dart في الجذر · core/{config,build_config,di,router}.dart ·
core/{design,ui,l10n,api,storage,session}/ · features/<f>/{cubit,data,view}/

## المرحلة 2 — نظام التصميم (docs/26)
- core/design: tokens (مسافات/زوايا/حركة/أحجام — الشاشة لا تخترع رقماً) ·
  typography (IBM Plex Sans Arabic + Inter محليّان، بأرقام tabular للأسعار
  والعدّادات) · TripzColors كـThemeExtension للأدوار الدلالية ·
  buildTheme(brightness, locale) بمدخلين لا ثالث لهما
- core/ui: 11 مكوّناً. TripzScaffold يحوّل status الـCubit وحده إلى
  skeleton/خطأ/فراغ/محتوى — فلا تكتب أي شاشة if (loading)
- core/l10n: ARB عربي/إنجليزي + gen_l10n. لا نص مرئي داخل widget
- SettingsCubit: المظهر واللغة. الاتجاه يتبع اللغة آلياً بلا Directionality
  مفروضة، وسقف تكبير النص 1.3

## المرحلة 3 — المصادقة
- طبقة الشبكة: تجديد استباقي بطلقة واحدة (عمر التوكن 15 دقيقة) ·
  x-app-role · x-device-id من device_info_plus — يُرسَل الآن كي يُفعَّل علم
  الخادم لاحقاً بلا تعديل التطبيق
- SessionCubit في core/session خلف واجهة SessionSource: الجلسة حالة على
  مستوى التطبيق لا ميزة، و core لا يستورد من features
- LoginCubit بخطواته الخمس، ProfileCubit، وست شاشات:
  الشروط ← إذن الموقع ← الهاتف ← الرمز ← إكمال الملف ← هيكل الرئيسية

من سيرو نُقل السلوك لا الكود: بوابتان قبل أي حقل إدخال · إعادة فحص الإذن عند
العودة من إعدادات النظام · تحقّق الهاتف الثلاثي. وأُضيف ما ينقصه: عدّاد إعادة
إرسال الرمز — بدونه تُحظر ثلاث ضغطات المستخدمَ خمس دقائق (docs/38 §2).

قراران موثّقان في docs/37: لا packages/tripz_ui (الوثيقتان المُلزِمتان تفرضان
core/design و core/ui داخل التطبيق)، والـCubit يُصدر رمز فشل لا نصّاً
(يجمع بين docs/23 §3 و docs/26 §4).

flutter analyze نظيف في التطبيقين · 48 ملف و3,140 سطر لكل تطبيق · الفرق
بينهما أربعة ملفات فقط: build_config · config · ملفّا ARB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:42:19 +03:00
Hamza-AyedandClaude Opus 5 0de8573d27 fix(marketing): تصحيح حاسبة التوفير ونسبة GMV لباقة النمو
- باقة النمو صارت شريحتين: 2% حتى $50k ثم 1.5% حتى $200k (كانت 2% حتى
  $200k)، محدَّثة في الخمس لغات
- مقارنة المنافس صارت 15% من GMV مع حدّ أدنى $0.10 للرحلة، بدل 10%/5%
- استُخرجت tripzMonthly كدالة مستقلة، وأُضيفت crossoverTripsPerDay
- تُعرض الآن نقطة التعادل تحت رقم التوفير، وكذلك حين لا يوجد توفير بعد

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:21:34 +03:00
Hamza-AyedandClaude Opus 5 69da4abc01 feat(apps): سقالة rider_new و driver_new على Cubit — المرحلة 1
بناء من الصفر على باك إند NestJS، بقرار المالك 2026-08-04 الذي يعكس قرارَي
نقل GetX (2026-07-21) واعتماد باك إند PHP (2026-07-27). لا يُورَّث سطر دارت
من apps/*-archive-cubit، وسيرو مرجع بصري وسلوكي لا مصدر نسخ.

الوثائق:
- docs/37: الخطة الكاملة بستّ مراحل وبواباتها
- docs/38: عقد الـAPI من 37 controller، معظمه متحقَّق حيّاً من السيرفر المنشور
- docs/39: جرد 202 شاشة في تطبيقَي سيرو، مصنّفة داخل/خارج النطاق

الطبقة الأصلية منقولة من *-archive-cubit وحدها لأنها هوية النشر:
- rider_new  → com.mobileapp.store.ride · shorebird 496cb3ac
- driver_new → com.sefer_driver (أندرويد) · com.sefer.driver (iOS) · 68cc9345
- أُصلح تعارض: هدف RunnerTests في driver_new كان يحمل bundle الراكب

الأصول مصدرها *-archive-cubit لا سيرو: أصول الأرشيف مجموعة أشمل (كل صور
سيرو + صور تريبز) وخطوطها هي خطوط docs/26. استُكمل السائق بعشرة ملفات
ناقصة من سيرو (شعارات مزوّدي الدفع + صوتان).

lib/ مكتوب من الصفر (11 ملف لكل تطبيق):
- AppConfig بأعلام const — أساس نموذج lite/pro/max
- TokenStore على التخزين الآمن، يقرأ exp محليّاً بلا حزمة خارجية
- AuthInterceptor بتجديد استباقي وطلقة واحدة — عمر التوكن 15 دقيقة فقط
- ApiClient و ApiException يفهم مصفوفة message في NestJS

flutter analyze نظيف في التطبيقين. البناء الفعلي لم يُجرَّب بعد: بوابة
المرحلة 1 تتطلّب البناء على السيرفر، والوصول إليه غير متاح حالياً.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:21:26 +03:00
Hamza-Ayed 1243e5a3b2 chore: initialize new database schema and update marketing tool dependencies 2026-08-04 12:30:54 +03:00
Hamza-Ayed 3b0b257528 تعديل منفذ Nginx في env.example 2026-07-29 03:00:35 +03:00
Hamza-Ayed f428b8b300 تحديث ملف .env.example ببيانات قواعد البيانات الجديدة 2026-07-29 02:58:22 +03:00
Hamza-Ayed 5dcdcd8a3a ضبط بورتات الدوكر لتفادي التعارض مع siro 2026-07-29 02:47:03 +03:00
Hamza-Ayed d34550e1ca إضافة الهوية الجديدة Tripz 2026-07-29 02:41:05 +03:00
Hamza-Ayed b9a97effe5 إضافة الهوية الجديدة Tripz 2026-07-29 02:31:04 +03:00
Hamza-AyedandClaude Opus 5 ecbe245b77 fix(driver): ضمّ حزم get و get_storage داخل المستودع
apps/driver كان يشير إلى `../../Intaleq/packages/get` خارج المستودع — وهو
مسار يُحل من apps/driver إلى Tripz/Intaleq/ غير الموجود، فلا يمرّ
`flutter pub get` إطلاقاً. (فجوة docs/30 §2.6.)

ضُمّت نسخة **انطلق** تحديداً (App/Intaleq/packages) لا فرع apps/rider:
الفرعان يختلفان فعلاً — snackbar_controller.dart و get_queue.dart وغيرهما
مختلفة بين النسختين، فاستعارة فرع rider كانت ستغيّر سلوك السائق صامتة.

  apps/driver/packages/get         (3.5 م.ب، 202 ملف dart مع get_storage)
  apps/driver/packages/get_storage
  pubspec: ../../Intaleq/packages/* → ./packages/*  (3 مواضع، منها
  dependency_overrides سطر 142)

المستودع صار مكتفياً ذاتياً: لا تبعية مسار خارج شجرته. لم يُمسّ App/Intaleq.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:16:10 +03:00
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:14:13 +03:00
Hamza-AyedandClaude Opus 5 9909d9b4c1 chore: أرشفة تطبيقات ولوحات تريبز التي سيحلّ محلها كود سيرو
استكمال قرار 2026-07-27 (كوميت fff76c9 أرشف الباك إند):
  apps/rider          → apps/rider-archive-cubit
  apps/driver         → apps/driver-archive-cubit
  dashboards/admin-web    → dashboards/admin-web-archive
  dashboards/service-web  → dashboards/service-web-archive

إعادة تسمية بلا تعديل محتوى. rider/driver كانا إعادة بناء Cubit مبنية على
عقد NestJS المؤرشف. admin-web و service-web نموذجان بملف HTML واحد (164 و
128 سطراً) — يحلّ محلهما siro_admin و siro_service المكتملان.

يُحمل من المؤرشف ولا يُعاد اكتشافه: عقد map-saas المثبَّت بالتجربة في
apps/rider-archive-cubit/lib/core/api/ (x-api-key ترويسة، reverse مصفوفة،
route في points) ودرس مسار FCM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:12:04 +03:00
Hamza-AyedandClaude Opus 5 fff76c949f chore: أرشفة باك إند NestJS إلى backend-archive
قرار المالك 2026-07-27: الباك إند المعتمد صار باك إند سيرو PHP، ويُنقل
إلى هذا المستودع كنسخة جديدة باسم «انطلق» على api.intaleqapp.com.

backend/ → backend-archive/ (305 ملفاً، إعادة تسمية بلا تعديل محتوى)
+ README-ARCHIVE.md يوضّح سبب الأرشفة وسبب عدم الحذف: scripts/e2e-test.mjs
  هو أدق توثيق سلوكي لدورة الرحلة كاملة، ويبقى مرجعاً عند بناء الوحدات
  الناقصة في باك إند PHP (المحفظتان · الاستحقاقات · محرّك التسعير · الوحدات).

مسارات backend/ في docs/*.md لم تُعدَّل عمداً — سجلّ تاريخي.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 04:54:35 +03:00
Hamza-AyedandClaude Opus 5 75b80bd299 refactor(marketing): تفكيك الموقع إلى وحدات + توحيد الباقات الثلاث
- marketing: index.html من 2274 إلى 775 سطراً، والباقي إلى
  styles/ (tokens·base·components·sections·responsive) و
  js/ (app·i18n·calculator·analytics) و i18n/ بخمس لغات
  (ar·en·fr·es·tr) عبر سمة data-i18n.
- docs/05 + docs/33 + لوحة السوبر-أدمن: الباقات صارت ثلاثاً
  (إطلاق · نمو · سيادة) بدل الأربع، وتحديث docs/31 بما يطابقها.
- docs/36: توثيق سيرو إكسبرس للتوصيل.
- تنظيف استيرادات غير مستعملة في apps/rider و apps/driver.

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

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<resources>
<string name="app_name">android_bot</string>
<string name="accessibility_service_description">Siro Scraper Accessibility Service helps automate competitor price checks.</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Android_bot" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true"
android:description="@string/accessibility_service_description"
android:notificationTimeout="100" />
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">location.intaleq.xyz</domain>
<domain includeSubdomains="true">intaleq.xyz</domain>
<domain includeSubdomains="true">siromove.com</domain>
</domain-config>
</network-security-config>
@@ -0,0 +1,17 @@
package com.siro.android_bot
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+6
View File
@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
+23
View File
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+32
View File
@@ -0,0 +1,32 @@
[versions]
agp = "8.13.2"
kotlin = "2.0.21"
coreKtx = "1.18.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Sun Jun 21 02:56:41 EET 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+10
View File
@@ -0,0 +1,10 @@
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file should *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=/Users/hamzaaleghwairyeen/Library/Android/sdk
+23
View File
@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "android_bot"
include(":app")
@@ -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
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
/build
+64
View File
@@ -0,0 +1,64 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.siro.android_bot"
compileSdk = 36
defaultConfig {
applicationId = "com.siro.android_bot"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BOT_SECRET_KEY", "\"SIRO_BOT_SUPER_SECRET_123\"")
// مضيف الباك إند في مكان واحد: تبديله عند نسخ البوت لعلامة أخرى
// (مثل انطلق على api.intaleqapp.com) يصير سطراً واحداً لا بحثاً في الكود.
buildConfigField("String", "BACKEND_HOST", "\"https://jordan-siro.intaleqapp.com\"")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.siro.android_bot
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.siro.android_bot", appContext.packageName)
}
}
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<package android:name="ae.com.yalla.go.dubai.client" />
<package android:name="com.zakinn.app" />
<package android:name="com.bis.taxi" />
<package android:name="com.careem.acma" />
<package android:name="com.ubercab" />
<package android:name="com.taxif.passenger" />
<package android:name="me.com.easytaxi" />
</queries>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Android_bot"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Android_bot">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".service.ScraperAccessibilityService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>
@@ -0,0 +1,104 @@
package com.siro.android_bot
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.siro.android_bot.ui.theme.Android_botTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN"
enableEdgeToEdge()
setContent {
Android_botTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
BotSetupScreen(
deviceId = androidId,
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun BotSetupScreen(deviceId: String, modifier: Modifier = Modifier) {
val context = LocalContext.current
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Siro Scraper Bot 🤖",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "Your Device ID (Android ID):",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = deviceId,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Black,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
Button(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Device ID", deviceId)
clipboard.setPrimaryClip(clip)
Toast.makeText(context, "Copied to clipboard!", Toast.LENGTH_SHORT).show()
}) {
Text("Copy ID")
}
Spacer(modifier = Modifier.height(48.dp))
Text(
text = "Make sure to add this ID to your Siro Server.\nAlso, remember to enable this app in Settings > Accessibility.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Android_botTheme {
BotSetupScreen("a1b2c3d4e5f6g7h8")
}
}
@@ -0,0 +1,126 @@
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/ من جذر السيرفر المحلي.
private val BASE_URL = "${BuildConfig.BACKEND_HOST}/backend/bot/standalone_worker.php"
private fun generateSignature(deviceId: String, ts: Long): String {
val message = "$deviceId$ts"
val algorithm = "HmacSHA256"
val mac = Mac.getInstance(algorithm)
val secretKeySpec = SecretKeySpec(SECRET_KEY.toByteArray(), algorithm)
mac.init(secretKeySpec)
val hashBytes = mac.doFinal(message.toByteArray())
return hashBytes.joinToString("") { "%02x".format(it) }
}
fun fetchTask(): JSONObject? {
try {
val ts = System.currentTimeMillis() / 1000
val sig = generateSignature(deviceId, ts)
val urlString = "$BASE_URL?device_id=$deviceId&ts=$ts&sig=$sig"
val url = URL(urlString)
Log.d(TAG, "Fetching task from: $urlString")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 10000
connection.readTimeout = 10000
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.readText()
reader.close()
return JSONObject(response)
} else {
Log.e(TAG, "Fetch Error: HTTP $responseCode")
}
} catch (e: Exception) {
Log.e(TAG, "Exception during fetchTask: ${e.message}")
}
return null
}
fun submitPrice(taskId: String, appName: String, startLat: Double, startLng: Double, endLat: Double, endLng: Double, distanceKm: Double, price: Double): Boolean {
try {
val ts = System.currentTimeMillis() / 1000
val sig = generateSignature(deviceId, ts)
val payload = JSONObject().apply {
put("device_id", deviceId)
put("ts", ts)
put("sig", sig)
put("task_id", taskId)
put("type", "price_check")
put("status", "success")
val resultData = JSONObject().apply {
put("app", appName)
put("distance_km", distanceKm)
put("price", price)
put("start_lat", startLat)
put("start_lng", startLng)
put("end_lat", endLat)
put("end_lng", endLng)
}
put("result_data", resultData)
}
val url = URL(BASE_URL)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
connection.doOutput = true
connection.connectTimeout = 10000
connection.readTimeout = 10000
val writer = OutputStreamWriter(connection.outputStream)
writer.write(payload.toString())
writer.flush()
writer.close()
val responseCode = connection.responseCode
return if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
Log.d(TAG, "Submit Success: ${reader.readText()}")
reader.close()
true
} else {
Log.e(TAG, "Submit Error: HTTP $responseCode")
false
}
} catch (e: Exception) {
Log.e(TAG, "Exception during submitPrice: ${e.message}")
return false
}
}
}
@@ -0,0 +1,47 @@
package com.siro.android_bot.service
import android.content.Context
import android.content.Intent
import android.util.Log
object AppLauncher {
private const val TAG = "AppLauncher"
// Maps the server app name to the actual Android package name
// Important: We will verify these exact package names tomorrow once you install them.
private val APP_PACKAGES = mapOf(
"yallago" to "ae.com.yalla.go.dubai.client",
"ae.com.yalla.go.dubai.client" to "ae.com.yalla.go.dubai.client",
"zaken" to "com.zakinn.app",
"com.zakinn.app" to "com.zakinn.app",
"tufaddal" to "com.bis.taxi",
"com.bis.taxi" to "com.bis.taxi",
"careem" to "com.careem.acma",
"com.careem.acma" to "com.careem.acma",
"uber" to "com.ubercab",
"com.ubercab" to "com.ubercab",
"taxif" to "com.taxif.passenger",
"com.taxif.passenger" to "com.taxif.passenger",
"jeeny" to "me.com.easytaxi",
"me.com.easytaxi" to "me.com.easytaxi"
)
fun launchApp(context: Context, appName: String): Boolean {
val packageName = APP_PACKAGES[appName.lowercase()]
if (packageName == null) {
Log.e(TAG, "Unknown app name from server: $appName")
return false
}
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(launchIntent)
Log.i(TAG, "Successfully launched $appName ($packageName)")
return true
} else {
Log.e(TAG, "App $appName ($packageName) is not installed on this device!")
return false
}
}
}
@@ -0,0 +1,11 @@
package com.siro.android_bot.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package com.siro.android_bot.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun Android_botTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package com.siro.android_bot.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

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