React في التقنية المالية

أين تنتمي React في المنتجات المالية، من التسجيل واعرف عميلك إلى لوحات التداول، وكيف تحافظ على صحة نماذج المبالغ وصغر نطاق PCI وصدق الأداء. دليل عملي لفرق التقنية المالية في المنطقة.

· · تحديث · 27 دقائق قراءة

رسم تجريدي لشجرة مكونات React بجانب لوحة تحكم مصرفية تتضمن نموذج تحويل
في هذا المقال

كل منتج تقنية مالية اقتربت منه كان له الشكل نفسه: دفتر حسابات ومجموعة خدمات خلفه تستغرق شهورًا حتى تستقيم، وواجهة ويب أمامه يحكم عليها المستخدمون في ثوانٍ. الواجهة الأمامية طبقة رقيقة، لكنها الطبقة الوحيدة التي يراها العميل أو التاجر أو مفتش الجهة الرقابية فعلًا. أصبحت React الطريقة الافتراضية لبناء هذه الطبقة على الويب، وهذا المقال عمّا يعنيه هذا الافتراض حين يكون ما يظهر على الشاشة هو مال أحدهم.

لماذا تهم React في التقنية المالية

الواجهة الأمامية هي حيث تُحسم الثقة

كشف حساب يستغرق أربع ثوانٍ ليظهر، ونموذج تحويل يقبل "1,000.000" ثم يقرّبها بصمت، ورصيد يرتجف بين قيمتين بعد عملية دفع: لا شيء من هذا خطأ في الخلفية، وكله يكلّف ثقة. تحمل الواجهة الأمامية أيضًا التزامات لا تستطيع الخلفية الوفاء بها نيابة عنها: نماذج متاحة لذوي الإعاقة، وشاشات موافقة بالصياغة المعتمدة حرفيًا، وحقول بطاقات تُبقي الصفحة خارج نطاق PCI، وتخطيطات عربية تصمد أمام رقم IBAN طويل. تهم React هنا لأنها البيئة التي يجري فيها معظم هذا العمل اليوم. المكتبة نفسها صغيرة؛ ما يهم هو المنظومة حولها: مكتبات النماذج، وذاكرة التخزين المؤقت لحالة الخادم، وأنظمة التصميم، ومكونات الدفع المستضافة التي يشحنها المزودون كأغلفة React قبل أي شيء آخر.

كيف تحوّلت مكتبة عرض إلى منصة

بدأت React كطريقة لعرض الواجهة انطلاقًا من الحالة. وعلى مدى عقد تراكم حولها ما يحتاجه فريق المنتج: نموذج مكونات ينطبق مباشرة على أنظمة التصميم، وTypeScript كقاعدة لا استثناء، وأطر عمل فوقها (Next.js وReact Router وTanStack Start) تتولى التوجيه والعرض على الخادم وتحميل البيانات. توصي الوثائق الرسمية في react.dev اليوم بالبدء بإطار عمل، وهذا يخبرك كيف يرى المشرفون دور المكتبة. بالنسبة إلى فريق تقنية مالية نادرًا ما يكون الخيار "React أم غيرها"، بل "أي أجزاء من المنظومة، وبأي انضباط، وأين نرسم الحد بين العميل والخادم".

هذا المقال ليس درسًا تعليميًا بل خريطة للموضوع لمن يقرر هل يبني على React، وكيف يوظّف لها، وأي أجزائها تنتج ملاحظات تدقيق إذا أُهملت.

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

أين تُستخدم

مزودو الدفع والمكونات المضمّنة

لوحة تحكم Stripe تطبيق مبني بـ React وTypeScript كما وصفت مدونتهم الهندسية. والأهم لمعظم الفرق أن Stripe Elements، وهي الحقول المستضافة للبطاقات والمحافظ والحسابات البنكية، تأتي مع حزمة React رسمية، ووثائق Elements تفترض أنك ستركّبها داخل شجرة React. تتبع Adyen Web Components وCheckout.com Frames النموذج نفسه: إطار iframe يستضيفه المزود وتحدد أنت موضعه بمكوّن، بينما تعيش الحقول الحساسة على نطاق المزود لا نطاقك. أما Plaid Link، الذي يربط حسابًا بنكيًا بتطبيق، فيُضمَّن عبر SDK بلغة JavaScript مع hook رسمي لـ React موثّق في وثائق Plaid Link.

واجهات التداول والوساطة

تطبيق Robinhood على الويب مبني بـ React، وقد كتب فريقهم الهندسي علنًا عن بنائه. ووثّقت Coinbase استخدام React على الويب وReact Native على الجوال. شاشات التداول، بأسعارها المتدفقة وجداولها ذات آلاف الصفوف، هي الحالة الأصعب، وReact مستخدمة فيها؛ فمخاوف الأداء تتعلق بكيفية استخدامها لا بقدرتها على التحمل.

تحويل الأموال والبنوك الرقمية

تحافظ Wise على نظام تصميم مفتوح المصدر مبني بـ React اسمه Neptune يقوم عليه منتجها على الويب. وكثير من البنوك الرقمية وشركات التحويلات التي انطلقت في العقد الأخير تصف واجهات React وTypeScript في إعلانات وظائفها ومدوناتها الهندسية، إلى جانب تطبيق جوال أصلي أو بـ React Native عادة.

المشهد في المنطقة

