IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA21/32-dars24 daqiqa
Mundarija (34)

To'lov UI React'da: Click, Payme va Uzum — checkout, qaytish sahifasi va holat

Qisqacha: Frontend pulni o'zi olmaydi. U serverdan buyurtma va tayyor to'lov manzilini so'raydi, mehmonni Payme yoki Click sahifasiga yuboradi, mehmon qaytgach esa serverdan "to'landimi?" deb so'rab turadi (polling). Summa va holat — faqat serverda: brauzerdagi amount ni har kim o'zgartiradi, ?status=paid ni har kim yozadi. vazifalar CSP'si ostida sinaldi: Payme uslubidagi forma POST'ni form-action 'self' to'sadi, Click uslubidagi GET yo'naltirish o'tadi. Haqiqiy to'lov, kalit va callback — 29-qismda.

Bu darsda

  • Onlayn to'lovning umumiy oqimini chizasiz: buyurtma → checkout → callback → qaytish → holat.
  • Payme (GET base64 va forma POST) va Click (GET) checkout manzillarini hujjat bo'yicha tuzasiz va tiyin/so'm farqini bilasiz.
  • Qaytish sahifasida holatni polling bilan so'raysiz: kutish, muvaffaqiyat, bekor, muddati o'tdi.
  • Ikki marta bosishdan idempotentlik kaliti bilan himoyalanasiz.
  • CSP'da form-action va yo'naltirish farqini tushuntirasiz; Uzum va Stripe Elements qayerga tushishini bilasiz.

Oldin bilishingiz kerak: TanStack Query III: mutatsiyalar, React'da auth holati, URL va URLSearchParams, Brauzer xavfsizlik modeli, Feature flag.

1. Nega bu kerak?

«Bahor»da onlayn buyurtma bor, lekin to'lov — yetkazib berilganda, naqd. Jasur aka endi Payme va Click orqali to'lov xohlaydi: kuryer pul sanamasin, buyurtma faqat to'langandan keyin oshxonaga tushsin.

Birinchi o'ylaydigan narsa: "Karta raqami formasini yasaymiz, raqamni serverga yuboramiz". Bu — eng xavfli yo'l. Karta ma'lumotini qabul qilgan, saqlagan yoki uzatgan har tizim qat'iy xavfsizlik standartiga (PCI DSS) bo'ysunadi. Kichik restoran buni ko'tarolmaydi.

Shuning uchun O'zbekiston to'lov tizimlari (Payme, Click, Uzum) va xalqaro Stripe bir xil yo'l taklif qiladi: karta raqamini ular so'raydi, o'z sahifasida. Sizning ilovangiz mehmonni u yerga yuboradi va natijani kutadi.

Hayotiy o'xshatish: kommunal to'lovni bankda to'lash. Siz kvitansiya bilan bank kassasiga borasiz, pulni kassir oladi. Kommunal xizmat sizning "to'ladim" degan gapingizga emas, bankning xabariga ishonadi. Ilova ham shunday: mehmon qaytib "to'ladim" sahifasini ochgani — dalil emas. Dalil — to'lov tizimining serverga yuborgan xabari.

2. Umumiy oqim

2.1 Kim nima qiladi

