دليل الواجهة البرمجية لمبرمج تطبيقات الجوال

Frontend API guide for the mobile engineer

جمهور الدليل: مبرمج فلاتر يربط شاشات الإدارة والمستخدم بالخادم.

العنوان الحالي للمختبر

http://174.138.29.61:18080
http://174.138.29.61:18080/v1
GET  http://174.138.29.61:18080/healthz

كل مسارات العمل تحت /v1. الصحة بدون /v1. التطبيقان يُبنيان بـ --dart-define=USE_HTTP=true فيقرآن العنوان من packages/core/lib/src/lab_url.dart.

1. قواعد عامة

{ "statusCode": 401, "message": "...", "messageAr": "..." }
الرمزالمعنى
400جسم ناقص أو مكرر (بريد/هاتف)
401بيانات دخول أو رمز وصول أو OTP
403الدور لا يملك المسار
404الصف غير موجود
409تعارض

2. الأدوار والتطبيقات

تطبيقمن يدخلماذا يفعل
farmer_appمزارع فقطتسجيل، دخول، قراءة، بنك، رمز
admin_appمشرف أو مالككتابة أسعار/إرشاد/محاصيل، بحث مزارعين، صرف

إن دخل مزارع لتطبيق الإدارة أو مشرف لتطبيق المزارع، ارفض محلياً برسالة «الدور غير مدعوم»، حتى لو الخادم قبل الرمز.

3. المصادقة

3.1 إنشاء حساب مستخدم (المزارع فقط)

POST /v1/auth/register
{
  "fullName": "أحمد علي",
  "email": "ahmed@example.com",
  "password": "at-least-8",
  "phone": "0120000099"
}

لا تُصدر رموزاً فوراً. الاستجابة:

{
  "status": "otp_required",
  "challengeId": "a1b2...",
  "phoneHint": "012***0099",
  "expiresIn": 300,
  "otpEcho": "482193"
}

otpEcho يظهر في المختبر فقط (OTP_ECHO=true). في الإنتاج لن يُعاد؛ الرمز يُرسل برسالة لاحقاً.

POST /v1/auth/verify-otp
{ "challengeId": "a1b2...", "code": "482193" }

نجاح:

{
  "status": "ok",
  "accessToken": "...",
  "refreshToken": "...",
  "expiresIn": 900,
  "user": {
    "id": "4",
    "fullName": "أحمد علي",
    "email": "ahmed@example.com",
    "phone": "0120000099",
    "phoneVerified": true,
    "role": "farmer",
    "farmerId": "2",
    "supportEligible": false
  }
}

احفظ accessToken وrefreshToken على الجهاز. لا تُظهرهما في السجلات.

3.2 الدخول بالبريد وكلمة المرور

POST /v1/auth/login
{ "email": "farmer.lab@nidaa.local", "password": "LabBridge#2026" }

إن كان للحساب هاتف (حسابات المختبر كلها كذلك) يعيد otp_required. اعرض شاشة OTP. بعد التأكيد يُعلَّم phoneVerified. إن لم يكن هاتف: status: "ok" مع الرموز مباشرة.

الدورالبريدكلمة المرور
مزارعfarmer.lab@nidaa.localLabBridge#2026
مشرفadmin.lab@nidaa.localLabBridge#2026
مالكowner.lab@nidaa.localLabBridge#2026

3.3 إعادة إرسال الرمز

POST /v1/auth/request-otp
{ "challengeId": "..." }

يعيد challengeId جديداً. أبطل القديم في الواجهة.

3.4 قوقل (الخدمة الرسمية)

زر «الدخول بقوقل» يفتح شاشة قوقل الرسمية عبر google_sign_in ثم يرسل معرّف الهوية:

POST /v1/auth/google
{ "idToken": "<Google ID token>" }

ينشئ مزارعاً إن لم يوجد، أو يربط الحساب إن وُجد نفس البريد. ضع GOOGLE_CLIENT_ID (عميل الويب) في .env وابنِ التطبيق بـ --dart-define=GOOGLE_SERVER_CLIENT_ID=....

حزمة أندرويد: org.nidaa.user — SHA-1: DF:47:F3:4B:56:20:29:9D:A0:11:92:9F:F7:E3:5B:2C:97:3F:78:AD

3.5 فيسبوك (الخدمة الرسمية)

زر «الدخول بفيسبوك» يفتح تسجيل فيسبوك الرسمي:

POST /v1/auth/facebook
{ "accessToken": "<Facebook access token>" }