في مصر والسعودية والإمارات التوثيق العلني أقل لأن معظم شركات التقنية المالية هنا أحدث عهدًا وتنشر أقل. ما يمكن ملاحظته أن React وTypeScript تظهران في الغالبية الكبرى من إعلانات وظائف الواجهات الأمامية لدى شركات الدفع ومزودي الشراء الآن والدفع لاحقًا والمحافظ والبنوك الرقمية في المنطقة، وأن بوابات التجار التي تقابلها كشريك هي في معظمها تطبيقات صفحة واحدة مبنية بـ React. لن أسمّي شركات لا أعرف منظوماتها إلا من الخارج.

الأدوات الداخلية

الاستخدام الأقل بريقًا والأكثر شيوعًا هو لوحة العمليات التي يراجع فيها فريق الامتثال حالات اعرف عميلك ويعكس فيها الدعم عمليات الدفع. هذه تطبيقات React في معظم الشركات، وكثيرًا ما تحمل صلاحيات وصول إلى بيانات العملاء أعلى مما يحمله المنتج الموجّه للعملاء، ونادرًا ما تنال العناية الأمنية نفسها.

نقاط القوة

نموذج المكونات يناسب الواجهات الخاضعة للرقابة

الواجهات المالية متكررة بطبيعتها: حقل المبلغ نفسه، ومحدد المستفيد نفسه، وشاشة تأكيد الرسوم نفسها، وشارة الحالة نفسها لقيد الانتظار والمسوّى والفاشل والمعكوس. تتيح لك React بناء كل منها مرة واحدة، واختباره مرة واحدة، ومراجعته من فريق الامتثال مرة واحدة، ثم إعادة استخدامه في كل مكان.

TypeScript هي القاعدة

React بلا TypeScript أمر غير معتاد في الفرق المحترفة، وفي التقنية المالية ينبغي ألا يُفكَّر فيه أصلًا. يتيح لك نظام الأنواع أن تقول "المبلغ عدد صحيح موسوم بالوحدات الصغرى بعملة محددة" فيرفض المترجم جمع جنيه مصري مع دولار. وتمدّ مكتبات مثل zod هذا الانضباط إلى وقت التشغيل: مخطط واحد يتحقق من النموذج ويحلل استجابة الـ API ويوثّق العقد.

مكتبات النماذج وحالة الخادم ناضجة

وصلت فئتان من المكتبات إلى مرحلة فيها خيار واضح ومملّ وصحيح. للنماذج، تتولى react-hook-form مع محلّل zod التحقق وسمات إتاحة الوصول وحالة الإرسال بكود قليل. ولبيانات الخادم، حلّت TanStack Query ونظيراتها مسائل التخزين المؤقت وإزالة التكرار والتحديث في الخلفية والتحديثات المتفائلة بالطريقة التي تحتاجها واجهة مصرفية: اعرض ما نعرفه، وحدّثه بهدوء، ولا تكذب بشأنه أبدًا.

أنظمة التصميم والتخطيطات من اليمين إلى اليسار

يُطلق منتج التقنية المالية في المنطقة بالعربية والإنجليزية على الأقل، وكثيرًا ما تختلف أعراف الأرقام بين سوق وأخرى. تتعامل أنظمة تصميم React مع الاتجاه عبر خصائص CSS المنطقية والسياق، ويجعل نموذج المكونات من العملي بناء حقل يعرف أنه يعرض IBAN (دائمًا من اليسار إلى اليمين) داخل فقرة عربية.

المخاطر والمزالق

المال بالفاصلة العائمة

لـ JavaScript نوع رقمي واحد، و0.1 + 0.2 لا تساوي 0.3. أي مبلغ يمر عبر number كقيمة عشرية هو خطأ تقريب ينتظر وقوعه، وخطأ التقريب في دفتر الحسابات يعني فجوة في التسوية. المبالغ أعداد صحيحة بالوحدات الصغرى (قروش، هللات، فلوس) منذ خروجها من الحقل حتى لحظة تنسيقها للعرض.

حالة تكذب

رصيد يتحدث قبل أن يؤكد الخادم التحويل ولا يتراجع أبدًا حين يرفضه الخادم هو شاشة تكذب بشأن المال. احتفظ بمصدر حقيقة واحد لكل جزء من حالة الخادم واشتق كل شيء آخر منه.

الإفراط في الجلب والطلبات المتسلسلة ومؤشرات التحميل

لوحة تحكم تطلق خمسة عشر طلبًا عند التحميل، كل مكوّن يجلب بياناته ويعرض مؤشر تحميله الخاص، هي النتيجة الافتراضية لـ React بلا طبقة بيانات. الحلول موجودة (استعلامات مجاورة للمكونات مع ذاكرة مؤقتة مشتركة، ومحمّلات على مستوى المسار، وعرض الحالة الأولية على الخادم) لكن يجب اختيارها عمدًا، وقبل أن تصبح اللوحة ستين شاشة.

تضخم الاعتماديات

مشروع React النموذجي يحمل مئات الاعتماديات المتعدية. في التقنية المالية هذا سطح هجوم بتاريخ موثق: حادثة event-stream عام 2018 حقنت كودًا يستهدف محفظة عملات رقمية عبر حزمة npm شائعة، واختراق polyfill.io عام 2024 حوّل نصًا برمجيًا مضمّنًا على نطاق واسع إلى قناة لتوزيع البرمجيات الخبيثة. انضباط سلسلة التوريد ضابط أمني، لا تنظيفًا منزليًا.

إتاحة الوصول كفكرة لاحقة

تخضع الخدمات المالية بشكل متزايد لالتزامات إتاحة الوصول، وحتى حيث يصمت القانون، فإن نموذجًا لا يستطيع مستخدم قارئ الشاشة إكماله يُقصي عملاء. لا تفعل React شيئًا لمنع الواجهة المتاحة ولا شيئًا لضمانها.

تقلّب أطر العمل

