ویجت
سه راه برای گرفتن پرداخت — هدایت میزبانیشده، پنجره بازشو، و کارت درونصفحهای — و اینکه چرا تأیید همیشه روی دامنه کوینلند انجام میشود
ویجت جایی است که مشتری واقعاً پرداخت میکند. تأیید پرداخت همیشه روی دامنه خودِ کوینلند و در نشانی
https://my.coinlandexchange.com/pay/{sessionId} انجام میشود؛ آنچه شما انتخاب میکنید این است که مشتری
چطور به آنجا برسد و چه مقدار از سفارش پیش از آن روی صفحه شما دیده شود.
هر سه از جلسه پرداختی شروع میشوند که از قبل روی سرور ساختهاید. ویجت خودش هرگز کلید API شما را نمیگیرد و چیزی برای تنظیم در آن نیست — برندینگ، عنوان و قیمتهای شما همراه جلسه پرداخت میآیند.

این تصویر حالت واردشده را نشان میدهد. مشتریای که به کوینلند وارد نشده باشد همین سربرگ فروشنده و خلاصه سفارش را میبیند با یک فرم ورود در زیر آن، و بعد از احراز هویت به همین صفحه میرسد. نام فروشنده، شماره سفارش و شماره رسید در این تصویرها داده نمونهاند؛ چیدمان، قلم و رفتار همان صفحه واقعی است. عنوان و توضیح سفارش، رشتههای خودِ فروشندهاند، پس در هر دو زبان دقیقاً همانطور نمایش داده میشوند که فروشنده نوشته است.
ویجت هیچ محاسبه پولیای انجام نمیدهد. هر عددی که روی آن میبینید فیلدی است که به آن داده شده و در لحظه ساخت جلسه تثبیت شده، و مبلغ روی دکمه پرداخت دقیقاً همان عددی است که دفتر حساب کسر میکند. هیچ محاسبهای در سمت مرورگر وجود ندارد که بتواند با مبلغ کسرشده اختلاف پیدا کند، و هیچ چیزی روی صفحه نمیتواند بین آنچه مشتری میخواند و آنچه میپردازد فاصله بیندازد.
آنچه مشتری میبیند به این بستگی دارد که کارمزد را چه کسی میپردازد. وقتی fee_bearer برابر
"merchant" باشد او یک عدد میبیند — مبلغ سفارش — چون کارمزد سهم شماست و چیزی به مبلغ او اضافه
نمیشود. وقتی برابر "customer" باشد، آن مبلغ اضافه بهعنوان یک سطر جداگانه نشان داده میشود، چون از
او خواسته میشود آن را بپردازد. نرخ پلتفرمِ شما در هیچکدام از این دو حالت روی این صفحه نیست.

