تخط وانتقل إلى المحتوى الرئيسي

الثيمات والواجهات المخصصة

ابنِ واجهة متجر مخصصة بالكامل بـ React أو Vue أو Next.js أو Flutter — مدعومة بواجهة DZBuild البرمجية.

بقلم: Support

تطلق DZBuild عدة ثيمات جاهزة لواجهة المتجر ومخصّصًا بدون كود. لكن إن أردت التحكم الكامل — واجهة React/Vue/Next.js/Flutter خاصة، تصميمك المثالي، التوجيه (routing) الخاص بك — فالواجهة البرمجية مبنية لك.

يمشي هذا الدليل عبر بناء واجهة headless من البداية للنهاية:

  1. عرض الكتالوج (المنتجات، الفئات، المتغيرات)

  2. بناء سلة (من جانب العميل أو الخادم، بحسبك)

  3. إرسال الطلب عبر API

  4. استقبال webhooks عند تغيّر الحالة

في النهاية ستكون واجهتك المخصصة متوافقة 100% مع لوحة تحكم التاجر في DZBuild — تظهر الطلبات، يخصم المخزون، يتكامل الشحن مع شركة التوصيل، بلا تنازلات.

البنية

┌──────────────────────┐         ┌──────────────────────────┐
│ واجهة مخصصة          │  HTTPS  │  api.dzbuild.app/v1/*    │
│ (React / Vue / Flutter)│ ─────► │  Authorization: Bearer … │
│                      │         │                          │
│ - تقرأ المنتجات      │ ◄────── │  استجابات JSON           │
│ - تعرض السلة         │         │                          │
│ - تُرسل الطلبات       │         └──────────────────────────┘
└──────────────────────┘                      │
                                              ▼
                                  لوحة تحكم التاجر في DZBuild
                                  - يؤكد الطلبات
                                  - يدير المخزون
                                  - يتكامل مع شركات التوصيل

أنت تملك الواجهة. DZBuild تملك البيانات والعمليات. التاجر يدخل dzbuild.com/dashboard لإدارة الطلبات المؤكَّدة في واجهتك المخصصة.

المتطلبات المسبقة

  • حساب تاجر DZBuild. خطة اشتراك المتجر لا علاقة لها بالوصول إلى الـ API — فالترقية لا تمنحه، والخطة المنتهية لا تسحبه.

  • مفتاح API مُسجَّل في البرنامج التجريبي. هذه هي البوابة الحقيقية: ما دام الـ API في المرحلة التجريبية، يجب أن تُسجّل DZBuild المفتاح. وبدون التسجيل يُرجع كل نداء 403 forbidden "API is in pilot mode; key not enrolled".

  • خادم خلفي (أو دالة serverless / edge worker) يحمل المفتاح. لا تشحن السر إلى المتصفح — انظر الأمان.

وموضع واحد تهمّ فيه خطة التاجر فعلًا: المتجر على الخطة Free يتوقف عن قبول الطلبات بعد 30 طلبًا في الشهر الميلادي، ويُرجع POST /v1/orders عندئذ 400 bad_request "Monthly order limit reached for this store plan". أما Pro و Unlimited و Enterprise فبلا سقف طلبات. عالج هذا الخطأ في صفحة الدفع لديك.

الخطوة 0 — احصل على مفتاح

لا توجد صفحة "Developer → API Keys" في لوحة تحكم التاجر. وخلال البرنامج التجريبي:

  1. اطلب من دعم DZBuild إصدار أول مفتاح للمتجر وتسجيله في البرنامج التجريبي.

  2. أنشئ أي مفاتيح إضافية بنفسك انطلاقًا من ذلك المفتاح:

curl -X POST 'https://api.dzbuild.app/v1/keys' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type":"platform","name":"Custom storefront — production"}'
# HTTP 200 → { "data": { "key_id", "bearer_token", "signing_secret", "note" } }

ينسخ المفتاح الجديد فئة تحديد المعدل وعَلَم البرنامج التجريبي من المفتاح المنادي. والصلاحيات غير قابلة للاختيار — فمفتاح المنصة يحمل دائمًا المجموعة الافتراضية الكاملة (products:read و products:write و orders:read و orders:write و store:read و store:write و customers:read و landing_pages:read و landing_pages:write و webhooks:read و webhooks:write و usage:read)، لذا افصل البيئات بمفاتيح منفصلة، لا بصلاحيات أضيق.

انسخ bearer_token — يظهر مرة واحدة عند الإنشاء. فقدته؟ ألغه وأنشئ جديدًا.

اختبر بـ:

curl https://api.dzbuild.app/v1/whoami \
  -H "Authorization: Bearer $DZ_KEY"
# المتوقع: { "data": { "key_id": "...", "store_id": 13, "type": "platform", "scopes": [...] } }

الخطوة 1 — عرض الكتالوج

// pages/index.js (Next.js)
export async function getServerSideProps() {
  const res = await fetch('https://api.dzbuild.app/v1/products?limit=50&status=active', {
    headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
  });
  const { data } = await res.json();
  return { props: { products: data.items } };
}export default function Home({ products }) {
  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>
          {p.primary_image && (
            <img src={`https://cdn.dzbuild.app/${p.primary_image}`} alt={p.name} />
          )}
          <h2>{p.name}</h2>
          <p>{p.price} DZD</p>
          <a href={`/product/${p.slug}`}>عرض</a>
        </li>
      ))}
    </ul>
  );
}

أمران عن الصور:

  • primary_image هو مسار تخزين (عادةً uploads/products/<store_id>/<file>)، فرابط الـ CDN هو ببساطة https://cdn.dzbuild.app/ + ذلك المسار — لا تُدرج مقطع /{store_id}/products/ من عندك. وقد تحمل المنتجات القديمة اسم ملف مجرّدًا بدلًا من ذلك؛ وتلك تُحَل على https://cdn.dzbuild.app/uploads/products/<store_id>/<filename>.

  • عنصر القائمة لا يتضمّن store_id. اقرأه مرة واحدة من GET /v1/store أو GET /v1/whoami واحتفظ به في إعداداتك.

  • primary_image يكون null حين لا صورة للمنتج. احترس لذلك، كما في المثال أعلاه.

خزّن الاستجابة — قائمة المنتجات مخزَّنة مؤقتًا لمدة 30 ثانية فالقراءات المتكررة عند الحجم رخيصة.

الخطوة 2 — صفحة تفاصيل منتج بالمتغيرات

const res = await fetch(`https://api.dzbuild.app/v1/products/${id}`, {
  headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
});
const { data: product } = await res.json();

تتضمن الاستجابة variants[] — كل إدخال مجموعة متغيرات بخياراتها:

"variants": [
  { "id": 11, "name": "Color", "type": "color",
    "options": [
      { "id": 14, "value": "Red",  "color_code": "#ff0000", "image_id": 28, "stock": 12 },
      { "id": 15, "value": "Blue", "color_code": "#0000ff", "image_id": 29, "stock": 5  }
    ]
  },
  { "id": 12, "name": "Size", "type": "text",
    "options": [
      { "id": 16, "value": "S", "stock": 10 },
      { "id": 17, "value": "M", "stock": 10 },
      { "id": 18, "value": "L", "stock": 5  }
    ]
  }
]

اعرض محدّدًا لكل مجموعة. لـ type: color اعرض swatches بالـ color_code؛ لـ type: text اعرض labels؛ لـ type: image_text اعرض صورًا مصغرة: فـ options[].image_id هو معرّف رقمي يطابق images[].id لأحد المدخلات في نفس استجابة المنتج.

وانتبه لاسم الحقل — images[].url هو مسار تخزين لا رابطًا. ضع أمامه https://cdn.dzbuild.app/ تمامًا كما فعلت مع primary_image في الخطوة 1 قبل وضعه في وسم img.

التعامل مع نفاذ المخزون: options[].stock يكون null إن لم يفعّل التاجر المخزون لكل متغيّر. إن كان رقمًا ≤ 0 ظلّل الخيار. التحقق الفعلي يحدث في الخادم وقت التأكيد فلا بأس بواجهة قديمة قليلًا.

الخطوة 3 — بناء سلة

السلة من جانب العميل (React state, Vuex, Pinia, localStorage…). لا تحتاج نداء API للإضافة. كل سطر:

{
  product_id:   26,
  product_name: "T-shirt",
  base_price:   1500,
  quantity:     1,
  selected_variants: [
    { group_name: "Color", option_name: "Red", color_code: "#ff0000", price_adjustment: 0 },
    { group_name: "Size",  option_name: "L",   color_code: null,     price_adjustment: 200 }
  ]
}

احسب إجمالي السطر من جانب العميل: (base_price + sum(price_adjustment)) × quantity. أعرض على العميل إجمالي السلة، لكن عامله كعرض فقط:

  • أي سعر ترسله داخل العنصر يُتجاهَل (حقل الـ API هو price). فالخادم يستعمل دائمًا سعر الكتالوج لـ product_id.

  • price_adjustment يُعاد حلّه من الكتالوج بحسب الزوج (group_name, option_name). وعند أي عدم تطابق تفوز قيمة الكتالوج. ولا تُستعمل قيمتك أنت إلا حين لا يُحَل الزوج إطلاقًا — وهذا بالضبط ما يحدث بعد أن يُعيد التاجر تسمية خيار متغيّر، فأبقِ سلاسل group_name/option_name لديك متزامنة مع الكتالوج.

  • group_name و option_name يُبتران بصمت إلى 100 حرف، و color_code إلى 7.

الخطوة 4 — جمع بيانات العميل + احتساب الشحن

نموذج checkout قياسي: الاسم، الهاتف، الولاية، البلدية، العنوان. ثبّت الـ 58 ولاية في الواجهة — فهي قائمة ثابتة.

GET /v1/store متاحة فعلًا ويستحق نداؤها مرة واحدة عند الإقلاع: تُرجع اسم المتجر و slug واللغة والوصف والشعار والأيقونة واللافتة وألوان الثيم والخط والنطاق الفرعي والنطاق المخصص (وهل هو موثَّق) و public_url و hide_branding و created_at. وهي إحدى نداءات GET المخزَّنة مؤقتًا (30 ثانية).

وما لا تحمله — وهذا هو بند v1.1 الحقيقي — هو أسعار الشحن لكل ولاية وقائمة مكاتب الاستلام لدى التاجر. لذا لتكلفة الشحن اليوم:

  • ثبّت الأسعار حسب الولاية + نوع التسليم في واجهتك (الأبسط)، أو

  • احفظها في إعداداتك الخاصة متزامنة مع التاجر خارج الـ API.

مرّر تكلفة الشحن المحسوبة إلى واجهة الطلبات كـ shipping_cost. الخادم لا يُعيد حساب الشحن لك حاليًا؛ وما تمرّره يصبح جزءًا من إجمالي الطلب.

الخطوة 5 — إرسال الطلب

// على خادمك الخلفي (Next.js API route, Edge function, server…)
async function placeOrder(req, res) {
  const cart = req.body;  const order = {
    customer: {
      name:      cart.name,
      phone:     cart.phone,
      email:     cart.email || null,
      wilaya_id: cart.wilaya_id,
      commune:   cart.commune,
      address:   cart.address || ''
    },
    delivery: {
      type:      cart.delivery_type,
      desk_id:   cart.desk_id || null,
      desk_name: cart.desk_name || null
    },
    items: cart.items.map(line => ({
      product_id: line.product_id,
      quantity:   line.quantity,
      variants:   line.selected_variants
    })),
    shipping_cost:  cart.shipping_cost,
    discount:       0,
    payment_method: 'cod',
    notes:          cart.notes || null
  };  const idempKey = req.headers['x-checkout-id'] || crypto.randomUUID();  const apiRes = await fetch('https://api.dzbuild.app/v1/orders', {
    method: 'POST',
    headers: {
      'Authorization':    `Bearer ${process.env.DZ_KEY}`,
      'Content-Type':     'application/json',
      'Idempotency-Key':  idempKey
    },
    body: JSON.stringify(order)
  });  if (!apiRes.ok) {
    const err = await apiRes.json();
    return res.status(apiRes.status).json(err);
  }
  const { data: createdOrder } = await apiRes.json();   // HTTP 200، وليس 201
  return res.json({
    order_number: createdOrder.order_number,
    total:        createdOrder.amounts.total
  });
}

يُجيب POST /v1/orders بـ HTTP 200 عند النجاح (لا توجد نقطة إنشاء في v1 تُرجع 201)، والجسم هو تفصيل الطلب الكامل المعياري — نفس الشكل الذي يُرجعه GET /v1/orders/{id}. تفرّع على apiRes.ok، لا على status === 201 أبدًا.

يرى العميل order_number في صفحة النجاح. ولوحة التاجر تُظهر الآن الطلب الجديد في قائمة pending جاهزًا للتأكيد.

حدود التحقق

كل قاعدة أدناه ترمي 400 bad_request مع الرسالة مباشرة، فاعرضها في نموذج الدفع لديك بدل اكتشافها في الإنتاج:

الحقل

القاعدة

items

من سطر إلى 50 سطرًا، مصفوفة غير فارغة

items[].product_id

يجب أن ينتمي لمتجر المفتاح

items[].quantity

من 1 إلى 9999

customer.name

من 1 إلى 255 حرفًا

customer.phone

يجب أن يطابق ^\+?[0-9 ]{6,20}$ — أرقام ومسافات فقط، مع + واحدة في البداية على الأكثر. الشرطات والأقواس مرفوضة، فطبّع الرقم قبل الإرسال

customer.wilaya_id

من 1 إلى 58

customer.commune

من 1 إلى 100 حرف

delivery.type

home أو desk أو digital

payment_method

cod أو free_digital أو digital_payment — لا غير (card و paypal وغيرها مرفوضة)

shipping_cost و discount و payment_fee

يجب أن تكون >= 0

notes

تُبتر إلى 1000 حرف (وليس خطأ)

يُضاف إلى ذلك سقف الخطة: على متجر بالخطة Free، الطلب رقم 31 في الشهر الميلادي يُرجع 400 bad_request "Monthly order limit reached for this store plan".

الخطوة 6 — استقبال webhooks (اختياري لكنه قوي)

سجّل webhook ليتفاعل متجرك مع أحداث الطلبات:

curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url":    "https://yourstorefront.com/api/dz-webhook",
    "events": ["order.created", "order.confirmed", "order.shipped", "order.delivered"]
  }'

ستحصل على secret في الاستجابة (HTTP 200) — احفظه. لكن لاحظ أن تسليمات API v1 ليست موقَّعة بذلك السر، فلا يمكنك التحقق منها بعد: أبقِ رابط الـ webhook غير قابل للتخمين، وأعد قراءة الطلب بـ GET /v1/orders/{id} قبل التصرّف. انظر التحقق من التواقيع للتفاصيل.

وهناك حدّان يستحقان المعرفة قبل أن تبني على هذا: order.created في API v1 يُطلق فقط للطلبات التي تُنشئها واجهتك عبر الـ API (لا لأي طلب يُوضع في واجهة DZBuild الخاصة بالتاجر)، وأحداث الحالة تُطلق فقط لتغييرات الحالة التي تتم عبر الـ API. انظر كتالوج الأحداث.

استخدم webhooks لـ:

  • إرسال SMS للعميل عند تأكيد طلبه.

  • تحديث CRM / Google Sheet / تحليلاتك.

  • إبطال صفحة "thank you" بعد تأكيد التاجر.

  • إطلاق تسليم منتج رقمي بعد order.delivered.

أنماط شائعة

واجهة متعددة اللغات

احفظ ترجماتك في الواجهة. الـ API يُعيد أسماء المنتجات والأوصاف كما أدخلها التاجر تمامًا. إن استعمل التاجر إضافة Multi-language، سيُعيد GET /v1/products/{id} (في v1.1) name_ar و name_fr و description_ar و description_fr إلى جانب الحقول الأساسية. حتى ذلك الحين، الكنوني فقط مكشوف — اختر ما يريده عميلك من جانب العميل.

اختيار مكتب الاستلام (Stop-desk)

لـ delivery.type = "desk":

// 1. اقرأ مكاتب التاجر للولاية المختارة
//    (مخطط v1.1: GET /v1/store/desks?wilaya=16)
//    حتى ذلك الحين استعلم شركة التوصيل مباشرة (Yalidine, ZR…)// 2. أعرض القائمة، يختار العميل:
selectedDesk = { id: 7842, name: "Yalidine Bab Ezzouar" };// 3. مرّره للطلب:
order.delivery = {
  type:      "desk",
  desk_id:    selectedDesk.id,
  desk_name:  selectedDesk.name
};

الدفع الإلكتروني (SlickPay / Edahabia)

لـ payment_method = "digital_payment":

  1. أرسل الطلب عبر API كالعادة؛ يُنشأ بـ pending و payment_status = pending.

  2. وجّه العميل إلى رابط checkout في SlickPay / Edahabia.

  3. عند النجاح يستلم خادمك الخلفي webhook من SlickPay → نادِ PATCH /v1/orders/{id} لتحويل payment_status إلى paid (مخطط v1.1؛ حتى ذلك الحين يُحدَّث الدفع عبر تكامل SlickPay الموجود أصلًا في المتجر).

الإرجاع والاسترداد

تُدار حاليًا من اللوحة. دعم API لـ POST /v1/orders/{id}/refund ضمن خارطة الطريق.

الأمان

  • لا تضع أبدًا مفتاح API في كود يصل إليه المتصفح. المفاتيح تنتمي لخادمك / serverless / edge worker. المتصفح ينادي خادمك وخادمك ينادي DZBuild.

  • HTTPS فقط بين واجهتك و api.dzbuild.app. طلبات HTTP العادية مرفوضة.

  • Idempotency-Key مطلوب على كل الكتابات. بدونه تحصل على 400 بالكود bad_request والرسالة "Idempotency-Key header is required for write requests" — فالكود هو bad_request، لا idempotency_key_required. ويجب ألا تتجاوز القيمة 64 حرفًا من [A-Za-z0-9_-:.]؛ فـ crypto.randomUUID() يمرّ، أما base64 ومعظم ترميزات التجزئة فلا (+ و / و = مرفوضة).

  • حدود المعدل تُحتسب لكل مفتاح API، ويتبع سقفها خطة المتجر الحالية — فالترقية والتخفيض يسريان فورًا دون الحاجة إلى أي تغيير في المفتاح. ولا شيء بلا حدود في الدقيقة. ولاحظ أن هذه الحدود تخصّ نداءات الواجهة البرمجية وحدها؛ أما واجهة المتجر التي تبنيها فتخدم متسوّقيك دون أن تستهلك منها شيئًا. انظر حدود المعدل للسقوف الحالية.

  • مفتاح لكل بيئة. لا تشارك مفاتيح dev/prod. أنشئ مفتاحًا منفصلًا للمرحلة (staging) وألغه عند انتهائها. (الصلاحيات متطابقة على كل مفاتيح المنصة — والفصل يأتي من المفتاح نفسه.)

استكشاف الأخطاء

المشكلة

السبب المرجَّح

الإصلاح

401 unauthorized "Missing Authorization header"

نسيت رأس Authorization: Bearer …

أضفه

401 unauthorized "Invalid bearer format"

الرمز بلا نقطة — غالبًا نسخ مبتور احتفظ بمعرّف المفتاح dzpk_live_… وحده

استعمل رمز bearer الكامل بمحارفه الـ 73

401 unauthorized "Invalid or revoked API key"

خطأ كتابي، مفتاح ملغى، أو متغير بيئة خاطئ

أنشئ مفتاحًا جديدًا بـ POST /v1/keys

403 forbidden "API is in pilot mode; key not enrolled"

المفتاح موجود لكن DZBuild لم تُسجّله في البرنامج التجريبي

راسل الدعم

400 bad_request "Product N does not belong to this store"

معرّف من متجر آخر

استعمل المفتاح الصحيح

400 bad_request "items must be a non-empty array"

سلة فارغة

لا تُرسل سلال فارغة

400 bad_request "Monthly order limit reached for this store plan"

متجر بالخطة Free بلغ سقف 30 طلبًا في الشهر

يرقّي التاجر إلى Pro أو أعلى

429 rate_limited

استعلام مفرط

استبدل بـ webhooks؛ وتراجع باستعمال ترويسة Retry-After

أمثلة الشيفرة

كل ما تحتاجه موجود في هذه الصفحة — عرض الكتالوج، وعرض المتغيرات، وشكل السلة، وإرسال الطلب، وتسجيل webhook. انسخ من الأقسام أعلاه؛ فلا توجد مستودعات بداية منفصلة لاستنساخها.

الإطلاق

  1. اختبر جيدًا بمفتاح pilot على متجر اختبار.

  2. أنشئ مفتاحًا منفصلًا للإنتاج (نفس الصلاحيات، اسم مختلف).

  3. انشر واجهتك بمفتاح prod في متغيرات البيئة.

  4. ضع طلب اختبار حقيقي؛ تأكد من ظهوره في اللوحة.

  5. اختبر إرجاعًا إن قدّمت إرجاعات.

  6. سجّل webhooks تشير إلى رابط الإنتاج.

  7. راقب GET /v1/usage يوميًا أول أسبوع.

خارطة الطريق

الميزة

ETA

أسعار الشحن حسب الولاية + قائمة مكاتب الاستلام على GET /v1/store (النقطة نفسها متاحة فعلًا)

v1.1

GET /v1/products/{id}/combinations للمخزون لكل تركيبة

v1.1

POST /v1/orders/{id}/refund لاستردادات عبر API

v1.1

رفع صور المنتج عبر presigned URL

v1.1

حقول متعددة اللغات على استجابات المنتجات

v1.1

إن احتجت أيًا منها أبكر، تواصل مع الدعم.

هل أجاب هذا عن سؤالك؟