أفسحت مكونات الأصناف المجال للـ hooks، وتقاعدت Create React App لصالح أطر العمل وVite، وتغيّر شكل قصة العرض على الخادم عدة مرات. اختيار مكتبات مملّة وجيدة الصيانة ومقاومة إعادة الكتابة مسؤولية قيادية.

السؤال ليس أبدًا "هل تستطيع React فعل هذا؟" فهي تستطيع في الغالب. السؤال هو "ماذا على الفريق أن يفعل كي تفعل React هذا بشكل صحيح، وهل سيظل يفعله بعد ثلاث سنوات؟"

أنماط البنية والتكامل

افصل نموذج المال عن العرض

القرار المعماري الأعلى قيمة هو تعريف المال والحسابات والتحويلات كأنواع مستقلة عن React، يُتحقق منها بمخطط، وتُشارك بين طبقة النماذج وعميل الـ API، ومثاليًا الخلفية أيضًا. نوع Money بالشكل { amountMinor: number; currency: "EGP" | "SAR" | "AED" | "USD" } مع مخططات zod حوله يلتقط أخطاء أكثر من أي قدر من اختبارات الواجهة. كل شيء آخر في الواجهة الأمامية هو عرض لهذا النموذج.

حالة الخادم ليست حالة العميل

حالة React المدمجة مخصصة لشؤون الواجهة: أي تبويب مفتوح، وهل النافذة المنبثقة ظاهرة، وماذا كتب المستخدم ولم يرسله بعد. كل ما يأتي من الخادم (الأرصدة والمعاملات وحالة اعرف عميلك والأسعار) هو ذاكرة مؤقتة لحقيقة يملكها غيرك، ومكانه مكتبة مبنية للذاكرة المؤقتة. الخلط بينهما هو كيف تتقادم الأرصدة.

المكتبة الأنسب لـ التخزين المؤقت والإبطال التحديثات المتفائلة ملاحظات للتقنية المالية
TanStack Query حالة الخادم عبر REST أو RPC مدمج، بالمفاتيح، مع إعادة جلب في الخلفية من الدرجة الأولى، مع لقطة وتراجع الخيار الافتراضي
SWR الشاشات كثيفة القراءة مدمج، stale-while-revalidate مدعومة بهيكلة أقل مناسبة للوحات القراءة فقط
RTK Query الفرق التي تستخدم Redux أصلًا مدمج، بالوسوم مدعومة منطقية إذا كنت تحتاج Redux على أي حال
Apollo Client واجهات GraphQL ذاكرة مؤقتة معيارية مدعومة التطبيع يحتاج عناية حول حقول المال
Zustand أو Jotai حالة الواجهة فقط لا يوجد غير قابل للتطبيق ليست لبيانات الخادم أبدًا
XState المسارات متعددة الخطوات مثل اعرف عميلك لا يوجد غير قابل للتطبيق حالات صريحة؛ سهلة المراجعة مع الامتثال

الجدول متحيز عمدًا. مسار اعرف عميلك ذو الاثنتي عشرة خطوة، بمستندات مشروطة لكل سوق وجلسات قابلة للاستئناف، هو آلة حالات، ونمذجته كذلك تجعله قابلًا للمراجعة من أشخاص لا يقرؤون React.

النماذج: تحقق عند حافة المكوّن

ينبغي أن ينتج نموذج المبلغ كائنًا مُتحققًا منه ومُنمّطًا أو لا شيء على الإطلاق. ثلاثة تفاصيل تهم في المثال التالي. يُلتقط المبلغ كسلسلة نصية ويُحوَّل إلى وحدات صغرى أثناء التحقق، فلا توجد قيمة بفاصلة عائمة أبدًا. فحص IBAN فحص للصيغة لا تحقق من الصحة؛ الخادم هو من يتحقق من رقم المراقبة والمستفيد. وكل خطأ مرتبط بحقله عبر aria-describedby ويُعلَن عبر role="alert".

الكود: نموذج تحويل مُتحقق منه بـ react-hook-form وzod

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const CURRENCIES = ["EGP", "SAR", "AED", "USD"] as const;

// Money never travels as a float: "1,250.50" becomes 125050 minor units.
function toMinorUnits(raw: string): number | null {
  const clean = raw.replace(/[,\s]/g, "");
  if (!/^\d+(\.\d{1,2})?$/.test(clean)) return null;
  const [whole, frac = ""] = clean.split(".");
  return Number(whole) * 100 + Number(frac.padEnd(2, "0"));
}

const schema = z.object({
  iban: z.string()
    .transform((s) => s.replace(/\s+/g, "").toUpperCase())
    .pipe(z.string().regex(/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/, "Enter a valid IBAN")),
  currency: z.enum(CURRENCIES),
  amount: z.string().min(1, "Amount is required").transform((s, ctx) => {
    const minor = toMinorUnits(s);
    if (minor === null || minor <= 0) {
      ctx.addIssue({ code: "custom", message: "Enter a positive amount, max 2 decimals" });
      return z.NEVER;
    }
    return minor;
  }),
});

type FormInput = z.input<typeof schema>;
export type Transfer = z.output<typeof schema>; // amount is already in minor units