سه سطح یکپارچهسازی
| سطح | چه مینویسید | مشتری چه میبیند | کِی مناسب است |
|---|---|---|---|
| ۱. هدایت میزبانیشده | یک ۳۰۲ به checkout_url | صفحه شما، بعد صفحه کوینلند | کمترین کد را میخواهید، یا اصلاً جاواسکریپت اجرا نمیکنید |
| ۲. پنجره بازشو | CoinlandPay.open() | صفحه شما، با کوینلند روی آن | میخواهید مشتری روی صفحه شما بماند |
| ۳. کارت درونصفحهای | CoinlandPay.mount() | کارت سفارش داخل صفحه شما | میخواهید سفارش در صفحه پرداخت خودتان دیده شود |
هر سه یک اسکریپت، یک جلسه پرداخت و یک خروجی یکسان دارند، پس جابهجایی بینشان چند خط است. از سطح ۱ شروع کنید و فقط وقتی بالاتر بروید که چیزی را که سطح بعدی اضافه میکند بخواهید.
سطح ۱: هدایت میزبانیشده
بدون هیچ جاواسکریپتی. پاسخ ساخت جلسه پرداخت، checkout_url را دارد؛ یک ۳۰۲ به آن بفرستید.
import type { Request, Response } from "express";
const API = "https://my.coinlandexchange.com";
export async function startCheckout(req: Request, res: Response) {
const order = await loadOrder(req.params.orderId);
const session = await fetch(`${API}/api/pay/v1/sessions`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.COINLAND_PAY_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
reference_id: order.id,
title: `Order ${order.number}`,
amounts: [{ currency: "usdt", amount: order.totalUsdt }],
return_url: "https://example.com/checkout/done",
cancel_url: "https://example.com/cart",
}),
}).then((r) => r.json());
// Store the session id against the order BEFORE sending the customer away.
await order.update({ paySessionId: session.id });
res.redirect(302, session.checkout_url);
}مشتری پرداخت میکند، با ?receipt=<token>&payment_id=<id> به return_url شما برمیگردد، و هندلر
وبهوک شما تحویل را انجام میدهد. با انتخاب این روش بهجای دو روش دیگر، تنها چیزی که از دست میرود ماندن
مشتری روی صفحه شماست.
اسکریپت جایگذاری
سطح ۲ و ۳ به یک تگ اسکریپت نیاز دارند، بدون مرحله ساخت و بدون باندل:
<script src="https://my.coinlandexchange.com/pay/v1.js"></script>این اسکریپت تنها یک شیء جهانی به نام CoinlandPay تعریف میکند و هیچ چیز دیگری را بار نمیکند. آن را با
defer در <head> یا در پایان <body> قرار دهید.
سطح ۲: پنجره بازشو
CoinlandPay.open({
sessionId: "3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60",
onComplete({ payment_id, receipt_no, receipt }) {
// مشتری پرداخت کرد. رابط کاربری خودتان را جلو ببرید و بعد سمت سرور
// مغایرتگیری کنید.
window.location.href = `/checkout/done?payment_id=${payment_id}`;
},
onCancel() {
// پنجره بازشو بسته شد. جلسه پرداخت دستنخورده است و تا زمان انقضا باز
// میماند، پس فراخوانی دوباره open() با همان sessionId آن را از سر میگیرد.
},
});نام
نوع
open() بلافاصله برمیگردد. هیچ Promiseای با پرداخت resolve نمیشود، چون پنجره بازشو میتواند از صفحهای
که آن را باز کرده عمر بیشتری داشته باشد — مشتریای که پرداخت را بعد از بسته شدن تب شما تمام میکند، هم
پرداختش انجام میشود و هم وبهوک به شما میرسد.
چون open() وقتی پنجره بازشو مسدود باشد ممکن است صفحه را ترک کند (پایینتر را ببینید)، آن را آخرین کار
هندلرتان بگذارید. کاری را که باید حتماً اجرا شود بعد از آن در صف نگذارید.
سطح ۳: کارت درونصفحهای
mount() کارت سفارش را داخل صفحه شما رندر میکند: مشتری برندینگ، مبالغ، گزینههای ارز و شمارش معکوس را
بدون ترک صفحه پرداخت شما میبیند. این کارت یک iframe به کوینلند است و فقط نمایشی است — فشردن دکمه پرداخت
از قاب بیرون میزند و صفحه میزبانیشده را در یک پنجره بازشو باز میکند، دقیقاً مثل سطح ۲.