ضع FACEBOOK_APP_ID وFACEBOOK_APP_SECRET وFACEBOOK_CLIENT_TOKEN في .env وفي farmer_app/android/oauth.properties. إن كان للحساب هاتف غير موثّق بعد الدخول الاجتماعي، الخادم يعيد otp_required.

3.6 التجديد والخروج والجهاز الحالي

POST /v1/auth/refresh   { "refreshToken": "..." }
POST /v1/auth/logout    { "refreshToken": "..." }
GET  /v1/auth/me

عند 401 على أي مسار محمي: امسح الجلسة وأعد شاشة الدخول.

4. تسلسل الشاشات المقترح

[دخول]
  بريد + كلمة مرور
  أو قوقل / فيسبوك          ← تطبيق المزارع فقط
  أو «تسجيل مستخدم جديد»
        ↓
[OTP]  رمز من 6 أرقام يؤكد الهاتف
        ↓
[هيكل التطبيق]

لا تتخطّ OTP إن كانت الاستجابة otp_required. لا تدخل المزارع قبل verify-otp.

5. مسارات المزارع بعد الدخول

رأس Bearer إلزامي.

الغرضالطلب
الأسعارGET /v1/crop-prices
الإرشاد المنشورGET /v1/content
بحث محتوىGET /v1/content?q=&type=article&stage=land_prep
القوائمGET /v1/lookups
دفعاتيGET /v1/payments/me
حسابي البنكيGET /v1/bank-accounts/me
إرسال بنكPOST /v1/bank-accounts
رمزيGET /v1/qr/my-code
إشعاراتGET /v1/notifications
POST /v1/bank-accounts
{
  "bankId": "1",
  "accountName": "أحمد علي",
  "accountNumber": "1234567890",
  "bankakNumber": "2499..."
}

lookups.banks[].id هو bankId. مراحل الزراعة: land_prep planting irrigation pest_control harvest. أنواع المحتوى: article image audio video. المزارع لا يستدعي POST /crops ولا /crop-prices ولا /content ولا GET /farmers — الخادم يعيد 403.

6. مسارات المشرف / المالك

نفس الرأس Bearer بعد OTP.

محاصيل

POST /v1/crops
{ "name": "سمسم", "categoryId": "1" }

PATCH /v1/crops/:id
{ "name": "...", "isActive": true }

categoryId من GET /v1/lookups → categories.

أسعار

POST /v1/crop-prices
{
  "cropId": "1",
  "marketId": "1",
  "unitId": "1",
  "price": 1300,
  "priceDate": "2026-08-15"
}

PATCH /v1/crop-prices/:id
{ "price": 1320, "notes": "تحديث يومي" }

GET /v1/crop-prices يعيد السعر الحالي والتغير المحسوب (previousPrice, change, changePct). لا ترسل تصنيف عربي في الاستعلام؛ صفِّ في الواجهة أو استخدم رمز التصنيف grains.

إرشاد / معلومات

POST /v1/content
{
  "title": "ري القمح",
  "body": "...",
  "type": "article",
  "farmingStageId": "3",
  "cropIds": ["1"]
}

PATCH /v1/content/:id/publish
GET   /v1/content?includeDrafts=1
PATCH /v1/content/:id
{ "title": "...", "body": "..." }

يُنشأ مسودة ثم يُنشر.

المزارعون وبيانات المستخدم

GET    /v1/farmers
GET    /v1/farmers?q=أحمد
GET    /v1/farmers/:id
POST   /v1/farmers
{ "fullName": "...", "email": "...", "phone": "...", "password": "..." }
PATCH  /v1/farmers/:id
{ "supportEligible": true, "locationText": "ود مدني" }
GET    /v1/farmers/:id/payments
PATCH  /v1/payments/:id/status
{ "status": "paid", "receiptNumber": "REC-1" }
PATCH  /v1/bank-accounts/:id/review
{ "status": "verified", "notes": "مطابق" }
POST   /v1/qr/verify
{ "token": "..." }

تفصيل GET /farmers/:id يتضمن المستخدم والموقع ونوع الزراعة والحسابات والدفعات. تفعيل الأهلية لمزارع بلا دفعات ينشئ خمس دفعات تلقائياً. المشرف والمالك ينشئان مستخدماً من شاشة المزارعين.

المشرفون — المالك فقط

GET    /v1/staff
POST   /v1/staff
{ "fullName": "...", "email": "...", "password": "...", "phone": "..." }
PATCH  /v1/staff/:id
{ "status": "inactive" }