export function TransferForm({ onSubmit }: { onSubmit: (t: Transfer) => Promise<void> }) {
  const { register, handleSubmit, formState: { errors, isSubmitting } } =
    useForm<FormInput, unknown, Transfer>({ resolver: zodResolver(schema) });
  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <label htmlFor="iban">Beneficiary IBAN</label>
      <input id="iban" autoComplete="off" aria-invalid={!!errors.iban}
        aria-describedby={errors.iban ? "iban-error" : undefined} {...register("iban")} />
      {errors.iban && <p id="iban-error" role="alert">{errors.iban.message}</p>}
      <label htmlFor="currency">Currency</label>
      <select id="currency" {...register("currency")}>
        {CURRENCIES.map((c) => <option key={c} value={c}>{c}</option>)}
      </select>
      <label htmlFor="amount">Amount</label>
      <input id="amount" inputMode="decimal" aria-invalid={!!errors.amount}
        aria-describedby={errors.amount ? "amount-error" : undefined} {...register("amount")} />
      {errors.amount && <p id="amount-error" role="alert">{errors.amount.message}</p>}
      <button type="submit" disabled={isSubmitting}>Send transfer</button>
    </form>
  );
}

تم الاختبار مع React 18 وTypeScript 5.

تحديثات متفائلة يمكن التراجع عنها

النمط الذي توثقه TanStack Query (لقطة في onMutate، واستعادة في onError، وإبطال في onSettled) هو بالضبط ما يحتاجه التحويل. تضيف التقنية المالية مفتاح عدم التكرار (idempotency key): معرّف فريد يُولَّد مرة واحدة لكل محاولة إرسال ويُرسل مع الطلب، فلا تستطيع إعادة المحاولة بسبب الشبكة إنشاء تحويل ثانٍ. يعيش المفتاح في متغيرات الطفرة، فتعيد إعادة المحاولة استخدامه بينما تحصل محاولة الإرسال الجديدة على مفتاح جديد.

الكود: تحويل متفائل بـ TanStack Query v5 ومفتاح عدم تكرار

import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { Transfer } from "./TransferForm";

type Account = { id: string; balanceMinor: number; currency: string };
type Variables = { transfer: Transfer; idempotencyKey: string };

async function postTransfer(sourceAccountId: string, { transfer, idempotencyKey }: Variables) {
  const res = await fetch("/api/transfers", {
    method: "POST",
    credentials: "include", // session cookie; no bearer token in localStorage
    headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
    body: JSON.stringify({ ...transfer, sourceAccountId }),
  });
  if (!res.ok) throw new Error(`Transfer failed with ${res.status}`);
  return (await res.json()) as { id: string; status: "pending" | "settled" };
}

export function useSendTransfer(sourceAccountId: string) {
  const qc = useQueryClient();
  const accountKey = ["account", sourceAccountId] as const;

  const mutation = useMutation({
    mutationFn: (vars: Variables) => postTransfer(sourceAccountId, vars),
    onMutate: async ({ transfer }) => {
      await qc.cancelQueries({ queryKey: accountKey });
      const previous = qc.getQueryData<Account>(accountKey);
      if (previous) {
        qc.setQueryData<Account>(accountKey, {
          ...previous,
          balanceMinor: previous.balanceMinor - transfer.amount,
        });
      }
      return { previous };
    },
    onError: (_error, _vars, context) => {
      // Roll back: never show a balance the server has not confirmed.
      if (context?.previous) qc.setQueryData(accountKey, context.previous);
    },
    onSettled: () => {
      qc.invalidateQueries({ queryKey: accountKey });
      qc.invalidateQueries({ queryKey: ["transfers", sourceAccountId] });
    },
  });

  // One key per submit attempt. Retries of this mutation reuse the same
  // variables, so the server can safely de-duplicate; a new submit gets a new key.
  const send = (transfer: Transfer) =>
    mutation.mutateAsync({ transfer, idempotencyKey: crypto.randomUUID() });

  return { send, isPending: mutation.isPending, error: mutation.error };
}

تم الاختبار مع React 18 وTypeScript 5.

خادم وسيط للواجهة، لا API عامة

ينبغي أن تتحدث معظم واجهات التقنية المالية إلى BFF: خادم رقيق يملكه فريق الواجهة الأمامية، يحتفظ بالجلسة، ويجمّع الخدمات الداخلية، ويحذف الحقول التي لا تحتاجها الواجهة، ويضبط ملفات تعريف الارتباط. هو الموطن الطبيعي لملف الجلسة httpOnly ورمز CSRF وحد المعدل وسجل التدقيق.

البيانات الفورية

تصل الأسعار وحالات الأوامر وتنبيهات الاحتيال عبر WebSockets أو أحداث الخادم المرسلة (SSE). تعامل مع التدفق كمصدر لتحديثات الذاكرة المؤقتة (تصل رسالة فتستدعي setQueryData على المفتاح المتأثر) لا كشجرة حالة منفصلة.

الأمان والامتثال

نطاق PCI DSS بكلمات بسيطة

إذا تعاملت صفحتك يومًا مع رقم بطاقة فهي داخل نطاق PCI DSS، وطريقة تعاملها مع هذا الرقم تحدد أي استبيان تقييم ذاتي ينطبق:

الأسلوب أين تُدخل بيانات البطاقة الاستبيان المعتاد ما يعنيه لتطبيق React لديك
إعادة توجيه إلى صفحة المزود المستضافة صفحة المزود SAQ A أصغر نطاق وأقل تحكم
حقول iframe يستضيفها المزود داخل صفحتك iframe المزود على نطاقه هو SAQ A مع ضوابط نصوص صفحة الدفع الأحدث شجرة React لديك لا ترى بيانات البطاقة أبدًا
حقولك أنت ترسل مباشرة إلى API المزود صفحتك SAQ A-EP كود JavaScript لديك يتعامل مع رقم البطاقة؛ نطاق أكبر بكثير
حقولك أنت ترسل إلى خادمك صفحتك وخادمك SAQ D تجنبه ما لم تكن شركة دفع