<div id="coinland-pay"></div>
<script src="https://my.coinlandexchange.com/pay/v1.js"></script>
<script>
CoinlandPay.mount(document.getElementById("coinland-pay"), {
sessionId: "3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60",
onComplete({ payment_id, receipt_no, receipt }) {
window.location.href = `/checkout/done?payment_id=${payment_id}`;
},
onCancel() {
// مشتری پنجره پرداخت را بدون پرداخت بست. کارت سر جایش میماند و جلسه
// پرداخت باز میمانَد.
},
});
</script>نام
نوع
به ظرف یک عرض بدهید و بگذارید کارت آن را پر کند؛ تا عرض گوشی واکنشگرا است. اگر جلسه پرداخت در لحظه
mount شدن از قبل completed، expired یا canceled باشد، کارت بهجای دکمه پرداخت همان وضعیت را نشان
میدهد.
ارزش دارد رابطه سه سطح را صریح بگوییم: سطح ۳ شامل سطح ۲ است. کارت درونصفحهای یک سطح نمایشی است، و
همان لحظه که مشتری تصمیم میگیرد، همان صفحه میزبانیشدهای را باز میکند که open() باز میکرد. اگر پنجره
بازشو مسدود شود، هر دو سطح به همان هدایت کامل صفحه برمیگردند و مشتری با
?receipt=<token>&payment_id=<id> به return_url شما بازمیگردد.
return_url شما باید در هر سه سطح کار کند
حتی اگر قصدتان فقط استفاده از پنجره بازشو یا کارت درونصفحهای باشد، مشتریای که پنجرههای بازشو را
غیرفعال کرده بهجای فعال شدن onComplete روی return_url فرود میآید. آن صفحه را طوری بسازید که
payment_id را بخواند، وضعیت سفارش را از سرور خودتان بپرسد، و اگر وبهوک هنوز نرسیده حالت «در انتظار»
نشان دهد.
چرا پرداخت هرگز بهطور کامل داخل صفحه شما انجام نمیشود
کارت درونصفحهای در قاب قرار میگیرد؛ مرحله تأیید هرگز. صفحه پرداخت به نشانی
https://my.coinlandexchange.com/pay/{sessionId} هدر X-Frame-Options: DENY و frame-ancestors 'none' میفرستد، پس مرورگر از نمایش آن داخل سایت شما خودداری میکند. تنها مسیر نمایشیِ کارت که
mount() بار میکند در قاب قرار میگیرد. سه دلیل مستقل، که هر کدام بهتنهایی کافی است:
- مشتری نمیتواند یک صفحه ورود داخل قاب را راستیآزمایی کند. نوار نشانی دامنه شما را نشان میدهد در
حالی که فرم، اطلاعات ورود کوینلند و یک کد یکبارمصرف میخواهد. هیچ راهی نیست که آن قاب را از قابی که
خودتان کشیده باشید تشخیص دهد، و این دقیقاً شکل یک صفحه فیشینگ است — و مشتری را عادت میدهد اطلاعات
ورود کوینلند را در محیطی غیر از کوینلند تایپ کند. پنجره بازشو در تمام مدت احراز هویت نشانی
my.coinlandexchange.comرا در نوار نشانی نگه میدارد، و این تنها نشانهای است که واقعاً از او محافظت میکند. - صفحه میزبان میتواند یک قاب را طوری زیر نظر بگیرد که یک پنجره بازشو را نمیتواند. فوکوس، زمانبندی ضربههای کلید و چیدمان از سمت میزبان قابل مشاهدهاند، و گذاشتن یک لایه روی قاب همان حمله کلاسیک clickjacking است: مشتری فکر میکند روی یک چیز کلیک میکند و چیز دیگری را تأیید میکند.
- بههرحال کار نمیکرد. مرورگرها اکنون کوکیهای شخص ثالث را بر اساس سایت میزبان تفکیک میکنند، پس یک نشست کوینلند داخل صفحه شما همان نشستی نیست که مشتری از قبل دارد. از او خواسته میشد هر بار دوباره وارد شود، آن هم در بیاعتمادترین جای ممکن.
کارت درونصفحهای دقیقاً به این دلیل امن است که نمیتواند پولی جابهجا کند. جلسهای را نمایش میدهد که برای هر کسی که شناسهاش را دارد از قبل قابل مشاهده است، هیچ اطلاعات محرمانهای نمیگیرد، و هیچ اختیاری برای تأیید چیزی ندارد. هر کاری که نیاز دارد مشتری هویتش را اثبات کند روی دامنه کوینلند انجام میشود، در پنجرهای که نشانیاش را میتواند بخواند.
مدیریت نتیجه
از هر سطحی که استفاده کنید، نتیجه به یک شکل میرسد.
onComplete یک کالبک رابط کاربری است، نه سیگنال تسویه
این تابع در مرورگر مشتری اجرا میشود و هر کسی با یک کنسول باز میتواند آن را فعال کند. از آن برای جلو
بردن رابط کاربری خودتان استفاده کنید. تحویل سفارش را از وبهوک یا
GET /payments/{id} انجام دهید، که تنها دو چیزی هستند که مشتری نمیتواند در آنها دست ببرد.
شناسهها میتوانند خالی باشند، پس هرگز مستقیم نمایششان ندهید
یک مسیر بازگشتِ کند وجود دارد — مشتری پنجره پرداخت را پیش از آنکه گزارش بدهد بسته است — که در آن
onComplete با payment_id، receipt_no و receipt همه بهصورت رشته خالی صدا زده میشود و
تنها چیزی که معلوم است انجام شدن پرداخت است. هندلری که receipt_no را مستقیم روی صفحه میگذارد، جای
شماره رسید را خالی نشان میدهد و هیچ خطایی هم بالا نمیآید تا خبرتان کند.
پس کالبک را اینطور بگیرید: «چیزی تمام شد، برو بپرس» — پرداخت را با reference_id خودتان و از سمت
سرور با GET /api/pay/v1/sessions/{reference_id} بخوانید و از روی آن نمایش دهید. این همان انضباطی
است که کالبک از قبل برای تسویه میخواست؛ فقط برای فیلدها هم صادق است.
در پشت صحنه، پنجره بازشو با postMessage به window.opener گزارش میدهد و کارت درونصفحهای همان پیام را
از قاب خودش بازپخش میکند. بدنه پیام { source: "coinland-pay", type, sessionId, ... } است، که در آن
type یکی از payment_completed یا checkout_canceled است. اگر خودتان پیامها را مدیریت میکنید — که
نیازی به آن نباید داشته باشید — تنها قاعدهای که واقعاً اهمیت دارد بررسی مبدأ است:
window.addEventListener("message", (event) => {
// هرگز از این صرفنظر نکنید. بدون آن، هر صفحهای در هر تبی میتواند یک
// "payment_completed" جعلی برای شما بفرستد و رابط کاربری شما را پیش ببرد.
if (event.origin !== "https://my.coinlandexchange.com") return;
if (event.data?.source !== "coinland-pay") return;
// ...
});اسکریپت همچنین بررسی میکند که پنجره پرداخت بسته شده یا نه. مشتریای که آن را بدون پرداخت میبندد هیچ
پیامی تولید نمیکند، پس همین بررسی دورهای بستهشدن پنجره است که آن را به onCancel تبدیل میکند.
ظاهر و برندینگ
نمیتوانید ظاهر ویجت را تغییر دهید، چون نمیتوانید به درونش دست ببرید. آنچه میتوانید عوض کنید در کنسول
کسبوکار شماست: نام نمایشی، نام نمایشی فارسی و لوگو، که همه بالای عنوان سفارش نمایش داده میشوند. title
و description جلسه پرداخت هم برای هر سفارش در اختیار خودتان است.
ویجت بهصورت پیشفرض فارسی و راستبهچپ است و زبان دلخواه خودِ مشتری در کوینلند را دنبال میکند، نه زبان صفحه شما.
فهرست بررسی
- تگ اسکریپت به
https://my.coinlandexchange.com/pay/v1.jsاشاره میکند، نه به نسخهای که خودتان میزبانی کردهاید. return_urlوcancel_urlهر دو https هستند و با بازدید مستقیم هم کار میکنند.- صفحه
return_urlشما تحمل میکند که پیش از رسیدن وبهوک باز شود. - هیچ چیزی در
onCompleteبهتنهایی دسترسی نمیدهد. - هرگز تلاش نمیکنید خودِ صفحه پرداخت را در iframe بگذارید — تنها
mount()چیزی را در قاب میگذارد، و آنچه در قاب میگذارد کارت نمایشی است.