المالك ينشئ مشرفاً من لوحة المالك. المشرف لا ينشئ مشرفاً. الحساب غير النشط يُرفض عند الدخول.

المالك فقط

GET /v1/owner/dashboard
GET /v1/reports/overview

المشرف على هذين المسارين يُمنع.

7. مثال تدفق كامل (مزارع جديد)

BASE=http://174.138.29.61:18080/v1

curl -s -X POST $BASE/auth/register -H 'Content-Type: application/json' \
  -d '{"fullName":"مزارع جديد","email":"n1@nidaa.local","password":"password1","phone":"0120000091"}'

curl -s -X POST $BASE/auth/verify-otp -H 'Content-Type: application/json' \
  -d '{"challengeId":"...","code":"..."}'

TOKEN=...
curl -s $BASE/auth/me -H "Authorization: Bearer $TOKEN"
curl -s $BASE/crop-prices -H "Authorization: Bearer $TOKEN"
curl -s $BASE/content -H "Authorization: Bearer $TOKEN"

مثال مشرف يضيف سعراً بعد OTP:

curl -s -X POST $BASE/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"admin.lab@nidaa.local","password":"LabBridge#2026"}'
# ثم verify-otp
curl -s -X POST $BASE/crop-prices -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"cropId":"1","marketId":"1","unitId":"1","price":1400,"priceDate":"2026-08-15"}'

8. ربط فلاتر

الملف: packages/core/lib/src/http_repository.dart

دالة المستودعالمسار
loginPOST /auth/login
registerPOST /auth/register
verifyOtpPOST /auth/verify-otp
resendOtpPOST /auth/request-otp
loginGooglePOST /auth/google
loginFacebookPOST /auth/facebook
cropPricesGET /crop-prices
createCropPricePOST /crop-prices
contentGET /content
createContentPOST /content + نشر
searchFarmers / getFarmerGET /farmers و GET /farmers/:id
createFarmerPOST /farmers — مشرف أو مالك
listStaff / createAdmin/staff — مالك فقط

لا تبنِ عميل HTTP ثانياً في الشاشات. أضف الدالة في BridgeRepository ثم نفّذها في HttpBridgeRepository وFakeBridgeRepository.

cd admin_app   # أو farmer_app
flutter build apk --release --dart-define=USE_HTTP=true

بدون USE_HTTP=true يعمل الوهم المحلي ولا يصل الخادم.

9. إعداد قوقل وفيسبوك الرسميين

  1. Google Cloud: عميل أندرويد لحزمة org.nidaa.user مع SHA-1 أعلاه، وعميل ويب. ضع معرّف عميل الويب في GOOGLE_CLIENT_ID وابنِ التطبيق بـ GOOGLE_SERVER_CLIENT_ID.
  2. Facebook Developers: تطبيق أندرويد بنفس الحزمة. ضع FACEBOOK_APP_ID وFACEBOOK_CLIENT_TOKEN في .env وoauth.properties.
  3. AUTH_OAUTH_DEV=false بعد التأكد من الرموز الحقيقية.
  4. التطبيق يرسل رمز قوقل/فيسبوك الحقيقي؛ لا حوار بريد محلي.
  5. إيقاف OTP_ECHO وربط بوابة SMS لاحقاً دون تغيير شكل المسارات.

Lab origin

http://174.138.29.61:18080
http://174.138.29.61:18080/v1
GET  http://174.138.29.61:18080/healthz

All product routes sit under /v1. Health is outside /v1. Both apps are built with --dart-define=USE_HTTP=true and read the origin from packages/core/lib/src/lab_url.dart.

1. Ground rules

{ "statusCode": 401, "message": "...", "messageAr": "..." }
CodeMeaning
400Invalid or duplicate body (email/phone)
401Bad credentials, access token, or OTP
403Role not allowed on this route
404Row not found
409Conflict

2. Roles and apps

AppWho signs inWhat they do
farmer_appFarmer onlyRegister, login, read, bank, QR
admin_appAdmin or ownerWrite prices/guidance/crops, search farmers, payouts

If a farmer opens the admin app, or an admin opens the farmer app, reject locally with a role-not-supported message even if the server accepted the token.

3. Authentication

3.1 Create a user account (farmer app only)

POST /v1/auth/register
{
  "fullName": "أحمد علي",
  "email": "ahmed@example.com",
  "password": "at-least-8",
  "phone": "0120000099"
}

