fix(payment-methods): admin display_name/description override locale on balance

White-label (per-method display_name + description in admin) was defeated on
balance/top-up pages: render used `translatedName || method.name` and
`translatedDesc || method.description`, so the bundled locale always won and
the admin override was ignored.

- Flip priority: admin-configured value first, locale as fallback
  (TopUpMethodSelect, Balance)
- Add missing Description field to payment-method edit page
  (mirrors Display name: description / reset_description)
- Add description to admin PaymentMethodConfig type
- Add description/descriptionHint i18n keys (ru, en)
This commit is contained in:
smediainfo
2026-07-16 09:55:08 +03:00
parent d7ea3c0635
commit 112e09bc6d
6 changed files with 36 additions and 6 deletions

View File

@@ -1627,6 +1627,8 @@
"openUrlDirectHint": "ON — the payment link opens immediately, without the intermediate button panel. OFF — an \"Open payment\" button is shown. Inside Telegram the link opens in the external browser either way (otherwise paying via a bank app/SBP fails); after payment the user returns to the result page. Stars/CryptoBot (t.me/ links) always open natively and ignore this flag.", "openUrlDirectHint": "ON — the payment link opens immediately, without the intermediate button panel. OFF — an \"Open payment\" button is shown. Inside Telegram the link opens in the external browser either way (otherwise paying via a bank app/SBP fails); after payment the user returns to the result page. Stars/CryptoBot (t.me/ links) always open natively and ignore this flag.",
"displayName": "Display name", "displayName": "Display name",
"displayNameHint": "Leave empty for default name", "displayNameHint": "Leave empty for default name",
"description": "Description",
"descriptionHint": "Shown under the method name. Empty for default text",
"subOptions": "Payment options", "subOptions": "Payment options",
"minAmount": "Min amount (kopeks)", "minAmount": "Min amount (kopeks)",
"maxAmount": "Max amount (kopeks)", "maxAmount": "Max amount (kopeks)",

View File

@@ -1649,6 +1649,8 @@
"openUrlDirectHint": "ВКЛ — ссылка оплаты открывается сразу, без промежуточной панели с кнопкой. ВЫКЛ — показывается кнопка «Открыть оплату». В Telegram ссылка в обоих случаях открывается во внешнем браузере (иначе оплата через банковское приложение/СБП не срабатывает), после оплаты — возврат на страницу результата. Stars/CryptoBot (t.me/-ссылки) всегда открываются нативно, флаг к ним не применяется.", "openUrlDirectHint": "ВКЛ — ссылка оплаты открывается сразу, без промежуточной панели с кнопкой. ВЫКЛ — показывается кнопка «Открыть оплату». В Telegram ссылка в обоих случаях открывается во внешнем браузере (иначе оплата через банковское приложение/СБП не срабатывает), после оплаты — возврат на страницу результата. Stars/CryptoBot (t.me/-ссылки) всегда открываются нативно, флаг к ним не применяется.",
"displayName": "Отображаемое имя", "displayName": "Отображаемое имя",
"displayNameHint": "Оставьте пустым для имени по умолчанию", "displayNameHint": "Оставьте пустым для имени по умолчанию",
"description": "Описание",
"descriptionHint": "Показывается под названием метода. Пусто — текст по умолчанию",
"subOptions": "Варианты оплаты", "subOptions": "Варианты оплаты",
"minAmount": "Мин. сумма (коп.)", "minAmount": "Мин. сумма (коп.)",
"maxAmount": "Макс. сумма (коп.)", "maxAmount": "Макс. сумма (коп.)",

View File