sequenceDiagram
  participant B as Brauzer (React)
  participant S as Bizning server
  participant P as Payme / Click
  B->>S: buyurtma: taomlar (summa YO'Q)
  S->>S: summani hisoblaydi, buyurtma "kutilmoqda"
  S-->>B: buyurtma id + checkout manzili
  B->>P: yo'naltirish (karta — P sahifasida)
  P->>S: callback: "42-buyurtma, 35 000 so'm"
  S->>S: summani tekshiradi → "to'landi"
  P-->>B: return_url ga qaytaradi
  B->>S: polling: 42 holati?
  S-->>B: "to'landi"

Nimaga qarang: bizning server ikki xabar oladi — brauzerdan (buyurtma) va to'lov tizimidan (callback). "To'landi" holatini faqat ikkinchisi beradi. Brauzer qaytishi esa shunchaki "mehmon sahifaga qaytdi" degani.

Bu yerda uchta yangi so'z bor:

  • Checkout (to'lov sahifasi) — to'lov tizimining karta so'raydigan sahifasi. Siz uni chizmaysiz, faqat manzilini tuzasiz.
  • Callback (to'lov tizimining serverga xabari; Payme'da — Merchant API, Click'da — Prepare/Complete) — to'lov tizimi sizning serveringizni chaqiradi: "shu buyurtma uchun pul tushdimi, summasi to'g'rimi?". Bu — backend ishi (29-qism).
  • Return URL (qaytish manzili) — to'lovdan keyin mehmon qaytadigan sahifa: /tolov/qaytish?buyurtma=42.

2.2 Frontend'ning to'rt ishi

  1. Savatcha va buyurtma: tanlangan taomlar serverga ketadi — summasiz. Summani server o'z narxlari bo'yicha hisoblaydi.
  2. To'lov usuli va yo'naltirish: mehmon Payme yoki Click'ni tanlaydi, server manzil qaytaradi, brauzer o'tadi.
  3. Qaytish sahifasi: holatni serverdan so'rab turadi va natijani ko'rsatadi.
  4. Xatolar: to'lov bekor qilindi, muddati o'tdi, tarmoq uzildi — har biriga tushunarli xabar.

3. Checkout manzili

3.1 Payme: base64 bilan GET

Payme hujjati (developer.help.paycom.uz, "checkout" bo'limi) ikki yo'l beradi. Birinchisi — parametrlarni ; bilan qo'shib, base64 qilib manzilga yozish: https://checkout.paycom.uz/<base64>. Asosiy parametrlar:

Parametr Ma'nosi
m merchant (kassa) id — Payme kabinetida beriladi
ac.order_id hisob maydoni — sizning buyurtma raqamingiz
a summa tiyinda (35 000 so'm = 3 500 000)
l til: uz, ru, en
c qaytish manzili (to'lov yoki bekor qilishdan keyin)

Ixtiyoriylari ham bor: ct — muvaffaqiyatli to'lovdan keyin qaytarishdan oldingi kutish (millisekund), cr — valyuta kodi. Manzilni server tuzadi — lekin funksiyaning o'zi oddiy JS, uni shu yerda ko'ramiz:

js
function paymeUrl({ merchant, orderId, tiyin, lang, returnUrl }) {
  const params = [
    `m=${merchant}`,
    `ac.order_id=${orderId}`,
    `a=${tiyin}`,
    `l=${lang}`,
    `c=${returnUrl}`,
  ].join(";");
  return `https://checkout.paycom.uz/${btoa(params)}`;
}

const url = paymeUrl({
  merchant: "SINOV",
  orderId: 42,
  tiyin: 3500000,
  lang: "uz",
  returnUrl: "https://example.com/q?b=42",
});
console.log(new URL(url).host);
console.log(atob(url.split("/").pop()));

Konsolda:

text
checkout.paycom.uz
m=SINOV;ac.order_id=42;a=3500000;l=uz;c=https://example.com/q?b=42

btoa — matnni base64 ga, atob — orqaga. Base64 — shifr emas, shunchaki boshqa yozuv: har kim atob bilan ochib, summani o'qiydi va o'zgartiradi. Demak hujumchi a=100 qilib (1 so'm) qayta kodlay oladi. Nega bu baribir xavfsiz? Payme to'lovdan oldin sizning serveringizdan so'raydi: "42-buyurtma uchun 100 tiyin to'g'rimi?" (CheckPerformTransaction). Server "yo'q, 3 500 000" deydi — to'lov o'tmaydi. Himoya — callback'da, manzilda emas.

Diqqat: Haqiqiy m (merchant id) va test muhiti — Payme kabinetidan. Biz hisob ochmadik: SINOV — o'rinbosar, bu manzil hech qayerga olib bormaydi. Kod hujjat bo'yicha, haqiqiy to'lov bilan sinalmagan.

3.2 Payme: forma POST

Ikkinchi yo'l — yashirin maydonli HTML forma, method="post" bilan Payme manziliga:

html
<form method="post" action="https://checkout.paycom.uz">
  <input type="hidden" name="merchant" value="SINOV" />
  <input type="hidden" name="amount" value="3500000" />
  <input type="hidden" name="account[order_id]" value="42" />
  <input type="hidden" name="lang" value="uz" />
  <input
    type="hidden"
    name="callback"
    value="https://example.com/q?b=42"
  />
  <button>Payme orqali to'lash</button>
</form>

Natija bir xil, faqat parametrlar manzilda emas, so'rov tanasida. Bu yo'lning React ilovada bitta muhim tuzog'i bor — CSP («Biz sinab ko'rdik» bo'limida).

3.3 Click: oddiy GET

Click hujjati (docs.click.uz, "Payment button" — to'lov tugmasi) — my.click.uz/services/pay manzili va oddiy so'rov parametrlari. Majburiylari — service_id, merchant_id, amount, transaction_param; ixtiyoriylari — return_url, card_type (uzcard/humo), merchant_user_id. Muhim farq: summa so'mda, hujjatdagi format N.NN — nuqtadan keyin ikki xona:

js
function clickUrl(order) {
  const { serviceId, merchantId, orderId, som, returnUrl } = order;
  const query = new URLSearchParams({
    service_id: serviceId,
    merchant_id: merchantId,
    amount: som.toFixed(2), // so'm, tiyin EMAS
    transaction_param: String(orderId),
    return_url: returnUrl,
  });
  return `https://my.click.uz/services/pay?${query}`;
}

const url = clickUrl({
  serviceId: "SINOV",
  merchantId: "SINOV",
  orderId: 42,
  som: 35000,
  returnUrl: "https://example.com/q?b=42",
});
for (const [key, value] of new URL(url).searchParams) {
  console.log(`${key} = ${value}`);
}

Konsolda:

text
service_id = SINOV
merchant_id = SINOV
amount = 35000.00
transaction_param = 42
return_url = https://example.com/q?b=42

URLSearchParams qaytish manzilidagi ? va = belgilarini o'zi kodlaydi (URLSearchParams) — qo'lda satr qo'shsangiz, return_url ichidagi ?b=42 asosiy manzilning parametri bo'lib qolardi.

Payme Click
Summa tiyinda, butun son so'mda, 35000.00
Buyurtma ac.order_id (hisob maydoni) transaction_param
Qaytish c / callback return_url
Usul GET (base64) yoki forma POST GET

Tiyin va so'mni aralashtirish — eng qimmat xato: 35 000 so'm o'rniga 350 so'm yoki 3.5 million. Pul bilan qanday ishlash (butun son, yaxlitlash) — Pul bilan ishlash JS'da.

3.4 Uzum va boshqalar

Uzum (Uzum Bank) va shunga o'xshash xizmatlar (Paynet, Octo, Atmos) ko'pincha boshqa uslubda ishlaydi. Server to'lov tizimining API'siga "shu buyurtma uchun to'lov ochib ber" so'rovini yuboradi. Javobda esa tayyor to'lov sahifasi manzilini oladi. Frontend uchun farqi yo'q — serverdan kelgan manzilga o'tiladi. Har tizimning parametr nomlari va qadamlari boshqacha, ularni hujjatdan o'qish — alohida ko'nikma (Uzum va boshqa tizimlar). Biz Uzum bilan sinamadik.

Shuning uchun ilovada to'lov tizimi nomi va manzil tuzilmasini komponentga yozmang. Komponent serverdan { redirectUrl } oladi va o'tadi — tizim qo'shilsa yoki o'zgarsa, frontend o'zgarmaydi.

Tekshirib ko'ring: Click uchun amount: 3500000 yuborildi (tiyin deb o'ylab). Nima bo'ladi?

Javob

Click sahifasi mehmondan 3 500 000 so'm — yuz baravar ko'p so'raydi. Mehmon to'lamaydi (yoki adashib to'laydi). Server callback'da summani tekshirsa, to'lov baribir buyurtmaga mos kelmaydi va rad etiladi. Shuning uchun manzilni bitta joyda — serverda, test bilan tuzish kerak.

4. Jonli: checkout sahifasi

Endi butun "to'lashga o'tish" qadami. Server — 200 ms kechikadigan soxta funksiya. Haqiqiy location.assign(url) o'rniga manzil konsolga yoziladi (natija oynasidan chiqib ketmaslik uchun):

jsx
import { useState } from "react";

const CART = [
  { id: "osh", name: "Osh", qty: 1 },
  { id: "choy", name: "Ko'k choy", qty: 2 },
];

// O'quv modeli: server narxni O'ZI biladi, brauzer faqat id va soni
const PRICES = { osh: 3500000, choy: 500000 }; // tiyin
function createOrder(items, provider) {
  return new Promise((resolve) =>
    setTimeout(() => {
      const tiyin = items.reduce(
        (sum, it) => sum + PRICES[it.id] * it.qty,
        0,
      );
      resolve({
        orderId: 42,
        tiyin,
        redirectUrl: `https://${provider}.example/pay?order=42`,
      });
    }, 200),
  );
}

function formatSom(tiyin) {
  const som = String(Math.round(tiyin / 100));
  return som.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " so'm";
}

export default function App() {
  const [provider, setProvider] = useState("payme");
  const [status, setStatus] = useState("idle");
  const [order, setOrder] = useState(null);

  async function pay() {
    setStatus("creating");
    const result = await createOrder(
      CART.map(({ id, qty }) => ({ id, qty })), // summa YO'Q
      provider,
    );
    setOrder(result);
    setStatus("redirecting");
    console.log("yo'naltirish:", result.redirectUrl);
    // haqiqiy ilovada: location.assign(result.redirectUrl);
  }

  return (
    <section>
      <ul>
        {CART.map((it) => (
          <li key={it.id}>
            {it.name} × {it.qty}
          </li>
        ))}
      </ul>
      <fieldset disabled={status !== "idle"}>
        <legend>To'lov usuli</legend>
        {["payme", "click"].map((p) => (
          <label key={p}>
            <input
              type="radio"
              name="provider"
              checked={provider === p}
              onChange={() => setProvider(p)}
            />
            {p === "payme" ? "Payme" : "Click"}{" "}
          </label>
        ))}
      </fieldset>
      <button onClick={pay} disabled={status !== "idle"}>
        To'lashga o'tish
      </button>
      <p role="status">
        {status === "creating" && "Buyurtma yaratilmoqda…"}
        {status === "redirecting" &&
          `Jami ${formatSom(order.tiyin)} — ` +
            "Click sahifasiga o'tilmoqda"}
      </p>
    </section>
  );
}

Konsolda:

text
yo'naltirish: https://click.example/pay?order=42

Ekranda:

text
Osh × 1
Ko'k choy × 2
To'lov usuli
Payme Click
To'lashga o'tish
Jami 45 000 so'm — Click sahifasiga o'tilmoqda

Qatorma-qator:

  • Brauzer summani yubormaydi — faqat { id, qty }. Jami (45 000 so'm) serverdan keldi: osh 35 000 + 2 × ko'k choy 5 000. Ekrandagi narxlar faqat ko'rsatish uchun, hisob — serverda.
  • disabled={status !== "idle"} — buyurtma yaratilayotganda tugma va radio guruh o'chiq: mehmon ikki marta bosolmaydi, usulni yarim yo'lda almashtirolmaydi. fieldset disabled ichidagi hamma maydonni birdan o'chiradi.
  • role="status" — Malikaning ekran o'quvchisi "Buyurtma yaratilmoqda" ni o'zi o'qiydi (accessibility).
  • formatSom — Intl.NumberFormat emas: natija oynasidagi Chrome uz uchun 35,000 chiqaradi (i18n II dagi tuzoq). Qo'lda bo'sh joy bilan.

Bu misolda matn "Click" ga qotib qolgan — 2-mashqda uni to'g'rilaysiz.

5. Qaytish sahifasi va polling

5.1 Qaytdi — lekin to'ladimi?

Mehmon Click sahifasidan /tolov/qaytish?buyurtma=42 ga qaytdi. Uch holat bo'lishi mumkin:

  • to'ladi, callback serverga allaqachon kelgan;
  • to'ladi, lekin callback hali kelmagan (tarmoq, navbat — bir necha soniya);
  • to'lamadi: "Bekor qilish" ni bosdi yoki sahifani yopib, keyin "orqaga" bilan qaytdi.

Qaytish sahifasi buni bilmaydi. U serverdan so'raydi va kerak bo'lsa qayta so'raydi — har 2 soniyada, ko'pi bilan 1–2 daqiqa. Bu — polling (so'rab turish; WebSocket va SSE darsida uchragan).

stateDiagram-v2
  [*] --> Tekshirilmoqda
  Tekshirilmoqda --> Tekshirilmoqda: "kutilmoqda" (2 s dan keyin yana)
  Tekshirilmoqda --> Tolandi: "tolandi"
  Tekshirilmoqda --> Bekor: "bekor"
  Tekshirilmoqda --> Noaniq: urinishlar tugadi
  Tolandi --> [*]
  Bekor --> [*]
  Noaniq --> [*]

Nimaga qarang: Noaniq holati bor. Urinishlar tugadi, javob — hamon "kutilmoqda". Bu "to'lov o'tmadi" degani emas! Mehmonga: "To'lov tekshirilmoqda. Pul yechilgan bo'lsa, buyurtma bir necha daqiqada tasdiqlanadi" — va qo'ng'iroq raqami. "Qayta to'lang" deyish — ikki marta to'lovga olib boradi.

5.2 Jonli: holatni so'rab turish

Server birinchi ikki so'rovda "kutilmoqda", uchinchisida "tolandi" deydi (o'quv modeli, oraliq 200 ms):

jsx
import { useEffect, useState } from "react";

const answers = ["kutilmoqda", "kutilmoqda", "tolandi"];
let calls = 0;
function fetchStatus(orderId) {
  return new Promise((resolve) =>
    setTimeout(() => resolve(answers[calls++] ?? "tolandi"), 50),
  );
}

const INTERVAL = 200; // haqiqiy ilovada ~2000 ms
const MAX_TRIES = 10;

function usePaymentStatus(orderId) {
  const [status, setStatus] = useState("kutilmoqda");
  useEffect(() => {
    let stopped = false;
    let timer;
    async function check(attempt) {
      const s = await fetchStatus(orderId);
      if (stopped) return; // sahifadan chiqib ketilgan
      console.log(`${attempt}-so'rov: ${s}`);
      if (s !== "kutilmoqda") return setStatus(s);
      if (attempt >= MAX_TRIES) return setStatus("noaniq");
      timer = setTimeout(() => check(attempt + 1), INTERVAL);
    }
    check(1);
    return () => {
      stopped = true;
      clearTimeout(timer);
    };
  }, [orderId]);
  return status;
}

const TEXT = {
  kutilmoqda: "To'lov tekshirilmoqda…",
  tolandi: "To'landi! Buyurtma oshxonaga yuborildi.",
  bekor: "To'lov bekor qilindi.",
  noaniq: "To'lov hali tasdiqlanmadi. Pul yechilgan bo'lsa, kuting.",
};

export default function App() {
  const status = usePaymentStatus(42);
  return <p role="status">{TEXT[status]}</p>;
}

Konsolda:

text
1-so'rov: kutilmoqda
2-so'rov: kutilmoqda
3-so'rov: tolandi

Ekranda:

text
To'landi! Buyurtma oshxonaga yuborildi.
  • Keyingi so'rov — oldingisi tugagandan keyin (setTimeout zanjiri, setInterval emas). Server sekin javob bersa, so'rovlar ustma-ust tushmaydi.
  • stopped va clearTimeout — effekt cleanup: mehmon sahifadan ketsa, polling to'xtaydi. Aks holda yopilgan komponent setStatus ni chaqirib yuradi.
  • MAX_TRIES — cheksiz so'rash yo'q; tugasa — «Noaniq».

Ilovangizda TanStack Query bo'lsa, xuddi shu narsa bitta sozlama: refetchInterval: (q) => q.state.data === "kutilmoqda" ? 2000 : false (TanStack Query). Kanon vazifalar da Query bor — to'lov qo'shilganda shu yo'l tabiiy.

Tekshirib ko'ring: Qaytish manzili /tolov/qaytish?buyurtma=42&holat=tolandi bo'lsa, holat ni o'qib, "To'landi!" ni darhol ko'rsatish mumkinmi?

Javob

Ko'rsatish uchun ham — yo'q. Manzildagi har parametrni mehmon o'zi yozishi mumkin: to'lamasdan ?holat=tolandi ni ochadi va "To'landi" ekranining skrinshotini kuryerga ko'rsatadi. Ba'zi tizimlar qaytish manziliga holat qo'shadi — uni faqat "taxmin" sifatida ("Tekshirilmoqda…" ni tezroq almashtirish uchun) ishlatish mumkin. Yakuniy so'z — serverniki.

6. Ikki marta bosish va idempotentlik

6.1 Muammo

Sekin internet. Mehmon «To'lashga o'tish» ni bosdi, hech narsa bo'lmadi, yana bosdi. Serverga ikkita "buyurtma yarat" so'rovi ketdi — ikkita buyurtma, oshxonaga ikki osh. Tugmani disabled qilish ko'p holatni yopadi, lekin hammasini emas: sahifa yangilanishi, tarmoq qayta urinishi, ikki tab.

Ishonchli yechim — idempotentlik (asinxron naqshlar): bir xil so'rov necha marta kelmasin, natija bitta. Brauzer har "to'lov urinishi" uchun bitta tasodifiy kalit yaratadi va har so'rovga qo'shadi. Server kalitni eslaydi: ko'rgan kalit kelsa — yangi buyurtma yaratmaydi, eskisini qaytaradi.

6.2 Jonli: kalit bilan

Tugma ataylab o'chirilmagan — kalit qanday ishlashini ko'rish uchun:

jsx
import { useState } from "react";

// O'quv modeli: server kalit → buyurtma xaritasini saqlaydi
const seen = new Map();
let nextId = 41;
function createOrder(idempotencyKey) {
  if (seen.has(idempotencyKey)) {
    console.log("server: takror —", seen.get(idempotencyKey));
    return seen.get(idempotencyKey);
  }
  nextId += 1;
  seen.set(idempotencyKey, nextId);
  console.log("server: yangi buyurtma", nextId);
  return nextId;
}

let keys = 0;
// haqiqiysi: crypto.randomUUID()
const newKey = () => `kalit-${++keys}`;

export default function App() {
  // bitta savatcha — bitta kalit
  const [key, setKey] = useState(newKey);
  return (
    <>
      <button onClick={() => createOrder(key)}>To'lash</button>
      <button onClick={() => setKey(newKey())}>Yangi savatcha</button>
    </>
  );
}

Konsolda:

text
server: yangi buyurtma 42
server: takror — 42
server: yangi buyurtma 43

Ikkinchi bosish yangi buyurtma yaratmadi — server kalitni tanidi. Yangi savatcha — yangi kalit, yangi buyurtma. Haqiqiy ilovada kalit crypto.randomUUID() bilan yaratiladi va sarlavhada yuboriladi: Idempotency-Key: 3f1c…. Stripe API aynan shu sarlavhani qabul qiladi. Kalitni qachon yangilash — savatcha o'zgarganda yoki to'lov tugaganda.

Callback'lar ham takrorlanadi: Payme va Click bir xabarni bir necha marta yuborishi mumkin. Server ularni ham tranzaksiya id bo'yicha bir marta qayta ishlaydi (webhook'lar).

7. Biz sinab ko'rdik: CSP va form-action

7.1 Tajriba

vazifalar ning CSP'sida form-action 'self' bor — formalar faqat o'z origin'imizga yuborilishi mumkin (Brauzer xavfsizlik modeli). To'lov sahifasiga o'tish bu qoidaga qanday ta'sir qiladi? Kanon tajribasi (tajriba/tolov/) ikki tugmali sahifani shu CSP ostida ochdi:

html
<form id="payme" method="post" action="https://checkout.paycom.uz">
  <input type="hidden" name="merchant" value="SINOV" />
  <input type="hidden" name="amount" value="3500000" />
  <button>Payme orqali (forma POST)</button>
</form>
<button id="click">Click orqali (yo'naltirish)</button>

Click tugmasi location.assign("https://my.click.uz/services/pay?…") qiladi. Har ikki tugma puppeteer bilan bosildi, tashqi so'rovlar esa uzildi — haqiqiy to'lov tizimiga hech narsa yetib bormadi. Natija (natijalar/tajriba/21-tolov-csp.txt, Chrome 155):

text
Payme: forma POST checkout.paycom.uz: CSP ["form-action https://checkout.paycom.uz/"]; tashqi so'rov urinildi: []
Click: location.assign my.click.uz: CSP []; tashqi so'rov urinildi: ["GET my.click.uz", …]

(Click qatoridagi qolgan uchta yozuv — uzilgan so'rovdan keyingi bo'sh hostli so'rovlar, qisqartirildi.)

7.2 Nima bo'ldi

  • Forma POST — brauzer form-action ni buzilish deb topdi va formani yubormadi: tashqi so'rov ham yo'q. Mehmon tugmani bosadi — hech narsa bo'lmaydi, faqat konsolda xato.
  • GET yo'naltirish — CSP to'smadi, brauzer my.click.uz ga yo'l oldi. location.assign — forma emas, oddiy sahifa o'tishi, CSP esa sahifa o'tishlarini cheklamaydi.

Ikki yechim:

  1. Payme'ni ham GET (base64) bilan ishlatish — CSP o'zgarmaydi.
  2. Forma kerak bo'lsa — CSP'ni ongli kengaytirish: form-action 'self' https://checkout.paycom.uz.

Saboq kengroq: CSP'ni yoqqan jamoa uchinchi tomon integratsiyasini o'z siyosati ostida sinashi kerak. Ishlab chiqishda CSP yo'q bo'lsa, xato production'da, birinchi haqiqiy mehmonda chiqadi.

7.3 Nega vazifalar da to'lov yo'q

Vazifalar ro'yxatida sotiladigan narsa yo'q — kanonga to'lov qo'shilmadi. To'lov uchun server kerak: buyurtmalar, callback'lar, imzo tekshiruvi. O'z serverimiz 25-qismda (HTTP, Express va REST API), to'lov tizimlari bilan to'liq integratsiya — 29-qismda. Bu darsdagi UI o'sha yerda haqiqiy backend'ga ulanadi.

8. Test rejimi va Stripe

8.1 Sinov muhiti

Har to'lov tizimi sinov muhiti (sandbox, test rejimi) beradi: alohida kassa id, test kartalar, haqiqiy pul yechilmaydi. Ish tartibi: kabinetda test kassa → test kartalar bilan butun oqim (to'lov, bekor, xato) → tizim integratsiyani tekshiradi → production kalitlari. Production'da haqiqiy pul bilan "sinab ko'rish" — yo'q. Test va production manzillari ham farq qiladi, shuning uchun manzil env'dan, kodga qotirilmaydi.

Frontend uchun sinov muhitining foydasi: «Bekor qilish», "karta rad etildi", "muddati o'tdi" holatlarini haqiqiy sahifada ko'rib, qaytish sahifangiz har birini to'g'ri ko'rsatishini tekshirasiz.

8.2 Stripe Elements (xalqaro)

Xalqaro loyihalarda Stripe — standart. U ikki yo'l beradi: tayyor sahifa (Checkout Session — Payme/Click kabi yo'naltirish) va Elements — karta maydonlari sizning sahifangizda, lekin har biri Stripe'ning <iframe> i ichida. Karta raqami sizning kodingizga umuman tegmaydi:

tsx
import { loadStripe } from "@stripe/stripe-js";
import {
  Elements,
  PaymentElement,
  useElements,
  useStripe,
} from "@stripe/react-stripe-js";

const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PK);

function PayForm() {
  const stripe = useStripe();
  const elements = useElements();
  async function pay(e: React.FormEvent) {
    e.preventDefault();
    if (!stripe || !elements) return;
    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: { return_url: `${location.origin}/qaytish` },
    });
    if (error) setMessage(error.message ?? "To'lov o'tmadi");
  }
  return (
    <form onSubmit={pay}>
      <PaymentElement />
      <button disabled={!stripe}>To'lash</button>
    </form>
  );
}

// clientSecret — serverdan (PaymentIntent), summa ham o'sha yerda
<Elements stripe={stripePromise} options={{ clientSecret }}>
  <PayForm />
</Elements>;

(Hujjat bo'yicha, docs.stripe.com — "Accept a payment"; biz Stripe hisobi ochmadik.)

  • VITE_STRIPE_PK — ochiq kalit (pk_…), brauzerda bo'lishi normal. Maxfiy sk_… — faqat serverda.
  • clientSecret — server yaratgan to'lov niyati (PaymentIntent). Summa unda, brauzerda emas.
  • CSP: script-src va frame-src ga https://js.stripe.com qo'shiladi — Stripe skripti o'z serveridan yuklanadi. Trusted Types bilan ishlashini kanon sinamagan.

Stripe faqat o'z ro'yxatidagi mamlakatlarda ro'yxatdan o'tgan biznes uchun ishlaydi. 2026-10 holatiga stripe.com/global ro'yxatida O'zbekiston yo'q: O'zbekistondagi kompaniya Stripe hisobini ocholmaydi. Shuning uchun mahalliy loyihalarda — Payme, Click va Uzum, Stripe esa xorijda ro'yxatdan o'tgan kompaniya uchun.

9. Hujumchi nigohi

1. Summani o'zgartirish. Hujumchi DevTools'da yashirin amount maydonini 3500000 dan 100 ga o'zgartiradi, yoki Payme base64'ini ochib a=100 yozadi, yoki o'z so'rovida { total: 1 } yuboradi. Himoya — uch qatlam, hammasi serverda:

  • buyurtmaga summa brauzerdan qabul qilinmaydi — server o'z narxlaridan hisoblaydi («Jonli: checkout sahifasi» dagi PRICES);
  • to'lov tizimi callback'ida summa buyurtma summasi bilan solishtiriladi — mos kelmasa, rad;
  • valyuta va birlik (tiyin/so'm) ham tekshiriladi.

2. Soxta "to'landi". Hujumchi to'lamasdan /tolov/qaytish?buyurtma=42&holat=tolandi ni ochadi. Frontend manzildan o'qisa — "To'landi!" ekrani. Himoya: holat faqat serverdan (polling). Server esa uni faqat to'lov tizimining imzolangan callback'idan oladi.

3. Soxta callback. Hujumchi to'g'ridan-to'g'ri sizning callback manzilingizga "42 to'landi" yuboradi. Himoya — serverda: Payme so'rovidagi Basic auth kalitini, Click'dagi sign_string imzosini tekshirish, kerak bo'lsa IP ro'yxati (Payme Merchant API, Click). Frontend bu yerda hech narsa qilolmaydi — shuning uchun "holat faqat serverdan" qoidasi.

4. Ochiq yo'naltirish. Qaytish manzili parametr bilan berilsa (?keyin=https://yomon.example), hujumchi o'z sahifasiga yo'naltiradi va "to'lov xatosi, kartani qayta kiriting" deb soxta forma ko'rsatadi. Himoya — 19/#07 dagi qoida: qaytish manzili faqat o'z origin'ingiz, server ro'yxatidan.

5. Karta ma'lumoti. O'z karta formangizni yasash — PCI DSS yuki va sizib chiqish xavfi. Karta faqat to'lov tizimining sahifasi yoki iframe'ida (To'lov xavfsizligi).

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Click 100 baravar ko'p so'radi tiyin yuborildi, Click so'm kutadi manzilni bitta joyda tuzing, birlik bilan nomlang
Payme tugmasi "jim" forma POST, form-action 'self' GET (base64) yoki CSP'ga https://checkout.paycom.uz
Mehmon "to'ladim" deydi, buyurtma "kutilmoqda" callback hali kelmagan, polling erta to'xtadi 1–2 daqiqa polling, «Noaniq» holati
Ikki buyurtma ikki marta bosish, qayta urinish disabled + idempotentlik kaliti
Qaytish sahifasi "To'landi" ni manzildan ko'rsatadi ?holat= ga ishonilgan faqat server holati
return_url ichidagi ?b=42 yo'qoldi qo'lda satr qo'shilgan URLSearchParams
Sahifadan ketgach ham so'rovlar polling cleanup'siz effektda clearTimeout va bayroq

11. Mashqlar

Mashqlar kurs/mashqlar/21/21-tolov-ui/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Payme checkout summasi qaysi birlikda:
  • Click amount 35 000 so'm uchun:
  • To'lovdan keyin mehmon qaytadigan manzil (inglizcha atama):
  • Holatni har necha soniyada serverdan so'rash:
  • Forma POST'ni tashqi manzilga to'sadigan CSP direktivasi:
Yechim

Payme — tiyinda (35 000 so'm = 3 500 000), Click — so'mda, ikki xona bilan. return_url (Payme'da c yoki callback) — faqat qaytish, to'lov dalili emas. Polling — callback kelguncha qayta-qayta so'rash. form-action formalarni cheklaydi, location.assign bilan o'tishni esa cheklamaydi.

2-mashq (o'rta): usul nomi va summa formati

«Jonli: checkout sahifasi» dagi kodda yakuniy matn doim «Click sahifasiga» deydi. Tuzating:

  • PROVIDERS obyekti: { payme: "Payme", click: "Click" } — radio yorliqlari ham, yakuniy matn ham undan.
  • formatSom ni tekshiring: 4500000 tiyin → «45 000 so'm», 100 → «1 so'm», 1250050 → «12 501 so'm» (yaxlitlash).

Sinov: Payme tanlangan holda (standart) «To'lashga o'tish» → «Jami 45 000 so'm — Payme sahifasiga o'tilmoqda».

Ishora: Object.entries(PROVIDERS).map(([id, name]) => …) (Object.entries).

Yechim
jsx
import { useState } from "react";

const PROVIDERS = { payme: "Payme", click: "Click" };
const PRICES = { osh: 3500000, choy: 500000 };

function createOrder(items) {
  return new Promise((resolve) =>
    setTimeout(() => {
      const tiyin = items.reduce(
        (sum, it) => sum + PRICES[it.id] * it.qty,
        0,
      );
      resolve({ orderId: 42, tiyin });
    }, 200),
  );
}

function formatSom(tiyin) {
  const som = String(Math.round(tiyin / 100));
  return som.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " so'm";
}

console.log([4500000, 100, 1250050].map(formatSom).join(" | "));

export default function App() {
  const [provider, setProvider] = useState("payme");
  const [order, setOrder] = useState(null);
  async function pay() {
    const items = [
      { id: "osh", qty: 1 },
      { id: "choy", qty: 2 },
    ];
    setOrder(await createOrder(items));
  }
  return (
    <section>
      {Object.entries(PROVIDERS).map(([id, name]) => (
        <label key={id}>
          <input
            type="radio"
            name="provider"
            checked={provider === id}
            onChange={() => setProvider(id)}
          />
          {name}{" "}
        </label>
      ))}
      <button onClick={pay}>To'lashga o'tish</button>
      {order && (
        <p role="status">
          Jami {formatSom(order.tiyin)} — {PROVIDERS[provider]}{" "}
          sahifasiga o'tilmoqda
        </p>
      )}
    </section>
  );
}

Konsolda:

text
45 000 so'm | 1 so'm | 12 501 so'm

Ekranda:

text
Payme Click To'lashga o'tish
Jami 45 000 so'm — Payme sahifasiga o'tilmoqda

1250050 tiyin = 12 500.50 so'm, Math.round → 12 501. Yaxlitlash qoidasini (yuqoriga, banknot qoidasi) biznes belgilaydi — Pul bilan ishlash da chuqur. Muhimi: yaxlitlash faqat ko'rsatishda, hisob tiyinda qoladi.

3-mashq (qiyin): bekor qilish va muddati o'tish

«Jonli: holatni so'rab turish» dagi usePaymentStatus ni kengaytiring:

  • Server javoblari: ["kutilmoqda", "bekor"] — ikkinchi so'rovda «To'lov bekor qilindi.» va «Qayta urinish» tugmasi chiqsin.
  • «Qayta urinish» polling'ni boshidan boshlasin (yangi attempt sanog'i). Ikkinchi aylanishda server faqat «kutilmoqda» qaytaradi — MAX_TRIES = 3 dan keyin «Noaniq» matni chiqsin.
  • Har so'rov konsolga aylanish N, so'rov M: holat.

Ishora: polling'ni qayta boshlash uchun effektning bog'liqligiga round state'ini qo'shing — round o'zgarsa, eski effekt tozalanadi va yangisi boshlanadi (dependency massivi).

Yechim
jsx
import { useEffect, useState } from "react";

const answers = ["kutilmoqda", "bekor"];
let calls = 0;
function fetchStatus() {
  return new Promise((resolve) =>
    setTimeout(() => resolve(answers[calls++] ?? "kutilmoqda"), 50),
  );
}

const INTERVAL = 100;
const MAX_TRIES = 3;

function usePaymentStatus(round) {
  const [status, setStatus] = useState("kutilmoqda");
  useEffect(() => {
    let stopped = false;
    let timer;
    setStatus("kutilmoqda");
    async function check(attempt) {
      const s = await fetchStatus();
      if (stopped) return;
      console.log(`aylanish ${round}, so'rov ${attempt}: ${s}`);
      if (s !== "kutilmoqda") return setStatus(s);
      if (attempt >= MAX_TRIES) return setStatus("noaniq");
      timer = setTimeout(() => check(attempt + 1), INTERVAL);
    }
    check(1);
    return () => {
      stopped = true;
      clearTimeout(timer);
    };
  }, [round]);
  return status;
}

const TEXT = {
  kutilmoqda: "To'lov tekshirilmoqda…",
  tolandi: "To'landi!",
  bekor: "To'lov bekor qilindi.",
  noaniq: "To'lov hali tasdiqlanmadi. Pul yechilgan bo'lsa, kuting.",
};

export default function App() {
  const [round, setRound] = useState(1);
  const status = usePaymentStatus(round);
  return (
    <>
      <p role="status">{TEXT[status]}</p>
      {status === "bekor" && (
        <button onClick={() => setRound((r) => r + 1)}>
          Qayta urinish
        </button>
      )}
    </>
  );
}

Konsolda:

text
aylanish 1, so'rov 1: kutilmoqda
aylanish 1, so'rov 2: bekor
aylanish 2, so'rov 1: kutilmoqda
aylanish 2, so'rov 2: kutilmoqda
aylanish 2, so'rov 3: kutilmoqda

Ekranda:

text
To'lov hali tasdiqlanmadi. Pul yechilgan bo'lsa, kuting.

round o'zgarishi bilan React eski effektning cleanup'ini chaqirdi (stopped = true), keyin yangisini boshladi. Haqiqiy ilovada «Qayta urinish» polling'ni emas, to'lovni qaytadan boshlaydi — yangi idempotentlik kaliti bilan, chunki eski urinish bekor qilingan.

4-mashq: tajriba qadami (ixtiyoriy)

vazifalar ga tegmang. Alohida papkada (kurs/tajriba/tolov/) bitta index.html yarating: <head> da vazifalar dagi CSP <meta> i, sahifada «Jonli: checkout sahifasi» va «CSP va form-action» dagi ikki tugma (Payme forma POST va Click location.assign). Manzillarda SINOV qoldiring — haqiqiy to'lov qilmang. Vite dev server bilan oching va DevTools konsolida har tugmadan keyin nima chiqqanini yozing.

Yechim
  • Payme (forma POST): konsolda CSP buzilishi — Sending form data to 'https://checkout.paycom.uz/' violates the following Content Security Policy directive: "form-action 'self'" ga o'xshash xabar; sahifa joyida qoladi.
  • Click (location.assign): brauzer my.click.uz ga o'tadi va Click sahifasi SINOV id'sini tanimaydi (xato sahifasi) — bu kutilgan, haqiqiy kassa id yo'q.
  • CSP'ga form-action 'self' https://checkout.paycom.uz qo'shsangiz, Payme forma ham o'tadi.

Kanon xom natijasi — «Biz sinab ko'rdik» bo'limida. Brauzer xabarining aniq matni versiyaga qarab farq qilishi mumkin.

12. Real ishda

  • O'zbekistondagi har onlayn do'kon va yetkazib berish xizmati Payme va Click'ni birga ulaydi, ko'pchiligi Uzum'ni ham. Frontend'chining ulushi — savatcha, usul tanlash, qaytish sahifasi va xato holatlari; backend'chiniki — Merchant API va callback'lar.
  • To'lov sahifasi — eng ko'p sinaladigan joy: QA test kartalar bilan "bekor", "rad", "muddati o'tdi", "sahifani yopib qaytish" ni bir necha bor o'tkazadi. Har holat uchun tushunarli matn bo'lmasa — reliz to'xtaydi.
  • Analitika va flag bilan birga: "to'lashga o'tdi" va "to'landi" hodisalari — konversiya voronkasi (analitika); yangi to'lov tizimi — avval flag bilan 10% ga (feature flag).
  • Intervyu savollari: "Nega summani frontend'dan qabul qilmaysiz?", "Return URL to'lov dalilimi?", "Ikki marta bosishdan qanday himoyalanasiz?", "Payme va Click summasi qaysi birlikda?", "CSP to'lovga qanday xalaqit berishi mumkin?".

Xulosa

  • Frontend pulni olmaydi: buyurtma (summasiz) → serverdan checkout manzili → to'lov tizimi sahifasi → qaytish → serverdan holat.
  • Payme: checkout.paycom.uz + base64 (m, ac.order_id, a tiyinda, l, c) yoki forma POST; Click: my.click.uz/services/pay (amount so'mda, transaction_param, return_url); Uzum va boshqalar — server API'dan tayyor manzil.
  • Qaytish sahifasi — polling: setTimeout zanjiri, cleanup, urinishlar chegarasi va «Noaniq» holati; manzildagi ?holat= ga ishonilmaydi.
  • Ikki marta bosish — disabled + idempotentlik kaliti; callback'lar ham takrorlanadi.
  • CSP: forma POST tashqi checkout'ga form-action 'self' da to'siladi, GET yo'naltirish o'tadi (kanon sinadi); Stripe Elements — iframe, js.stripe.com.
  • Summa va holat — faqat serverdan; haqiqiy to'lov va callback — 29-qism.

Keyingi dars: Telegram Mini Apps I: asoslar — «Bahor» menyusi Telegram ichida ochiladi: tema ranglari, asosiy tugma, orqaga tugmasi va foydalanuvchi ma'lumoti — SDK'ni soxta muhit bilan sinaymiz.

Manbalar

  • Payme Business: "Checkout — GET va POST orqali chek yuborish" (parametrlar m, ac.*, a, l, c) — developer.help.paycom.uz
  • Click: "Payment button installation instructions" (service_id, merchant_id, amount N.NN, transaction_param, return_url, card_type) — docs.click.uz
  • Stripe: qo'llab-quvvatlanadigan mamlakatlar ro'yxati — stripe.com/global (2026-10-18 da tekshirildi)
  • Stripe: "Accept a payment" (Payment Element, confirmPayment), "Idempotent requests" — docs.stripe.com
  • MDN: CSP form-action, Location.assign(), btoa() / atob(), URLSearchParams — developer.mozilla.org
  • PCI Security Standards Council: "PCI DSS" (SAQ A — to'lov sahifasi tashqarida) — pcisecuritystandards.org
  • Kanon: tajriba/tolov/ (index.html, sahifa.js, sinov.mjs), natijalar/tajriba/21-tolov-csp.txt (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
To'lov UI React'da: Click, Payme va Uzum — checkout, qaytish sahifasi va holat — IlmHamroh