mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(topup): direct-open payment page when method.open_url_direct is set
User asked for the gift-style seamless flow on balance top-up: provider checkout opens inside Telegram MiniApp WebView without a click-to-open link panel. Made it an admin per-method toggle so it can be enabled selectively. src/pages/TopUpAmount.tsx — on topup mutation success: * Move saveTopUpPendingInfo to BEFORE any redirect so /balance/top-up/result can still pick up the pending payment after the provider's return_url fires * If method?.open_url_direct === true AND the URL is not a t.me/ deep link (Stars/CryptoBot), window.location.href = redirectUrl and return early * Otherwise fall through to the existing setPaymentUrl panel — preserves current behavior for methods without the flag enabled The t.me/ guard is important: window.location.href to a Telegram deep link inside a MiniApp WebView is unreliable (native shell cant always intercept). Those URLs continue to go through openTelegramLink / openInvoice in the panel path. Stars never reaches topUpMutation.onSuccess anyway (handled by the separate starsPaymentMutation); the guard is defense-in-depth for CryptoBot and any future t.me-deep-link providers. src/pages/AdminPaymentMethodEdit.tsx — added Open URL directly toggle with the same slider styling as the is_enabled toggle. Defaults to off. src/types/index.ts * PaymentMethod.open_url_direct?: boolean (user-facing) * PaymentMethodConfig.open_url_direct: boolean (admin shape) Translations added for ru/en/zh/fa with a hint clarifying behavior and the t.me/ exemption.
This commit is contained in:
@@ -1515,6 +1515,8 @@
|
|||||||
"noMethods": "No payment methods configured",
|
"noMethods": "No payment methods configured",
|
||||||
"methodEnabled": "Method enabled",
|
"methodEnabled": "Method enabled",
|
||||||
"providerNotConfigured": "Provider not configured in env",
|
"providerNotConfigured": "Provider not configured in env",
|
||||||
|
"openUrlDirect": "Open payment page directly",
|
||||||
|
"openUrlDirectHint": "Skip the link panel — the provider opens inside the MiniApp/tab right after the click. After payment the user returns to /balance/top-up/result. Ignored for Stars/CryptoBot and other t.me/ links.",
|
||||||
"displayName": "Display name",
|
"displayName": "Display name",
|
||||||
"displayNameHint": "Leave empty for default name",
|
"displayNameHint": "Leave empty for default name",
|
||||||
"subOptions": "Payment options",
|
"subOptions": "Payment options",
|
||||||
|
|||||||
@@ -2082,6 +2082,8 @@
|
|||||||
"noMethods": "هیچ روش پرداخت پیکربندیشدهای نیست",
|
"noMethods": "هیچ روش پرداخت پیکربندیشدهای نیست",
|
||||||
"methodEnabled": "روش فعال است",
|
"methodEnabled": "روش فعال است",
|
||||||
"providerNotConfigured": "ارائهدهنده در env تنظیم نشده",
|
"providerNotConfigured": "ارائهدهنده در env تنظیم نشده",
|
||||||
|
"openUrlDirect": "صفحه پرداخت را مستقیماً باز کن",
|
||||||
|
"openUrlDirectHint": "بدون پنل لینک — ارائهدهنده داخل MiniApp/تب بلافاصله پس از کلیک باز میشود. پس از پرداخت کاربر به /balance/top-up/result بازمیگردد. برای Stars/CryptoBot و سایر لینکهای t.me/ این پرچم نادیده گرفته میشود.",
|
||||||
"displayName": "نام نمایشی",
|
"displayName": "نام نمایشی",
|
||||||
"displayNameHint": "برای نام پیشفرض خالی بگذارید",
|
"displayNameHint": "برای نام پیشفرض خالی بگذارید",
|
||||||
"subOptions": "گزینههای پرداخت",
|
"subOptions": "گزینههای پرداخت",
|
||||||
|
|||||||
@@ -1537,6 +1537,8 @@
|
|||||||
"noMethods": "Нет настроенных платёжных методов",
|
"noMethods": "Нет настроенных платёжных методов",
|
||||||
"methodEnabled": "Метод включён",
|
"methodEnabled": "Метод включён",
|
||||||
"providerNotConfigured": "Провайдер не настроен в env",
|
"providerNotConfigured": "Провайдер не настроен в env",
|
||||||
|
"openUrlDirect": "Открывать страницу оплаты сразу",
|
||||||
|
"openUrlDirectHint": "Без панели со ссылкой — провайдер открывается внутри MiniApp/вкладки сразу после клика. После оплаты юзер возвращается на /balance/top-up/result. Для Stars/CryptoBot и других t.me/-ссылок флаг игнорируется.",
|
||||||
"displayName": "Отображаемое имя",
|
"displayName": "Отображаемое имя",
|
||||||
"displayNameHint": "Оставьте пустым для имени по умолчанию",
|
"displayNameHint": "Оставьте пустым для имени по умолчанию",
|
||||||
"subOptions": "Варианты оплаты",
|
"subOptions": "Варианты оплаты",
|
||||||
|
|||||||
@@ -1280,6 +1280,8 @@
|
|||||||
"noMethods": "没有配置的支付方法",
|
"noMethods": "没有配置的支付方法",
|
||||||
"methodEnabled": "方法已启用",
|
"methodEnabled": "方法已启用",
|
||||||
"providerNotConfigured": "提供商未在env中配置",
|
"providerNotConfigured": "提供商未在env中配置",
|
||||||
|
"openUrlDirect": "直接打开支付页面",
|
||||||
|
"openUrlDirectHint": "跳过链接面板 — 点击后提供商在 MiniApp/标签页内直接打开。支付完成后用户返回 /balance/top-up/result。Stars/CryptoBot 和其他 t.me/ 链接忽略此标志。",
|
||||||
"displayName": "显示名称",
|
"displayName": "显示名称",
|
||||||
"displayNameHint": "留空以使用默认名称",
|
"displayNameHint": "留空以使用默认名称",
|
||||||
"subOptions": "支付选项",
|
"subOptions": "支付选项",
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export default function AdminPaymentMethodEdit() {
|
|||||||
const [firstTopupFilter, setFirstTopupFilter] = useState<'any' | 'yes' | 'no'>('any');
|
const [firstTopupFilter, setFirstTopupFilter] = useState<'any' | 'yes' | 'no'>('any');
|
||||||
const [promoGroupFilterMode, setPromoGroupFilterMode] = useState<'all' | 'selected'>('all');
|
const [promoGroupFilterMode, setPromoGroupFilterMode] = useState<'all' | 'selected'>('all');
|
||||||
const [selectedPromoGroupIds, setSelectedPromoGroupIds] = useState<number[]>([]);
|
const [selectedPromoGroupIds, setSelectedPromoGroupIds] = useState<number[]>([]);
|
||||||
|
const [openUrlDirect, setOpenUrlDirect] = useState(false);
|
||||||
|
|
||||||
// Initialize state when config loads
|
// Initialize state when config loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,6 +80,7 @@ export default function AdminPaymentMethodEdit() {
|
|||||||
setFirstTopupFilter(config.first_topup_filter);
|
setFirstTopupFilter(config.first_topup_filter);
|
||||||
setPromoGroupFilterMode(config.promo_group_filter_mode);
|
setPromoGroupFilterMode(config.promo_group_filter_mode);
|
||||||
setSelectedPromoGroupIds(config.allowed_promo_group_ids);
|
setSelectedPromoGroupIds(config.allowed_promo_group_ids);
|
||||||
|
setOpenUrlDirect(config.open_url_direct);
|
||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ export default function AdminPaymentMethodEdit() {
|
|||||||
first_topup_filter: firstTopupFilter,
|
first_topup_filter: firstTopupFilter,
|
||||||
promo_group_filter_mode: promoGroupFilterMode,
|
promo_group_filter_mode: promoGroupFilterMode,
|
||||||
allowed_promo_group_ids: promoGroupFilterMode === 'selected' ? selectedPromoGroupIds : [],
|
allowed_promo_group_ids: promoGroupFilterMode === 'selected' ? selectedPromoGroupIds : [],
|
||||||
|
open_url_direct: openUrlDirect,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Display name
|
// Display name
|
||||||
@@ -215,6 +218,34 @@ export default function AdminPaymentMethodEdit() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Open URL directly toggle */}
|
||||||
|
<div className="flex items-center justify-between border-t border-dark-800 pt-6">
|
||||||
|
<div className="pr-4">
|
||||||
|
<div className="text-sm font-medium text-dark-200">
|
||||||
|
{t('admin.paymentMethods.openUrlDirect', 'Открывать страницу оплаты сразу')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 text-xs text-dark-500">
|
||||||
|
{t(
|
||||||
|
'admin.paymentMethods.openUrlDirectHint',
|
||||||
|
'Без панели со ссылкой — провайдер открывается внутри MiniApp/вкладки сразу после клика. После оплаты юзер возвращается на /balance/result.',
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenUrlDirect(!openUrlDirect)}
|
||||||
|
className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${
|
||||||
|
openUrlDirect ? 'bg-accent-500' : 'bg-dark-600'
|
||||||
|
}`}
|
||||||
|
aria-label="Toggle open URL directly"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-1 h-4 w-4 rounded-full bg-white transition-transform ${
|
||||||
|
openUrlDirect ? 'left-6' : 'left-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Display name */}
|
{/* Display name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-dark-300">
|
<label className="mb-2 block text-sm font-medium text-dark-300">
|
||||||
|
|||||||
@@ -255,9 +255,8 @@ export default function TopUpAmount() {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const redirectUrl = data.payment_url || data.invoice_url;
|
const redirectUrl = data.payment_url || data.invoice_url;
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
setPaymentUrl(redirectUrl);
|
// Save payment info for the result page (do BEFORE possible redirect,
|
||||||
|
// иначе после window.location.href этот код не выполнится).
|
||||||
// Save payment info for the result page
|
|
||||||
if (method && data.payment_id) {
|
if (method && data.payment_id) {
|
||||||
const methodKey = method.id.toLowerCase().replace(/-/g, '_');
|
const methodKey = method.id.toLowerCase().replace(/-/g, '_');
|
||||||
const displayName =
|
const displayName =
|
||||||
@@ -270,6 +269,22 @@ export default function TopUpAmount() {
|
|||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// open_url_direct: seamless флоу как при покупке подарка.
|
||||||
|
// window.location.href внутри Telegram MiniApp WebView навигирует
|
||||||
|
// в том же контейнере без открытия внешнего браузера. После
|
||||||
|
// оплаты return_url возвращает на /balance/top-up/result.
|
||||||
|
//
|
||||||
|
// t.me/ URL (Telegram Stars, CryptoBot) — всегда через нативный
|
||||||
|
// handler (openInvoice / openTelegramLink в setPaymentUrl-ветке).
|
||||||
|
// Stars уже отбит раньше через starsPaymentMutation, здесь — защита
|
||||||
|
// на случай CryptoBot и других Telegram-deep-link провайдеров.
|
||||||
|
if (method?.open_url_direct && !redirectUrl.startsWith('https://t.me/')) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPaymentUrl(redirectUrl);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err: unknown) => {
|
onError: (err: unknown) => {
|
||||||
|
|||||||
@@ -442,6 +442,9 @@ export interface PaymentMethod {
|
|||||||
max_amount_kopeks: number;
|
max_amount_kopeks: number;
|
||||||
is_available: boolean;
|
is_available: boolean;
|
||||||
options?: PaymentMethodOption[] | null;
|
options?: PaymentMethodOption[] | null;
|
||||||
|
// Если true — после получения payment_url кабинет сразу делает
|
||||||
|
// window.location.href вместо показа панели с кнопкой "Открыть".
|
||||||
|
open_url_direct?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Referral types
|
// Referral types
|
||||||
@@ -682,6 +685,7 @@ export interface PaymentMethodConfig {
|
|||||||
first_topup_filter: 'any' | 'yes' | 'no';
|
first_topup_filter: 'any' | 'yes' | 'no';
|
||||||
promo_group_filter_mode: 'all' | 'selected';
|
promo_group_filter_mode: 'all' | 'selected';
|
||||||
allowed_promo_group_ids: number[];
|
allowed_promo_group_ids: number[];
|
||||||
|
open_url_direct: boolean;
|
||||||
is_provider_configured: boolean;
|
is_provider_configured: boolean;
|
||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user