الفرق بين الصفين الثاني والثالث هو كل سبب وجود الحقول المستضافة. مع حقل iframe لا يستطيع خطأ XSS في تطبيق React لديك قراءة رقم البطاقة، لأنه يعيش في مستند مختلف على نطاق مختلف. كما أضافت النسخة الحالية من PCI DSS متطلبات لإدارة ومراقبة النصوص البرمجية على صفحات الدفع؛ وهي تنطبق حتى مع الحقول المستضافة، فأبقِ صفحة الدفع صغيرة وقائمة نصوصها قصيرة.

الكود: حقل بطاقة داخل نطاق PCI مركّب كإطار iframe مستضاف

import { useEffect, useRef, useState, type FormEvent } from "react";

// Minimal shape of a hosted-fields SDK. Stripe Elements, Adyen Components and
// Checkout.com Frames all follow this pattern: the provider's script renders an
// <iframe> on the provider's origin and hands you a mount/tokenize API.
type HostedCardField = {
  mount: (el: HTMLElement) => void;
  unmount: () => void;
  on: (event: "change", cb: (e: { complete: boolean; error?: { message: string } }) => void) => void;
  tokenize: () => Promise<{ token: string }>;
};
type HostedFieldsSdk = { createCardField: (opts: { locale: string }) => HostedCardField };
declare global { interface Window { PaymentsSdk?: HostedFieldsSdk } }

export function CardField({ onToken }: { onToken: (token: string) => Promise<void> }) {
  const mountRef = useRef<HTMLDivElement>(null);
  const fieldRef = useRef<HostedCardField | null>(null);
  const [complete, setComplete] = useState(false);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    const sdk = window.PaymentsSdk;
    if (!sdk || !mountRef.current) return;
    // Why an iframe: the PAN, expiry and CVC are typed into a document on the
    // provider's origin. Our React state only ever holds "complete" and an error
    // string, never the digits, so this page can stay in PCI DSS SAQ A territory
    // instead of A-EP, and an XSS bug in our bundle cannot read card numbers.
    const field = sdk.createCardField({ locale: document.documentElement.lang || "en" });
    field.mount(mountRef.current);
    field.on("change", (e) => {
      setComplete(e.complete);
      setError(e.error?.message ?? null);
    });
    fieldRef.current = field;
    return () => { field.unmount(); fieldRef.current = null; };
  }, []);

  const submit = async (ev: FormEvent) => {
    ev.preventDefault();
    if (!fieldRef.current) return;
    const { token } = await fieldRef.current.tokenize(); // a reference, not card data
    await onToken(token); // your backend charges with the token; it never sees the PAN
  };
  return (
    <form onSubmit={submit}>
      <span id="card-label">Card details</span>
      <div ref={mountRef} role="group" aria-labelledby="card-label"
        aria-describedby={error ? "card-error" : undefined} />
      {error && <p id="card-error" role="alert">{error}</p>}
      <button type="submit" disabled={!complete}>Pay</button>
    </form>
  );
}

تم الاختبار مع React 18 وTypeScript 5.

XSS وسياسة أمان المحتوى

تهرّب React النص المعروض افتراضيًا، وهذا يزيل أكثر نواقل الحقن شيوعًا ويترك البقية: dangerouslySetInnerHTML، وقيم href المبنية من بيانات المستخدم، والنصوص البرمجية من أطراف ثالثة، وHTML الخادم المجمّع يدويًا. سياسة أمان محتوى (CSP) صارمة (نصوص برمجية بـ nonce، ولا unsafe-inline، وframe-ancestors، وconnect-src محصور في نطاقاتك ومزوديك) تحوّل معظم هذه من اختراقات إلى أخطاء في وحدة التحكم؛ وورقة OWASP المرجعية لـ CSP هي المرجع.

سلسلة توريد الاعتماديات

أودع ملف القفل في المستودع وثبّت بـ npm ci لتكون البنية قابلة لإعادة الإنتاج. شغّل npm audit وماسح ثغرات في CI، وتعامل مع المخرجات كقائمة مهام لها مالكون لا كبوابة. فضّل الحزم المنشورة مع إثبات المنشأ (provenance) الذي يربط الحزمة بالـ commit المصدري والبنية التي أنتجتها. ثبّت وراجع كل ما يلمس الـ DOM أو الشبكة أو التشفير، ولا تحمّل نصوصًا برمجية من CDN خارجي وقت التشغيل (أظهرت polyfill.io السبب)، وعلى صفحة الدفع افرض قائمة سماح قصيرة للنصوص عبر CSP وSubresource Integrity.

الرموز وملفات تعريف الارتباط وCSRF

خزّن الجلسة في ملف تعريف ارتباط httpOnly وSecure وSameSite يضبطه الـ BFF. لا تضع رموز الوصول في localStorage: كل ما تستطيع حمولة XSS قراءته تستطيع تسريبه. تعيد ملفات تعريف الارتباط CSRF إلى النطاق، فيحتاج الـ BFF إلى SameSite=Lax أو Strict مع ترويسة طلب مخصصة لا ترسلها المتصفحات عبر المواقع، أو رمز مزامنة. دوّر رموز التجديد واطلب مصادقة تصعيدية للعمليات عالية القيمة، وهذا ما يقنّنه متطلب المصادقة القوية للعميل في PSD2 بأوروبا وما تتوقعه إرشادات SAMA وCBUAE عمليًا.

الاختطاف بالنقر والتأطير

ينبغي أن ترسل صفحاتك frame-ancestors 'none' (أو قائمة سماح صارمة) كي لا يستطيع أحد وضع نسخة شفافة من زر التحويل لديك فوق موقع خبيث. حقول البطاقات المستضافة أطر iframe على نطاق المزود، والمزود يتحكم في سياسة تأطيرها؛ ما تتحكم فيه أنت هو هل يمكن تأطير صفحة الدفع نفسها. تأكد أنها لا تُؤطَّر.