@@ -232,6 +232,7 @@ export default function AdminPaymentMethodEdit() {
// Local state for editing // Local state for editing
const [isEnabled, setIsEnabled] = useState(false); const [isEnabled, setIsEnabled] = useState(false);
const [customName, setCustomName] = useState(''); const [customName, setCustomName] = useState('');
const [customDesc, setCustomDesc] = useState('');
const [subOptions, setSubOptions] = useState<Record<string, boolean>>({}); const [subOptions, setSubOptions] = useState<Record<string, boolean>>({});
const [minAmount, setMinAmount] = useState<number | ''>(''); const [minAmount, setMinAmount] = useState<number | ''>('');
const [maxAmount, setMaxAmount] = useState<number | ''>(''); const [maxAmount, setMaxAmount] = useState<number | ''>('');
@@ -249,6 +250,7 @@ export default function AdminPaymentMethodEdit() {
if (config) { if (config) {
setIsEnabled(config.is_enabled); setIsEnabled(config.is_enabled);
setCustomName(config.display_name || ''); setCustomName(config.display_name || '');
setCustomDesc(config.description || '');
setSubOptions(config.sub_options || {}); setSubOptions(config.sub_options || {});
setMinAmount(config.min_amount_kopeks ?? ''); setMinAmount(config.min_amount_kopeks ?? '');
setMaxAmount(config.max_amount_kopeks ?? ''); setMaxAmount(config.max_amount_kopeks ?? '');
@@ -290,6 +292,13 @@ export default function AdminPaymentMethodEdit() {
data.reset_display_name = true; data.reset_display_name = true;
} }
// Description
if (customDesc.trim()) {
data.description = customDesc.trim();
} else {
data.reset_description = true;
}
// Sub-options // Sub-options
if (config.available_sub_options) { if (config.available_sub_options) {
data.sub_options = subOptions; data.sub_options = subOptions;
@@ -478,6 +487,22 @@ export default function AdminPaymentMethodEdit() {
</p> </p>
</div> </div>
{/* Description */}
<div>
<label className="mb-2 block text-sm font-medium text-dark-300">
{t('admin.paymentMethods.description')}
</label>
<textarea
value={customDesc}
onChange={(e) => setCustomDesc(e.target.value)}
rows={2}
className="input"
/>
<p className="mt-1 text-xs text-dark-500">
{t('admin.paymentMethods.descriptionHint')}
</p>
</div>
{/* Sub-options */} {/* Sub-options */}
{config.available_sub_options && config.available_sub_options.length > 0 && ( {config.available_sub_options && config.available_sub_options.length > 0 && (
<div> <div>

View File

@@ -335,11 +335,11 @@ export default function Balance() {
onClick={() => method.is_available && navigate(`/balance/top-up/${method.id}`)} onClick={() => method.is_available && navigate(`/balance/top-up/${method.id}`)}
> >
<div className="font-semibold text-dark-100"> <div className="font-semibold text-dark-100">
{translatedName || method.name} {method.name || translatedName}
</div> </div>
{(translatedDesc || method.description) && ( {(method.description || translatedDesc) && (
<div className="mt-1 text-sm text-dark-500"> <div className="mt-1 text-sm text-dark-500">
{translatedDesc || method.description} {method.description || translatedDesc}
</div> </div>
)} )}
<div className="mt-3 text-xs text-dark-400"> <div className="mt-3 text-xs text-dark-400">

View File

@@ -74,12 +74,12 @@ export default function TopUpMethodSelect() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<PaymentMethodIcon method={methodKey} className="h-8 w-8 flex-shrink-0" /> <PaymentMethodIcon method={methodKey} className="h-8 w-8 flex-shrink-0" />
<div className="font-semibold text-dark-100"> <div className="font-semibold text-dark-100">
{translatedName || method.name} {method.name || translatedName}
</div> </div>
</div> </div>
{(translatedDesc || method.description) && ( {(method.description || translatedDesc) && (
<div className="mt-1 text-sm text-dark-500"> <div className="mt-1 text-sm text-dark-500">
{translatedDesc || method.description} {method.description || translatedDesc}
</div> </div>
)} )}
<div className="mt-3 text-xs text-dark-400"> <div className="mt-3 text-xs text-dark-400">

View File

@@ -689,6 +689,7 @@ export interface PaymentMethodConfig {
is_enabled: boolean; is_enabled: boolean;
display_name: string | null; display_name: string | null;
default_display_name: string; default_display_name: string;
description: string | null;
sub_options: Record<string, boolean> | null; sub_options: Record<string, boolean> | null;
available_sub_options: PaymentMethodSubOptionInfo[] | null; available_sub_options: PaymentMethodSubOptionInfo[] | null;
quick_amounts: number[] | null; quick_amounts: number[] | null;