Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 2. Umumiy oqim
- 2.1 Kim nima qiladi
- 2.2 Frontend'ning to'rt ishi
- 3. Checkout manzili
- 3.1 Payme: base64 bilan GET
- 3.2 Payme: forma POST
- 3.3 Click: oddiy GET
- 3.4 Uzum va boshqalar
- 4. Jonli: checkout sahifasi
- 5. Qaytish sahifasi va polling
- 5.1 Qaytdi — lekin to'ladimi?
- 5.2 Jonli: holatni so'rab turish
- 6. Ikki marta bosish va idempotentlik
- 6.1 Muammo
- 6.2 Jonli: kalit bilan
- 7. Biz sinab ko'rdik: CSP va form-action
- 7.1 Tajriba
- 7.2 Nima bo'ldi
- 7.3 Nega vazifalar da to'lov yo'q
- 8. Test rejimi va Stripe
- 8.1 Sinov muhiti
- 8.2 Stripe Elements (xalqaro)
- 9. Hujumchi nigohi
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): usul nomi va summa formati
- 3-mashq (qiyin): bekor qilish va muddati o'tish
- 4-mashq: tajriba qadami (ixtiyoriy)
- 12. Real ishda
- Xulosa
- Manbalar
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
amountni har kim o'zgartiradi,?status=paidni har kim yozadi.vazifalarCSP'si ostida sinaldi: Payme uslubidagi forma POST'niform-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-actionva 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
- Savatcha va buyurtma: tanlangan taomlar serverga ketadi — summasiz. Summani server o'z narxlari bo'yicha hisoblaydi.
- To'lov usuli va yo'naltirish: mehmon Payme yoki Click'ni tanlaydi, server manzil qaytaradi, brauzer o'tadi.
- Qaytish sahifasi: holatni serverdan so'rab turadi va natijani ko'rsatadi.
- 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:
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:
checkout.paycom.uz
m=SINOV;ac.order_id=42;a=3500000;l=uz;c=https://example.com/q?b=42btoa — 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:
<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:
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:
service_id = SINOV
merchant_id = SINOV
amount = 35000.00
transaction_param = 42
return_url = https://example.com/q?b=42URLSearchParams 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: 3500000yuborildi (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):
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:
yo'naltirish: https://click.example/pay?order=42Ekranda:
Osh × 1
Ko'k choy × 2
To'lov usuli
Payme Click
To'lashga o'tish
Jami 45 000 so'm — Click sahifasiga o'tilmoqdaQatorma-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 disabledichidagi hamma maydonni birdan o'chiradi.role="status"— Malikaning ekran o'quvchisi "Buyurtma yaratilmoqda" ni o'zi o'qiydi (accessibility).formatSom—Intl.NumberFormatemas: natija oynasidagi Chromeuzuchun35,000chiqaradi (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):
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:
1-so'rov: kutilmoqda
2-so'rov: kutilmoqda
3-so'rov: tolandiEkranda:
To'landi! Buyurtma oshxonaga yuborildi.- Keyingi so'rov — oldingisi tugagandan keyin (
setTimeoutzanjiri,setIntervalemas). Server sekin javob bersa, so'rovlar ustma-ust tushmaydi. stoppedvaclearTimeout— effekt cleanup: mehmon sahifadan ketsa, polling to'xtaydi. Aks holda yopilgan komponentsetStatusni 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=tolandibo'lsa,holatni 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:
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:
server: yangi buyurtma 42
server: takror — 42
server: yangi buyurtma 43Ikkinchi 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:
<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):
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-actionni 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.uzga yo'l oldi.location.assign— forma emas, oddiy sahifa o'tishi, CSP esa sahifa o'tishlarini cheklamaydi.
Ikki yechim:
- Payme'ni ham GET (base64) bilan ishlatish — CSP o'zgarmaydi.
- 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:
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. Maxfiysk_…— faqat serverda.clientSecret— server yaratgan to'lov niyati (PaymentIntent). Summa unda, brauzerda emas.- CSP:
script-srcvaframe-srcgahttps://js.stripe.comqo'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
amount35 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:
PROVIDERSobyekti:{ payme: "Payme", click: "Click" }— radio yorliqlari ham, yakuniy matn ham undan.formatSomni tekshiring:4500000tiyin → «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
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:
45 000 so'm | 1 so'm | 12 501 so'mEkranda:
Payme Click To'lashga o'tish
Jami 45 000 so'm — Payme sahifasiga o'tilmoqda1250050 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
attemptsanog'i). Ikkinchi aylanishda server faqat «kutilmoqda» qaytaradi —MAX_TRIES = 3dan 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
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:
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: kutilmoqdaEkranda:
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): brauzermy.click.uzga o'tadi va Click sahifasiSINOVid'sini tanimaydi (xato sahifasi) — bu kutilgan, haqiqiy kassa id yo'q. - CSP'ga
form-action 'self' https://checkout.paycom.uzqo'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,atiyinda,l,c) yoki forma POST; Click:my.click.uz/services/pay(amountso'mda,transaction_param,return_url); Uzum va boshqalar — server API'dan tayyor manzil. - Qaytish sahifasi — polling:
setTimeoutzanjiri, 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,amountN.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)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!