البيانات الشخصية في التحليلات والسجلات وتسجيل الجلسات

ستلتقط أدوات تسجيل الجلسات وحزم التحليلات ومبلّغات الأخطاء رقم الهوية الوطنية المكتوب في نموذج اعرف عميلك أو رقم IBAN ما لم تُؤمر بغير ذلك. أخفِ كل الحقول افتراضيًا، وامنع التسجيل على مسارات التسجيل والدفع، وأبقِ تسجيل وحدة التحكم خارج بنية الإنتاج. تعامل قوانين حماية البيانات الشخصية في مصر والسعودية والإمارات هذه البيانات كبيانات حساسة، وتسجيل مخزّن في أداة طرف ثالث خارج البلاد هو بالضبط نوع النقل الذي تسأل عنه الجهة الرقابية.

أعلام الميزات للتغييرات الخاضعة للرقابة

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

الجهات الرقابية بالاسم

الأطر التي تشكّل قرارات الواجهة الأمامية في المنطقة هي قواعد البنك المركزي السعودي (SAMA) وإطاره للأمن السيبراني، ولوائح مصرف الإمارات المركزي (CBUAE) لخدمات الدفع والقيمة المخزنة، وقواعد البنك المركزي المصري (CBE) لمقدمي خدمات الدفع والبنوك الرقمية، ولمن يخدم عملاء أوروبيين PSD2 ومتطلبها للمصادقة القوية للعميل. لا يذكر أي منها React. وكلها تهتم بقوة المصادقة وإقامة البيانات وسجلات التدقيق والموافقة، والواجهة الأمامية هي حيث يُنفَّذ معظمها.

التزامات إتاحة الوصول

WCAG 2.2 بمستوى AA هو الهدف العملي. تشير معايير الحكومة الرقمية في السعودية والإمارات إلى WCAG، وينطبق قانون إتاحة الوصول الأوروبي على الخدمات المالية لعملاء الاتحاد الأوروبي. بلغة React: أزرار وحقول حقيقية، وتسميات مرتبطة بالحقول، وأخطاء معلَنة، وإدارة التركيز في النوافذ المنبثقة والمسارات متعددة الخطوات، وتباين في كلا السمتين، ووصول بلوحة المفاتيح إلى كل إجراء.

الأداء والتوسع

حجم الحزمة وتقسيم الكود

لا تحتاج لوحة التحكم إلى مكتبة الرسوم البيانية على صفحة الدخول ولا إلى عارض PDF على شاشة التحويل. ضع ميزانية حزمة لكل مسار، وقسّم على المسارات والمكونات الثقيلة بـ React.lazy، وراقب الميزانية في CI. معظم الوزن ليس React بل ما يُستورد معها: مكتبات التواريخ، ومجموعات الأيقونات، والرسوم البيانية، ومحررات النص المنسق.

SSR ومكونات الخادم وNext.js بصدق

يفيد العرض على الخادم في حالتين: محتوى ينبغي أن يظهر قبل تحميل JavaScript (التسويق والمساعدة والأسعار)، وصفحات مصادَق عليها يهم فيها الرسم الأول والبيانات متاحة من الخادم. تذهب مكونات الخادم في React أبعد بإبقاء الأجزاء كثيفة البيانات وغير التفاعلية من الشجرة بعيدًا عن العميل كليًا. ما لا تساعد فيه: شاشة تداول تفاعلية، ونموذج متعدد الخطوات، وأي شيء يقوده WebSockets. تطبيق Next.js كل مساراته "use client" يدفع ثمن إطار متكامل ويحصل على SPA ثابتة؛ وتطبيق Vite خلف BFF ما يزال بنية ممتازة للوحة تحكم مصادَق عليها.

الترطيب (Hydration)

عدم تطابق الترطيب مصدر كلاسيكي للارتجاف في عرض المبالغ: نسّق الخادم رقمًا بلغة محلية والعميل بأخرى، أو عرض الخادم طابعًا زمنيًا بتوقيت UTC والعميل بتوقيت القاهرة. نسّق الأرقام والتواريخ بلغة محلية ومنطقة زمنية صريحتين تُمرَّران من الخادم، أو اعرضها على العميل فقط، ولا تدع Date.now() يدخل مخرجات العرض على الخادم أبدًا.

البيانات الفورية والجداول الافتراضية

قائمة معاملات بعشرة آلاف صف يجب أن تكون افتراضية (TanStack Virtual أو ما يشبهها) كي لا يكون في الـ DOM سوى الصفوف المرئية. تدفق أسعار يتحدث خمسين مرة في الثانية يجب خنقه إلى معدل تحديث الشاشة ودمجه في تحديث حالة واحد، وهو ما يساعد فيه التجميع التلقائي في React 18 ويمنع useTransition من حجب الكتابة.

الأرقام والعملات والاتجاه والأرقام العربية

استخدم Intl.NumberFormat (المرجع على MDN) بلغة محلية وعملة صريحتين؛ ولا تلصق رمزًا بنتيجة toFixed أبدًا. تستخدم ar-EG وar-SA الأرقام العربية المشرقية افتراضيًا، بينما يتوقع كثير من مستخدمي الخدمات المصرفية في المنطقة الأرقام الغربية على الشاشات المالية. اجعل ذلك قرار منتج، واضبطه مرة واحدة بامتداد nu (ar-EG-u-nu-latn)، وطبّقه في كل مكان. للاتجاه من اليمين إلى اليسار استخدم خصائص CSS المنطقية، ولفّ أرقام IBAN والبطاقات والأكواد المرجعية بـ dir="ltr" مع عزل ثنائي الاتجاه، واختبر كل مبلغ بعلامة سالبة ورمز عملة.

