الجوال والشاشات
لوحة معلومات الجوال (Mobile Dashboard)
الشاشة الأولى في تطبيق أوكتا للجوال لوحة معلومات، لا قائمة تطبيقات. قائمة التطبيقات لها تبويبها الخاص «التطبيقات» في الشريط السفلي؛ واللوحة تعرض ما تقوله التطبيقات المثبَّتة نفسها: أرقام اليوم، طوابير الانتظار، آخر ما حدث.
تطبيقك يستطيع أن يضع بطاقة فيها. وهذا متاح لكل الأوضاع — native
وwebview وexternal سواء — لأن البطاقة بيانات لا واجهة.
وللبوّابات الشاشة الرئيسية نفسها
شاشة الطالب وشاشة وليّ الأمر لوحتا معلومات أيضاً، وقائمة تطبيقات البوابة
في تبويب «التطبيقات» الخاص بها — الشكل نفسه الذي لمدير الجهة. بطاقتك تصل
البوابة تلقائياً متى أعلن تطبيقك جمهوراً لها؛ لا شيء جديداً تعلنه،
وaudiences تُضيّق هناك كما تُضيّق للأدوار — اكتب ["guardian"] لتخاطب
أولياء الأمور وحدهم.
شيئان يختلفان، وكلاهما مقصود:
-
وسيط الجمهور يحمل البوابة. الوسيط الأول لمزوّدك (وحقل
roleفي نداء external) يصلstudentأوguardianحيث يصل مفتاح الدور في نداء الجهة. الوسيط نفسه والمعنى نفسه — من يقرأ — ومزوّد يتجاهله يخاطب كل الجماهير بالأرقام نفسها، وهذا افتراض سليم. -
البوابة عابرة للجهات، وأنت تقرّر كيف يُقرأ ذلك. وليّ أمر أبناؤه في مدرستين ثبّتتا تطبيقك يرى — افتراضياً — بطاقتين، لأن الأرقام تختلف وجمع مدرستين في رقم واحد يصف لا هذه ولا تلك. لكن تطبيقاً موضوعه الابن لا المدرسة يُقرأ رديئاً هكذا: بطاقتان بعنوان «أبنائي» كلٌّ منهما تحمل نصف الجواب. لهذا
portal_scope: "combined"يطوي البطاقتين في بطاقة واحدة فيها قسم لكل مدرسة.ومزوّدك لا يتغيّر في الحالتين: يعمل مرة لكل مدرسة داخل سياقها ويجيب عنها وحدها — عزل المخطّط لكل تثبيت يجعل غير ذلك مستحيلاً أصلاً — والدمج يحدث في okta-web بعد أن تتكلّم كل مدرسة. البطاقة تكسب أقساماً، وأنت لا تكسب شيئاً تحترس منه.
"dashboard": { "enabled": true, "title": "أبنائي", "portal_scope": "combined" }مدرسة واحدة أجابت؟ تُفكّ البطاقة إلى بطاقة عادية بلا أقسام — قسمٌ واحد عنوانه اسم المدرسة يقول ما تقوله البطاقة نفسها. وكل قسم يحمل حالته: مدرسة متعذّرة لا تُعطّل أرقام أختها، بل تظهر ملاحظتها وحدها.
القاعدة التي تحكم كل ما بعدها: أنت تقول ماذا، والمضيف يقرّر كيف
أنت تُرجع أرقاماً وعناوين وأزراراً؛ تطبيق أوكتا هو الذي يرسمها، بنفس اللغة البصرية لكل التطبيقات. الخط الفاصل ليس بين «بسيط» و«غنيّ» — بل بين ما تريده على البطاقة وكيف يُرسَم. الأول لك بسعة، والثاني ليس لأحد منّا وحده.
فالمفردات واسعة: أرقام، صفوف، أيقونات، أشرطة تقدّم، وأزرار تنفّذ عمليات حقيقية (انظر الأفعال أدناه). والممنوع ثلاثة فقط، ولكلٍّ سبب يبقى صحيحاً مهما اتّسعت المفردات:
- لا markup ولا HTML — بطاقة تأتي بتخطيطها الخاص تجعل لوحةً تحمل خمسة تطبيقات خمسَ شاشات متجاورة، وسؤال القارئ الأول («من يقول لي هذا، وكم عمر هذا الرقم؟») يحصل على خمس إجابات.
- لا ألوان سداسية —
toneمعنى يحلّه المضيف إلى لوحته في الوضعين الفاتح والداكن؛ لونٌ تختاره أنت لن يطابق أياً منهما. - لا صور من مضيفك — أصلٌ لم يُراجَع على أول شاشة، لا يتبع الثيم، ويفشل تحميله على الاتصالات نفسها التي وُجدت البطاقة لتخدمها. الأيقونات من مفردات المضيف بدلاً عنها.
وهو أيضاً سبب أن البطاقة لا تكلّف شيئاً: لا ترجمة تطبيق مصغّر، ولا صندوق رملي، ولا نسخة عقد. خمس بطاقات هي خمسة مصفوفات مُعادة، لا خمسة مفسّرات.
الإعلان
داخل mobile في البيان:
"mobile": {
"supported": true,
"mode": "native",
"entry": "main",
"dashboard": {
"enabled": true,
"title": "حضور اليوم",
"title_en": "Today's attendance",
"provider": "Modules\\OktaHdor\\App\\Services\\DashboardProvider",
"audiences": ["tenant-admin", "teacher"],
"cache_ttl": 300
}
}
| المفتاح | إلزامي | الوصف |
|---|---|---|
enabled |
يجب أن يكون true. لا تشحن بطاقة معطَّلة — احذف الكتلة كلها. |
|
title / title_en |
(أحدهما) | عنوان البطاقة. بدونه تُعنون بـ slug تطبيقك، وهو لا يقول لحامل الهاتف شيئاً. |
provider |
للـ embedded | صنف تحت Modules\ ينفّذ MobileDashboardProvider. |
endpoint |
للـ external | رابط https يستقبل POST موقَّعاً. |
audiences |
— | مفاتيح أدوار. تُضيّق ولا تُوسّع (انظر أدناه). |
cache_ttl |
— | ثوانٍ. يُحصر بين 30 و3600. |
portal_scope |
— | per_tenant (افتراضي) أو combined — شكل البطاقة على شاشة البوابة حين يرى الشخص أكثر من مدرسة. |
mode |
— | مَن يرسم الداخل: data (افتراضي — المضيف يرسم أرقامك) أو native (كودك Dart يرسمه — انظر أدناه). |
entry |
مع native |
ملف Dart للبطاقة تحت okta_app/native/<حزمة>/lib/. |
min_contract |
مع native |
عقد مضيف okta-app الذي تحتاجه البطاقة. الأرضية 21، والأدنى يُرفع إليها. |
💡 بديل عن تحرير manifest.json يدوياً: أداة MCP
set_mobile_dashboardتكتب الكتلة مباشرة على إصدار قابل للتعديل وتشغّل فحوص النشر فوراً، وget_mobile_surfaceتقرؤها. والمزامنة من مستودعك (sync_from_manifest) تحملها كذلك.
النقل يتبع integrationType ولا يُعلَن. تطبيق embedded يسمّي صنفاً
تناديه المنصّة داخل عمليتها، وتطبيق external يسمّي رابطاً تُوقّع إليه
المنصّة طلباً واحداً. إعلان الاثنين معاً — أو الخطأ منهما — يُرفض عند
النشر، لأن مفتاحاً ثانياً لسؤال يجيب عنه البيان أصلاً يعني أن أول تطبيق
يضبطهما متناقضين ينشر بطاقة لا تظهر أبداً.
تطبيق مدمج (Embedded): نفّذ العقد
namespace Modules\OktaHdor\App\Services;
use App\Services\PartnerApi\Contracts\MobileDashboardProvider;
final class DashboardProvider implements MobileDashboardProvider
{
public function dashboard(string $roleKey, string $locale): array
{
$today = Attendance::query()->whereDate('taken_at', today());
return [
'stats' => [
[
'label' => $locale === 'en' ? 'Present' : 'حاضر',
'value' => number_format($today->clone()->where('status', 'present')->count()),
'tone' => 'success',
],
[
'label' => $locale === 'en' ? 'Absent' : 'غائب',
'value' => number_format($today->clone()->where('status', 'absent')->count()),
'tone' => 'danger',
],
],
'rows' => [
['title' => 'الصف الأول', 'subtitle' => 'أ', 'value' => '98%'],
],
'open_label' => $locale === 'en' ? 'Open register' : 'فتح السجل',
];
}
}
يعمل داخل سياق مودولك، فيقرأ مخططك باعتماداتك تماماً كما تفعل صفحاتك. ويجب أن تُقصر كل استعلام على الجهة الحالية.
تطبيق خارجي (External): استقبل POST موقَّعاً
POST <endpoint>
X-Okta-Timestamp: 1755859200
X-Okta-Signature: HMAC-SHA256( "<timestamp>.<body>", install_signing_secret )
{"tenant_id": 42, "role": "tenant-admin", "locale": "ar"}
تحقّق من التوقيع قبل الإجابة، وأجب بنفس شكل المصفوفة أعلاه. المهلة 6 ثوانٍ — ما بعدها يُعامَل كتعذّر.
لا يُرسَل إليك أي شيء يعرّف الشخص. الرقم على اللوحة يخصّ المدرسة، ولستَ بحاجة لتعرف أيّ موظّف ينظر إلى هاتفه ليعدّ غياب اليوم.
شكل البيانات
[
'title' => 'حضور اليوم', // اختياري — يتقدّم على عنوان البيان
'stats' => [ ['label' =>, 'value' =>, 'caption' => ?, 'tone' => ?, 'icon' => ?] ],
'rows' => [ ['title' =>, 'subtitle' => ?, 'value' => ?, 'tone' => ?,
'icon' => ?, 'progress' => ?, 'action' => ?] ],
'actions' => [ ['key' =>, 'label' =>, 'tone' => ?, 'style' => ?,
'confirm' => ?, 'icon' => ?] ], // حتى ٣
'open_label' => 'فتح السجل', // اختياري — احذفه فلا يُعرض زر الفتح
'message' => null, // ملاحظة تُعرض مع الأرقام
]
value نصّ صغتَه أنت. النِّسب والعملات والنِّسَب المئوية والأعداد لا
تشترك في مُنسِّق واحد، وأنت تعرف أيّها تُنتج، وحقلٌ رقمي كان سيجبر
العميل على التخمين — أو على حمل مفردات تنسيق تكبر مع كل شريك.
tone معنى لا لون: neutral | success | warning | danger | info.
المضيف يحلّه إلى لوحة تطابق بقية الشاشة في الوضعين الفاتح والداكن؛ لونٌ
سداسي تختاره أنت لن يطابق أياً منهما.
الحدود مفروضة: 6 أرقام و8 صفوف بحد أقصى، و120 حرفاً لكل نص. الزائد يُقصّ بصمت — بطاقة تأخذ الشاشة كلها ليست بطاقة.
المفاتيح خارج المفردات تُسقَط، لا تُمرَّر. مفتاح غير معروف يصل العميل يصير جزءاً واقعياً من العقد أول ما يعتمد عليه شريك.
icon من مفردات المضيف لا رابط صورة:
bell · calendar · chart · check · clock · alert · users · user · phone · message · money · bus · book · flag · pin · star · download · upload · search · settings · lock · shield · heart · home.
اسمٌ خارجها يُسقَط ولا يُستبدَل — صورة خاطئة أسوأ من لا صورة.
progress كسرٌ بين 0 و1 يرسمه المضيف شريطاً. يُحصر ولا يُرفض: نسبة
خرجت 1.02 خطأ تقريب، لا سبب لإسقاط الصف الذي تنتمي إليه.
الأفعال: أزرار تعمل، لا تعرض
كانت البطاقة أرقاماً وزرّاً واحداً يفتح تطبيقك. ذلك جعل اللوحة مكاناً للقراءة فقط: تطبيقٌ حركته المفيدة «نادِ كل من ينتظر» كان يعرض الرقم ٣١ ثم يرسل القارئ للبحث داخل التطبيق. الآن تُعلن أزراراً في البيانات التي تُعيدها، ويصلك مفتاح الزرّ المضغوط.
'actions' => [
['key' => 'call_waiting', 'label' => 'نادِ الجميع', 'style' => 'primary',
'icon' => 'phone', 'confirm' => 'سيُنادى ٣١ طالباً الآن. متابعة؟'],
],
| المفتاح | إلزامي | الوصف |
|---|---|---|
key |
ما يعود إليك عند الضغط. زرّ بلا مفتاح لا يفعل شيئاً فيُسقَط. | |
label |
نصّ الزر. | |
style |
— | primary أو secondary (افتراضي). المضيف يرسمهما. |
tone |
— | معنى لا لون، كما في الأرقام. |
confirm |
— | نصّ تأكيد يعرضه المضيف قبل التنفيذ. |
icon |
— | من مفردات الأيقونات أعلاه. |
ويمكن لصفٍّ أن يكون قابلاً للضغط بإسناد action له بمفتاح فعل — فيصير
الصف نفسه زرّاً دون إضافة زرّ ثالث.
التنفيذ — مدمج (Embedded)
نفّذ MobileDashboardActionProvider على نفس الصنف الذي يقدّم البطاقة:
use App\Services\PartnerApi\Contracts\MobileDashboardActionProvider;
use App\Services\PartnerApi\Contracts\MobileDashboardProvider;
final class DashboardProvider implements MobileDashboardProvider, MobileDashboardActionProvider
{
public function dashboard(string $roleKey, string $locale): array { /* … */ }
public function dashboardAction(string $actionKey, string $roleKey, string $locale): array
{
if ($actionKey !== 'call_waiting') {
return ['ok' => false, 'message' => 'إجراء غير معروف.'];
}
$count = CallWaitingStudents::run(); // ضع البطيء في طابور
return ['ok' => true, 'message' => "نودي {$count} طالباً.", 'refresh' => true];
}
}
التنفيذ — خارجي (External)
نفس الـ endpoint، ويميّزه وجود action في الجسم:
POST <endpoint>
X-Okta-Signature: HMAC-SHA256( "<timestamp>.<body>", install_signing_secret )
{"tenant_id": 42, "role": "tenant-admin", "locale": "ar", "action": "call_waiting"}
أجب {"ok": true, "message": "…", "refresh": true}. المهلة ١٠ ثوانٍ
هنا (لا ٦) لأن هذا كتابة لا قراءة.
ما تضمنه لك المنصّة، وما يبقى عليك
المنصّة تتحقّق أن التطبيق مثبَّت ونشط، وأن الضاغط يرى تطبيقك أصلاً في
كتالوجه تحت (الجهة، الدور) الحالية، وأن audiences تشمل دوره — نفس بوّابة
القراءة حرفياً، فلا يُبلَغ فعلٌ بإرسال slug من دورٍ لا يرى التطبيق. وتقفل
قفلاً قصيراً لكل (جهة، تطبيق، دور، فعل) يبتلع الضغطة المزدوجة وإعادة
المحاولة على شبكة بطيئة. وrefresh: true يُسقط كاش بطاقتك فتُقرأ من جديد.
ويبقى عليك أنت:
- المفتاح المجهول رفضك أنت. المنصّة لا تتحقّق أن المفتاح كان بين ما رسمتَه: البطاقة أمام الشخص قد تكون من الكاش، ومنصّةٌ تحاول ضبط ذلك سترفض ضغطات مشروعة دون أن تجعل المفتاح المجهول آمناً. ارفضه بنفسك.
- الصلاحية والنافذة الزمنية — «هذا الفعل يحتاج صلاحية لا يملكها القارئ»، «انتهت نافذة اليوم» — كلها رفضٌ منك بجملة يفهمها الشخص.
- الفعل مرّة واحدة (idempotency) حيث تستطيع؛ القفل يغطّي الضغطة المزدوجة لا أكثر.
الفشل هنا جواب لا حالة. القراءة تتسامح مع التعذّر (بطاقة «غير محدَّث» والشاشة تُرسَم)، أما الضغط فلا: الشخص ضغط وينتظر جملة تقول هل حدث أم لا.
التضييق بالأدوار: يُضيّق ولا يُوسّع أبداً
audiences مفاتيح أدوار تُقصر البطاقة على بعض من يرون تطبيقك أصلاً.
اللوحة تُبنى من الكتالوج الذي يستلمه المستخدم بالفعل، فالتطبيق الذي
لا يظهر لدور لا يستطيع مخاطبة ذلك الدور من الباب الخلفي. اترك القائمة
فارغة ليراها كل دور يرى التطبيق.
الفشل حالة لا استثناء
اِرمِ استثناءً عند الفشل. المنصّة تلتقطه وتسجّله وتعرض بطاقتك بحالة «غير متصل»، أو بأرقامها السابقة موسومةً «غير محدَّث» إن كانت مخزَّنة.
لا تُرجع مصفوفة فارغة بدل ذلك. الفراغ لا يُفرَّق عن «لا يوجد شيء اليوم فعلاً»، فيُري مشغّلاً صفراً واثقاً في صباحٍ كان الاستعلام فيه معطوباً.
والمنصّة تحتفظ بنسخة احتياطية ليوم كامل تُجيب بها حين لا تُجيب أنت: حضور الأمس موسوماً «غير محدَّث» أنفع لمن يقف عند بوّابة مدرسة من صندوق فارغ.
ارسم البطاقة بنفسك — mode: "native"
كل ما سبق يبقى كما هو — ثم يستطيع التطبيق المدمج (embedded) خطوة أبعد: أن يرسم داخل بطاقته بلغة Dart حقيقية، عبر نفس مسار «المصدر يُترجم على الجهاز» الذي يستعمله تطبيقه المصغّر أصلاً:
"dashboard": {
"enabled": true,
"title": "حضور اليوم",
"title_en": "Today's attendance",
"provider": "Modules\\OktaHdor\\App\\Services\\DashboardProvider",
"mode": "native",
"entry": "okta_app/native/card/lib/main.dart",
"min_contract": 21
}
الإطار يبقى للمضيف. الزوايا والترويسة باسم تطبيقك وأيقونته وشارة المزامنة وزر «فتح» كلها للمنصّة؛ ودجتك تملك داخل صندوق لا تصل حوافّه — وارتفاعه أيضاً: البطاقة تُقصّ عند 320pt ولا تُمرّر، لأن قائمة تتمرّر داخل شاشة تتمرّر تسرق السحبة من القارئ.
provider يبقى إلزامياً، وهذا هو التصميم لا بقايا منه. الأرقام التي
يعيدها هي البطاقة التي يرسمها الهاتف الأقدم — والبطاقة التي يرسمها
هذا الهاتف لحظة يفشل أي شيء. كل فشل يهبط إليها، بصمت، وفي الجلسة
نفسها: مصدر لا يُترجم، ودجت ترمي أثناء البناء، إطار أول لا يصل
خلال 500ms من تسليم المحرّك، جهاز عقده أدنى من min_contract. لا
حالة خطأ للشريك على الشاشة الرئيسية — القارئ يرى بطاقة بياناتك،
والرئيسية لا تنكسر أبداً بسبب شريك. اختبر بطاقتك بأن تجعلها
ترمي عمداً: ما يجب أن تراه هو أرقامك أنت.
entryحزمة مستقلة. يعيش تحتokta_app/native/<حزمة>/lib/ككل مدخل native، واجعله حزمة خاصة (okta_app/native/card/) لا ملفاً ثانياً داخل حزمة تطبيقك — التسليم يقتطع حزمة واحدة لكل مدخل، فمدخل بطاقة داخل حزمة التطبيق يعني تنزيل تطبيقك كله وترجمته لرسم بطاقة واحدة.min_contractإلزامي والأرضية 21 — فيه شُحن محرّك البطاقات، والأدنى منه يُرفع إليه. الجهاز دونه لا يركّب البطاقة أصلاً ويعرض بطاقة البيانات؛ رفضٌ مقصود وغير مرئي، لا خطأ.- الصندوق الرملي هو صندوق التطبيق المصغّر نفسه. نفس
Okta.*، نفس العزل، نفس الثيم. رمز واحد خاص بالبطاقات:Okta.openMiniApp()يفتح تطبيقك الكامل — نفس فعل زر «فتح» في إطار المضيف. الإبحار العميق مكانه التطبيق؛ البطاقة خلاصة لا شاشة. - البوّابات تُضاعف البطاقة لا حالتها. وليّ أمر أبناؤه في مدرستين يرى بطاقتين تُترجمان وتُركّبان منفصلتين، لا تتشاركان حالة ولا جهة ولا delegate — نفس عزل المدرسة-بمدرسة الذي للتطبيق الكامل.
- للمدمج فقط. البطاقة native تُترجم من حزمة مصدرك الموقّعة،
والتطبيق external لا يشحن كوداً —
mode: "native"يُرفض عند النشر؛ ابقَ علىmode: "data"مع endpoint.
set_mobile_dashboard تكتب المفاتيح الثلاثة وتشغّل هذه الفحوص وقت
التطوير؛ ومخطّط البيان وبوّابة النشر يكرّرانها.
ما يجب أن تعرفه قبل النشر
- بطاقتك تظهر فقط لمن يرى تطبيقك أصلاً في الكتالوج.
cache_ttlيُحصر بين 30 و3600 في الطرفين — البيان المنشور يُظهر الرقم الذي سيُستعمل فعلاً، لا الذي كتبته.providerيجب أن يكون تحتModules\. المنصّة تُنشئ هذا الصنف بالاسم، فأي اسم خارج فضاء مودولك هو بيانٌ يختار داخليّة منصّة لتُنشَأ — والفحصinstanceofمتأخّر، لأن المُنشئ يكون قد نُفِّذ.- البطاقة تحمل أزراراً تعمل (حتى ٣) وصفوفاً قابلة للضغط — لكنها تبقى
بيانات: لا HTML، ولا ألوان سداسية، ولا صور من مضيفك. الأيقونات من
مفردات المضيف، والألوان من
tone. - الأزرار تحتاج نصفها الثاني: أزرار ترسمها بلا
MobileDashboardActionProvider(أو بلا استجابة external لـaction) تظهر ثم تعتذر عند أول ضغطة.