Tokens are not issued yet. Response:

{
  "status": "otp_required",
  "challengeId": "a1b2...",
  "phoneHint": "012***0099",
  "expiresIn": 300,
  "otpEcho": "482193"
}

otpEcho is lab-only (OTP_ECHO=true). Production will not return it; the code will be sent by SMS later.

POST /v1/auth/verify-otp
{ "challengeId": "a1b2...", "code": "482193" }

Success:

{
  "status": "ok",
  "accessToken": "...",
  "refreshToken": "...",
  "expiresIn": 900,
  "user": {
    "id": "4",
    "fullName": "أحمد علي",
    "email": "ahmed@example.com",
    "phone": "0120000099",
    "phoneVerified": true,
    "role": "farmer",
    "farmerId": "2",
    "supportEligible": false
  }
}

Store accessToken and refreshToken on the device. Do not log them.

3.2 Email and password login

POST /v1/auth/login
{ "email": "farmer.lab@nidaa.local", "password": "LabBridge#2026" }

If the account has a phone (all lab accounts do), the response is otp_required. Show the OTP screen. After verify, phoneVerified becomes true. If there is no phone: status: "ok" with tokens immediately.

RoleEmailPassword
Farmerfarmer.lab@nidaa.localLabBridge#2026
Adminadmin.lab@nidaa.localLabBridge#2026
Ownerowner.lab@nidaa.localLabBridge#2026

3.3 Resend OTP

POST /v1/auth/request-otp
{ "challengeId": "..." }

Returns a new challengeId. Discard the old one in the UI.

3.4 Google (official)

The Google button opens the official Google account sheet via google_sign_in, then sends the ID token:

POST /v1/auth/google
{ "idToken": "<Google ID token>" }

Creates a farmer if missing, or links the account when the email already exists. Set GOOGLE_CLIENT_ID (web client) in .env and build with --dart-define=GOOGLE_SERVER_CLIENT_ID=....

Android package: org.nidaa.user — SHA-1: DF:47:F3:4B:56:20:29:9D:A0:11:92:9F:F7:E3:5B:2C:97:3F:78:AD

3.5 Facebook (official)

The Facebook button opens official Facebook Login:

POST /v1/auth/facebook
{ "accessToken": "<Facebook access token>" }

Set FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, and FACEBOOK_CLIENT_TOKEN in .env and farmer_app/android/oauth.properties. If the account has an unverified phone after social login, the server also returns otp_required.

3.6 Refresh, logout, current user

POST /v1/auth/refresh   { "refreshToken": "..." }
POST /v1/auth/logout    { "refreshToken": "..." }
GET  /v1/auth/me

On 401 from any protected route: clear the session and return to login.

4. Suggested screen sequence

[Login]
  email + password
  or Google / Facebook          ← farmer app only
  or “create new user”
        ↓
[OTP]  6-digit code confirms the phone
        ↓
[App shell]

Do not skip OTP when the response is otp_required. Do not enter the farmer shell before verify-otp.

5. Farmer routes after login

Bearer header is required.

PurposeRequest
PricesGET /v1/crop-prices
Published guidanceGET /v1/content
Content searchGET /v1/content?q=&type=article&stage=land_prep
LookupsGET /v1/lookups
My paymentsGET /v1/payments/me
My bankGET /v1/bank-accounts/me
Submit bankPOST /v1/bank-accounts
My QRGET /v1/qr/my-code
NotificationsGET /v1/notifications
POST /v1/bank-accounts
{
  "bankId": "1",
  "accountName": "أحمد علي",
  "accountNumber": "1234567890",
  "bankakNumber": "2499..."
}

lookups.banks[].id is bankId. Farming stages: land_prep planting irrigation pest_control harvest. Content types: article image audio video. A farmer calling POST /crops, /crop-prices, /content, or GET /farmers gets 403.

6. Admin / owner routes

Same Bearer header after OTP.

Crops

POST /v1/crops
{ "name": "سمسم", "categoryId": "1" }

PATCH /v1/crops/:id
{ "name": "...", "isActive": true }

categoryId comes from GET /v1/lookups → categories.

Prices

POST /v1/crop-prices
{
  "cropId": "1",
  "marketId": "1",
  "unitId": "1",
  "price": 1300,
  "priceDate": "2026-08-15"
}

PATCH /v1/crop-prices/:id
{ "price": 1320, "notes": "daily update" }