Core Web Vitals عمليًا

تنطبق مقاييس Largest Contentful Paint وInteraction to Next Paint وCumulative Layout Shift جيدًا على ما تحتاجه الواجهة المالية: اعرض الرصيد بسرعة، واستجب للنقرات فورًا، ولا تحرّك التخطيط مع وصول البيانات. يشرح دليل web.dev العتبات. قِس بمراقبة المستخدمين الحقيقيين من الأسواق التي تخدمها؛ فدرجة مختبرية من مركز بيانات أوروبي لا تقول الكثير عن اتصال 4G في صعيد مصر.

الفريق والتوظيف في المنطقة

عمق المواهب

لدى مصر واحد من أكبر تجمعات مهندسي الواجهات الأمامية في المنطقة، وReact هي المهارة المهيمنة فيه. ينمو سوق السعودية بسرعة بدفع من البرامج الرقمية الوطنية ومتطلبات التوطين، ويتنافس بشدة على الكبار. والإمارات هي حيث تتقاطع قيادات المنتج وكبار المهندسين من كل مكان، بتكاليف تناسب ذلك. الشكل الواقعي هو نواة صغيرة من الكبار حيث توجد الرخصة، وفريق بناء أكبر في مصر، ونظام تصميم مشترك يبقيهما متوائمين.

انضباط TypeScript

"يعرف React" معيار منخفض. الإشارة المهمة هي هل يلجأ المرشح إلى الأنواع لتشفير قواعد العمل (نوع MinorUnits موسوم، واتحاد مميّز لحالة التحويل، ومخطط مشترك بين النموذج والـ API) أم يعامل TypeScript كمدقق يُسكَت بـ any. اطلب منه نمذجة تحويل وراقب ماذا يفعل بالمبلغ.

إعادة استخدام نظام التصميم

الفريق الذي يبني أزراره الخاصة في كل مشروع سيبني حقل مبلغه الخاص في كل مشروع، وأحد هذه الحقول سيقبل فاصلة عائمة. استثمر في نظام تصميم مبكرًا، وعيّن له مالكًا، وعامله كمنتج له إصدارات.

إشارات المقابلة

اسأل كيف سيخزّن رمز الجلسة ولماذا؛ وماذا يحدث للرصيد إذا فشل التحويل بعد التحديث المتفائل؛ وكيف يجعل نموذجًا قابلًا للاستخدام بلوحة المفاتيح فقط؛ وما ناتج 0.1 + 0.2 في JavaScript وماذا يفعل حياله. كلها تفصل بين من شحن واجهات مال ومن شحن واجهات فحسب.

شكل الفريق والملكية

أعطِ فريق الواجهة الأمامية ملكية الـ BFF. الفريق الذي يملك الجلسة وشكل الـ API الذي يستهلكه وملفات تعريف الارتباط التي يضبطها يمكن تحميله مسؤولية أمان الواجهة؛ أما الفريق الذي يملك المكونات فقط فلا.

إطار اتخاذ القرار

متى تكون React الخيار الصحيح

لوحات التحكم والبوابات المصادَق عليها ذات التفاعل الغني. مسارات التسجيل واعرف عميلك ذات الخطوات الكثيرة والمنطق الشرطي. شاشات التداول والخزينة ذات البيانات الفورية. أدوات العمليات الداخلية. وأي شيء يضمّن مكونات مزودين تُشحن لـ React أولًا.

متى تكون المنظومة المعروضة على الخادم أفضل

الصفحات العامة كثيفة المحتوى (الأسعار ومراكز المساعدة والإفصاحات التنظيمية) تخدمها أفضل منظومة معروضة على الخادم أو ثابتة، قد تكون Next.js أو Astro بجزر React، أو بلا React إطلاقًا. أدوات المكاتب الخلفية البسيطة ذات قاعدة المستخدمين الصغيرة تُشحن أسرع على إطار معروض على الخادم (Django أو Rails أو Laravel) مع قليل من التفاعلية فوقه.

متى يفوز التطبيق الأصلي

الخدمات المصرفية للأفراد في المنطقة تبدأ من الجوال، والهاتف يجلب قدرات لا يملكها الويب: القياسات الحيوية، والتخزين الآمن، وتصديق الجهاز، والإشعارات الفورية للموافقة على المعاملات. React على الويب هي الموطن الصحيح للوحة التحكم وبوابة التاجر ولوحة العمليات؛ أما محفظة الأفراد فعادة تطبيق أصلي أو Flutter، مع React Native خيارًا معقولًا للفرق المتعمقة في React أصلًا.

مصفوفة القرار

الحاجة React SPA (Vite) مع BFF Next.js أو إطار React آخر Vue أو Svelte Angular تطبيق أصلي أو Flutter
لوحة تحكم مصادَق عليها بتفاعل غني قوي قوي قوي قوي لا ينطبق على الويب
محتوى عام وSEO ورسم أول ضعيف قوي قوي مع SSR متوسط لا ينطبق
اعرف عميلك متعدد الخطوات مع استئناف قوي قوي قوي قوي قوي
شاشات تداول فورية قوي قوي (الأجزاء على العميل) قوي قوي قوي
مكونات الدفع المستضافة أغلفة React رسمية أغلفة React رسمية SDK بـ JavaScript عادي SDK بـ JavaScript عادي SDK أصلية
عمق التوظيف في المنطقة الأعمق الأعمق متوسط متوسط، يميل للمؤسسات Flutter قوية في مصر
أمان الجهاز (قياسات حيوية، تصديق) WebAuthn فقط WebAuthn فقط محدود محدود قوي
مخاطر الصيانة طويلة الأمد منخفضة مع الانضباط متوسطة (تقلب الإطار) منخفضة منخفضة تعتمد على المنصة