GET /v1/crop-prices returns the current price and computed change (previousPrice, change, changePct). Do not send an Arabic category in the query; filter in the UI or use the category code grains.

Guidance / information

POST /v1/content
{
  "title": "ري القمح",
  "body": "...",
  "type": "article",
  "farmingStageId": "3",
  "cropIds": ["1"]
}

PATCH /v1/content/:id/publish
GET   /v1/content?includeDrafts=1
PATCH /v1/content/:id
{ "title": "...", "body": "..." }

Created as a draft, then published.

Farmers and user data

GET    /v1/farmers
GET    /v1/farmers?q=أحمد
GET    /v1/farmers/:id
POST   /v1/farmers
{ "fullName": "...", "email": "...", "phone": "...", "password": "..." }
PATCH  /v1/farmers/:id
{ "supportEligible": true, "locationText": "ود مدني" }
GET    /v1/farmers/:id/payments
PATCH  /v1/payments/:id/status
{ "status": "paid", "receiptNumber": "REC-1" }
PATCH  /v1/bank-accounts/:id/review
{ "status": "verified", "notes": "match" }
POST   /v1/qr/verify
{ "token": "..." }

GET /farmers/:id includes the user, location, farm type, bank accounts, and payments. Turning eligibility on for a farmer with no payments creates five payment rows. Admin and owner create users from the farmers screen.

Admins — owner only

GET    /v1/staff
POST   /v1/staff
{ "fullName": "...", "email": "...", "password": "...", "phone": "..." }
PATCH  /v1/staff/:id
{ "status": "inactive" }

The owner creates admins from the owner dashboard. An admin cannot create another admin. Inactive accounts are rejected at login.

Owner only

GET /v1/owner/dashboard
GET /v1/reports/overview

An admin is forbidden on these two routes.

7. Full sample flow (new farmer)

BASE=http://174.138.29.61:18080/v1

curl -s -X POST $BASE/auth/register -H 'Content-Type: application/json' \
  -d '{"fullName":"مزارع جديد","email":"n1@nidaa.local","password":"password1","phone":"0120000091"}'

curl -s -X POST $BASE/auth/verify-otp -H 'Content-Type: application/json' \
  -d '{"challengeId":"...","code":"..."}'

TOKEN=...
curl -s $BASE/auth/me -H "Authorization: Bearer $TOKEN"
curl -s $BASE/crop-prices -H "Authorization: Bearer $TOKEN"
curl -s $BASE/content -H "Authorization: Bearer $TOKEN"

Admin adding a price after OTP:

curl -s -X POST $BASE/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"admin.lab@nidaa.local","password":"LabBridge#2026"}'
# then verify-otp
curl -s -X POST $BASE/crop-prices -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"cropId":"1","marketId":"1","unitId":"1","price":1400,"priceDate":"2026-08-15"}'

8. Flutter wiring

File: packages/core/lib/src/http_repository.dart

Repository methodRoute
loginPOST /auth/login
registerPOST /auth/register
verifyOtpPOST /auth/verify-otp
resendOtpPOST /auth/request-otp
loginGooglePOST /auth/google
loginFacebookPOST /auth/facebook
cropPricesGET /crop-prices
createCropPricePOST /crop-prices
contentGET /content
createContentPOST /content + publish
searchFarmers / getFarmerGET /farmers and GET /farmers/:id
createFarmerPOST /farmers — admin or owner
listStaff / createAdmin/staff — owner only
createFarmerPOST /farmers — admin or owner
listStaff / createAdmin/staff — owner only

Do not build a second HTTP client in screens. Add the method on BridgeRepository, then implement it in HttpBridgeRepository and FakeBridgeRepository.

cd admin_app   # or farmer_app
flutter build apk --release --dart-define=USE_HTTP=true

Without USE_HTTP=true the app uses the local fake repository and never hits the server.

9. Official Google / Facebook setup

  1. Google Cloud: Android client for package org.nidaa.user with the SHA-1 above, plus a web client. Put the web client ID in GOOGLE_CLIENT_ID and build with GOOGLE_SERVER_CLIENT_ID.
  2. Facebook Developers: Android app with the same package. Put FACEBOOK_APP_ID and FACEBOOK_CLIENT_TOKEN in .env and oauth.properties.
  3. AUTH_OAUTH_DEV=false after real tokens work.
  4. The app sends the real Google/Facebook token; there is no local email dialog.
  5. Turn off OTP_ECHO and later attach an SMS gateway without changing route shapes.