المصفوفة ليست بطاقة درجات تُجمع. لمعظم شركات التقنية المالية في المنطقة الجواب هو React لسطح الويب، وتطبيق جوال للأفراد، ومنظومة معروضة على الخادم للمحتوى العام.

الأسئلة الشائعة

هل React آمنة بما يكفي للتطبيقات المصرفية؟

React آمنة بقدر أمان التطبيق المبني عليها. تهرّب النص المعروض افتراضيًا، وهذا يزيل أكثر نواقل الحقن شيوعًا، وليس لها رأي في الجلسات أو ملفات تعريف الارتباط أو CSP أو الاعتماديات، وهي حيث تُخترق التطبيقات المصرفية فعلًا. يأتي الوضع الأمني من القرارات المحيطة: ملفات httpOnly عبر BFF، وCSP صارمة، وقائمة اعتماديات مراجَعة، وحقول مستضافة للبطاقات، ولا بيانات شخصية في نصوص الأطراف الثالثة.

هل نستخدم Next.js أم React وحدها للوحة تحكم في التقنية المالية؟

إذا كان للمنتج سطح عام معتبر، أو كان الرسم الأول على الصفحات المصادَق عليها مهمًا وبياناتك متاحة من الخادم، فإن Next.js أو إطار React آخر بعرض على الخادم يستحق تعقيده. وإذا كان المنتج لوحة تحكم مصادَق عليها بقاعدة مستخدمين مستقرة وتفاعلية كثيفة، فإن SPA بـ Vite خلف BFF أبسط تشغيلًا وأبسط تأمينًا وأسهل تبريرًا في التدقيق.

كيف نتعامل مع المبالغ وتنسيق العملات في React؟

احمل المبالغ كأعداد صحيحة بالوحدات الصغرى من الحقل إلى الـ API، مع العملة بجانبها. تحقق عند حدود النموذج بـ zod أو ما يشبهها، ولا تضع مبلغًا عشريًا في number أبدًا، ونسّق للعرض فقط في اللحظة الأخيرة بـ Intl.NumberFormat ولغة محلية وعملة ونظام أرقام صريحة. قرر مرة واحدة هل تستخدم الشاشات العربية الأرقام الغربية أم المشرقية وطبّق القرار في كل مكان.

هل يمكن إبقاء نطاق PCI DSS صغيرًا مع صفحة دفع مبنية بـ React؟

نعم، وهذا السبب الرئيسي لاستخدام الحقول التي يستضيفها المزود. حين يُكتب رقم البطاقة في iframe على نطاق المزود لا يحتفظ تطبيق React لديك به أبدًا، ويمكن لتقييمك عادة أن يكون SAQ A الأبسط بدلًا من SAQ A-EP. ما تزال تملك الصفحة التي تضمّن الـ iframe، فعليك التحكم في نصوصها البرمجية، ومنع تأطيرها، وإبعاد أدوات التحليلات والتسجيل عنها.

هل من الصعب توظيف مطوري React في مصر والسعودية والإمارات؟

العثور على مطوري React ليس صعبًا؛ فهي أشيع مهارة واجهات أمامية في الأسواق الثلاثة. الأصعب هو العثور على مهندسين يعاملون TypeScript كأداة نمذجة، ويفهمون حالة الخادم، وشحنوا نموذج مبلغ يتعامل مع الوحدات الصغرى بشكل صحيح، وهنا ينبغي أن تركز المقابلات. تقدم مصر التجمع الأعمق، والسعودية الطلب الأسرع نموًا، والإمارات أكثر الكفاءات الكبيرة تركّزًا وبأعلى تكلفة.

الخلاصة

  • React هي الخيار الافتراضي في التقنية المالية بسبب منظومتها لا المكتبة نفسها: النماذج وتخزين حالة الخادم وأنظمة التصميم ومكونات الدفع المستضافة كلها تُشحن لـ React أولًا.
  • المال عدد صحيح بالوحدات الصغرى مع عملة. يدخل كسلسلة نصية، ويُتحقق منه عند حدود النموذج، ولا يُنسَّق إلا عند العرض.
  • حالة الخادم مكانها مكتبة حالة خادم بإبطال وتراجع صريحين؛ قد يسبق التحديث المتفائل الخادم للحظة، لكنه لا يجوز أن يبقى خاطئًا.
  • كل طفرة تحرّك مالًا تحمل مفتاح عدم تكرار يُولَّد مرة لكل محاولة إرسال ويُعاد استخدامه عند إعادة المحاولة.
  • بيانات البطاقة مكانها iframe يستضيفه المزود؛ هذا القرار وحده يُبقي تطبيق React خارج أثقل نطاقات PCI DSS.
  • رموز الجلسة في ملفات تعريف ارتباط httpOnly يضبطها BFF، مع حماية CSRF وCSP صارمة؛ ولا شيء حساس في localStorage أو التحليلات أو تسجيل الجلسات.
  • كن صادقًا بشأن العرض على الخادم: استخدمه حيث يهم الرسم الأول والمحتوى العام، واقبل مكونات العميل حيث التفاعل هو الهدف، واضبط ميزانيات الحزمة والمقاييس الحيوية في CI من اليوم الأول.
  • وظّف على أساس انضباط TypeScript وحسن التقدير في التعامل مع المال لا الألفة مع React، واستثمر مبكرًا في نظام تصميم مشترك.