From 2154fce094554e3c98d4b9cee7c289d6d1fbae96 Mon Sep 17 00:00:00 2001 From: CaIon Date: Wed, 17 Jun 2026 20:47:12 +0800 Subject: [PATCH] feat(payment): enhance payment method configuration with icons and improve UI interactions --- .../operation_setting/payment_setting_old.go | 14 +- web/default/scripts/sync-i18n.mjs | 2 +- .../src/components/react-icon-by-name.tsx | 173 ++ .../risk-acknowledgement-dialog.tsx | 64 +- .../integrations/payment-method-dialog.tsx | 178 ++- .../payment-methods-visual-editor.tsx | 167 +- .../integrations/payment-settings-section.tsx | 1404 +++++++++-------- .../components/common-logs-filter-bar.tsx | 138 +- .../features/wallet/hooks/use-topup-info.ts | 19 +- web/default/src/features/wallet/lib/ui.tsx | 20 +- web/default/src/features/wallet/types.ts | 4 +- web/default/src/i18n/locales/en.json | 22 + web/default/src/i18n/locales/fr.json | 26 +- web/default/src/i18n/locales/ja.json | 32 +- web/default/src/i18n/locales/ru.json | 24 +- web/default/src/i18n/locales/vi.json | 26 +- web/default/src/i18n/locales/zh.json | 28 +- 17 files changed, 1437 insertions(+), 904 deletions(-) create mode 100644 web/default/src/components/react-icon-by-name.tsx diff --git a/setting/operation_setting/payment_setting_old.go b/setting/operation_setting/payment_setting_old.go index d34b6f0b..41994a8b 100644 --- a/setting/operation_setting/payment_setting_old.go +++ b/setting/operation_setting/payment_setting_old.go @@ -19,18 +19,18 @@ var USDExchangeRate = 7.3 var PayMethods = []map[string]string{ { - "name": "支付宝", - "color": "rgba(var(--semi-blue-5), 1)", - "type": "alipay", + "name": "支付宝", + "icon": "SiAlipay", + "type": "alipay", }, { - "name": "微信", - "color": "rgba(var(--semi-green-5), 1)", - "type": "wxpay", + "name": "微信", + "icon": "SiWechat", + "type": "wxpay", }, { "name": "自定义1", - "color": "black", + "icon": "LuCreditCard", "type": "custom1", "min_topup": "50", }, diff --git a/web/default/scripts/sync-i18n.mjs b/web/default/scripts/sync-i18n.mjs index c962f7d2..ff5d463a 100644 --- a/web/default/scripts/sync-i18n.mjs +++ b/web/default/scripts/sync-i18n.mjs @@ -39,6 +39,7 @@ const BRAND_AND_LITERAL_KEYS = new Set([ 'AccessKey / SecretAccessKey', 'AZURE_OPENAI_ENDPOINT *', 'Baidu V2', + 'CC Switch', 'ChatGPT', 'ChatGPT Subscription (Codex)', 'Claude', @@ -317,4 +318,3 @@ main().catch((err) => { console.error(err) process.exitCode = 1 }) - diff --git a/web/default/src/components/react-icon-by-name.tsx b/web/default/src/components/react-icon-by-name.tsx new file mode 100644 index 00000000..9c5c9cdd --- /dev/null +++ b/web/default/src/components/react-icon-by-name.tsx @@ -0,0 +1,173 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { useEffect, useState } from 'react' +import type { IconBaseProps, IconType } from 'react-icons' + +type IconPackModule = Record +type IconPackLoader = () => Promise + +const ICON_PACK_LOADERS = { + ai: () => import('react-icons/ai').then((module) => module as IconPackModule), + bi: () => import('react-icons/bi').then((module) => module as IconPackModule), + bs: () => import('react-icons/bs').then((module) => module as IconPackModule), + cg: () => import('react-icons/cg').then((module) => module as IconPackModule), + ci: () => import('react-icons/ci').then((module) => module as IconPackModule), + di: () => import('react-icons/di').then((module) => module as IconPackModule), + fa: () => import('react-icons/fa').then((module) => module as IconPackModule), + fa6: () => + import('react-icons/fa6').then((module) => module as IconPackModule), + fc: () => import('react-icons/fc').then((module) => module as IconPackModule), + fi: () => import('react-icons/fi').then((module) => module as IconPackModule), + gi: () => import('react-icons/gi').then((module) => module as IconPackModule), + go: () => import('react-icons/go').then((module) => module as IconPackModule), + gr: () => import('react-icons/gr').then((module) => module as IconPackModule), + hi: () => import('react-icons/hi').then((module) => module as IconPackModule), + hi2: () => + import('react-icons/hi2').then((module) => module as IconPackModule), + im: () => import('react-icons/im').then((module) => module as IconPackModule), + io: () => import('react-icons/io').then((module) => module as IconPackModule), + io5: () => + import('react-icons/io5').then((module) => module as IconPackModule), + lia: () => + import('react-icons/lia').then((module) => module as IconPackModule), + lu: () => import('react-icons/lu').then((module) => module as IconPackModule), + md: () => import('react-icons/md').then((module) => module as IconPackModule), + pi: () => import('react-icons/pi').then((module) => module as IconPackModule), + ri: () => import('react-icons/ri').then((module) => module as IconPackModule), + rx: () => import('react-icons/rx').then((module) => module as IconPackModule), + si: () => import('react-icons/si').then((module) => module as IconPackModule), + sl: () => import('react-icons/sl').then((module) => module as IconPackModule), + tb: () => import('react-icons/tb').then((module) => module as IconPackModule), + tfi: () => + import('react-icons/tfi').then((module) => module as IconPackModule), + ti: () => import('react-icons/ti').then((module) => module as IconPackModule), + vsc: () => + import('react-icons/vsc').then((module) => module as IconPackModule), + wi: () => import('react-icons/wi').then((module) => module as IconPackModule), +} satisfies Record + +type IconPackId = keyof typeof ICON_PACK_LOADERS + +const ICON_PACK_CACHE = new Map>() + +const ICON_PACK_CANDIDATES: Array<[RegExp, IconPackId[]]> = [ + [/^Ai[A-Z0-9]/, ['ai']], + [/^Bi[A-Z0-9]/, ['bi']], + [/^Bs[A-Z0-9]/, ['bs']], + [/^Cg[A-Z0-9]/, ['cg']], + [/^Ci[A-Z0-9]/, ['ci']], + [/^Di[A-Z0-9]/, ['di']], + [/^Fa[A-Z0-9]/, ['fa6', 'fa']], + [/^Fc[A-Z0-9]/, ['fc']], + [/^Fi[A-Z0-9]/, ['fi']], + [/^Gi[A-Z0-9]/, ['gi']], + [/^Go[A-Z0-9]/, ['go']], + [/^Gr[A-Z0-9]/, ['gr']], + [/^Hi[A-Z0-9]/, ['hi2', 'hi']], + [/^Im[A-Z0-9]/, ['im']], + [/^Io[A-Z0-9]/, ['io5', 'io']], + [/^Lia[A-Z0-9]/, ['lia']], + [/^Lu[A-Z0-9]/, ['lu']], + [/^Md[A-Z0-9]/, ['md']], + [/^Pi[A-Z0-9]/, ['pi']], + [/^Ri[A-Z0-9]/, ['ri']], + [/^Rx[A-Z0-9]/, ['rx']], + [/^Si[A-Z0-9]/, ['si']], + [/^Sl[A-Z0-9]/, ['sl']], + [/^Tb[A-Z0-9]/, ['tb']], + [/^Tfi[A-Z0-9]/, ['tfi']], + [/^Ti[A-Z0-9]/, ['ti']], + [/^Vsc[A-Z0-9]/, ['vsc']], + [/^Wi[A-Z0-9]/, ['wi']], +] + +function normalizeIconName(name: string | null | undefined): string | null { + const trimmed = name?.trim() + if (!trimmed || !/^[A-Z][A-Za-z0-9]*$/.test(trimmed)) return null + return trimmed +} + +function getCandidatePacks(iconName: string): IconPackId[] { + return ( + ICON_PACK_CANDIDATES.find(([pattern]) => pattern.test(iconName))?.[1] ?? [] + ) +} + +function loadIconPack(packId: IconPackId): Promise { + const cached = ICON_PACK_CACHE.get(packId) + if (cached) return cached + + const promise = ICON_PACK_LOADERS[packId]() + ICON_PACK_CACHE.set(packId, promise) + return promise +} + +function isIconComponent(value: unknown): value is IconType { + return typeof value === 'function' +} + +async function resolveReactIcon(iconName: string): Promise { + for (const packId of getCandidatePacks(iconName)) { + try { + const icon = (await loadIconPack(packId))[iconName] + if (isIconComponent(icon)) return icon + } catch { + // Missing chunks or unknown packs should behave the same as unknown names. + } + } + return null +} + +type ReactIconByNameProps = IconBaseProps & { + name?: string | null +} + +type ResolvedIconState = { + iconName: string + Icon: IconType | null +} + +export function ReactIconByName({ name, ...props }: ReactIconByNameProps) { + const iconName = normalizeIconName(name) + const [resolvedIcon, setResolvedIcon] = useState( + null + ) + + useEffect(() => { + let cancelled = false + + if (!iconName) return + + void resolveReactIcon(iconName).then((Icon) => { + if (!cancelled) setResolvedIcon({ iconName, Icon }) + }) + + return () => { + cancelled = true + } + }, [iconName]) + + if (!iconName || resolvedIcon?.iconName !== iconName || !resolvedIcon.Icon) { + return null + } + + const Icon = resolvedIcon.Icon + + return +} diff --git a/web/default/src/components/risk-acknowledgement-dialog.tsx b/web/default/src/components/risk-acknowledgement-dialog.tsx index 19b77798..388223f5 100644 --- a/web/default/src/components/risk-acknowledgement-dialog.tsx +++ b/web/default/src/components/risk-acknowledgement-dialog.tsx @@ -30,8 +30,8 @@ import { } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' type RequiredTextPart = { type: 'input' | 'static' @@ -63,6 +63,10 @@ type RiskAcknowledgementDialogProps = { className?: string } +function getRequiredTextRows(text: string) { + return Math.max(1, Math.ceil(Array.from(text).length / 42)) +} + export function RiskAcknowledgementDialog({ open, onOpenChange, @@ -90,15 +94,21 @@ export function RiskAcknowledgementDialog({ const normalizedRequiredTextParts = useMemo< NormalizedRequiredTextPart[] >(() => { - let inputIndex = 0 - return requiredTextParts.map((part) => { - if (part.type === 'input') { - const normalizedPart = { ...part, inputIndex } - inputIndex += 1 - return normalizedPart - } - return part - }) + return requiredTextParts.reduce<{ + parts: NormalizedRequiredTextPart[] + inputIndex: number + }>( + (acc, part) => { + if (part.type !== 'input') { + return { ...acc, parts: [...acc.parts, part] } + } + return { + parts: [...acc.parts, { ...part, inputIndex: acc.inputIndex }], + inputIndex: acc.inputIndex + 1, + } + }, + { parts: [], inputIndex: 0 } + ).parts }, [requiredTextParts]) const requiredTextInputCount = useMemo( @@ -114,9 +124,12 @@ export function RiskAcknowledgementDialog({ useEffect(() => { if (!open) return - setCheckedItems(Array(checklist.length).fill(false)) - setTypedText('') - setTypedTextParts(Array(requiredTextInputCount).fill('')) + const timer = window.setTimeout(() => { + setCheckedItems(Array(checklist.length).fill(false)) + setTypedText('') + setTypedTextParts(Array(requiredTextInputCount).fill('')) + }, 0) + return () => window.clearTimeout(timer) }, [open, checklist.length, requiredTextInputCount]) const allChecked = useMemo(() => { @@ -225,21 +238,21 @@ export function RiskAcknowledgementDialog({ -
+
{requiredTextToDisplay}
{hasSegmentedRequiredText ? ( -
+
{normalizedRequiredTextParts.map((part, index) => part.type === 'static' ? ( {part.text} ) : ( - @@ -254,30 +267,27 @@ export function RiskAcknowledgementDialog({ inputPlaceholder ?? t('Type the confirmation text here') } + rows={getRequiredTextRows(part.text)} autoFocus={open && part.inputIndex === 0} - onCopy={(event) => event.preventDefault()} - onCut={(event) => event.preventDefault()} - onPaste={(event) => event.preventDefault()} - onDrop={(event) => event.preventDefault()} + wrap='soft' aria-invalid={hasTypedRequiredText && !typedMatched} - className='w-full font-mono sm:w-64' + className='min-h-10 resize-none overflow-hidden font-mono text-sm leading-6' /> ) )}
) : ( - setTypedText(event.target.value)} placeholder={ inputPlaceholder ?? t('Type the confirmation text here') } + rows={getRequiredTextRows(requiredText)} autoFocus={open} - onCopy={(event) => event.preventDefault()} - onCut={(event) => event.preventDefault()} - onPaste={(event) => event.preventDefault()} - onDrop={(event) => event.preventDefault()} + wrap='soft' aria-invalid={hasTypedRequiredText && !typedMatched} + className='min-h-16 resize-none overflow-hidden font-mono text-sm leading-6' /> )} {hasTypedRequiredText && !typedMatched ? ( diff --git a/web/default/src/features/system-settings/integrations/payment-method-dialog.tsx b/web/default/src/features/system-settings/integrations/payment-method-dialog.tsx index df81c226..9826a865 100644 --- a/web/default/src/features/system-settings/integrations/payment-method-dialog.tsx +++ b/web/default/src/features/system-settings/integrations/payment-method-dialog.tsx @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useEffect, useMemo } from 'react' +import { useEffect } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' @@ -34,12 +34,13 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Dialog } from '@/components/dialog' +import { ReactIconByName } from '@/components/react-icon-by-name' const createPaymentMethodDialogSchema = (t: (key: string) => string) => z.object({ name: z.string().min(1, t('Payment method name is required')), - type: z.string().min(1, t('Payment method type is required')), - color: z.string().min(1, t('Color is required')), + type: z.string().min(1, t('Payment type key is required')), + icon: z.string().optional(), min_topup: z.string().optional(), }) @@ -52,8 +53,9 @@ const PAYMENT_METHOD_FORM_ID = 'payment-method-form' export type PaymentMethodData = { name: string type: string - color: string + icon?: string min_topup?: string + color?: string } type PaymentMethodDialogProps = { @@ -63,42 +65,14 @@ type PaymentMethodDialogProps = { editData?: PaymentMethodData | null } -const PAYMENT_TYPES = [ - { value: 'alipay', label: 'Alipay' }, - { value: 'wxpay', label: 'WeChat Pay' }, - { value: 'stripe', label: 'Stripe' }, -] - -const getColorPreview = (color: string) => { - if (color.includes('var(--')) { - return null - } - return color +const PAYMENT_TYPE_ICON_NAMES: Record = { + alipay: 'SiAlipay', + stripe: 'SiStripe', + waffo_pancake: 'LuCreditCard', + wxpay: 'SiWechat', } -const COLOR_PRESETS = [ - { value: '#1677FF', label: 'Blue (Alipay)' }, - { value: '#07C160', label: 'Green (WeChat)' }, - { value: '#635BFF', label: 'Purple (Stripe)' }, - { value: '#1890FF', label: 'Sky Blue' }, - { value: '#52C41A', label: 'Lime Green' }, - { value: 'black', label: 'Black' }, - { value: '#FF4D4F', label: 'Red' }, - { value: '#FFA940', label: 'Orange' }, -].map((preset) => { - const previewColor = getColorPreview(preset.value) - return { - ...preset, - icon: previewColor ? ( -
- ) : ( -
- ), - } -}) +const getDefaultIconName = (type: string) => PAYMENT_TYPE_ICON_NAMES[type] ?? '' export function PaymentMethodDialog({ open, @@ -109,41 +83,60 @@ export function PaymentMethodDialog({ const { t } = useTranslation() const isEditMode = !!editData const paymentMethodDialogSchema = createPaymentMethodDialogSchema(t) + const paymentTypeOptions = [ + { + iconName: 'SiAlipay', + label: `${t('Alipay')} (Epay: alipay)`, + name: t('Alipay'), + value: 'alipay', + }, + { + iconName: 'SiWechat', + label: `${t('WeChat Pay')} (Epay: wxpay)`, + name: t('WeChat Pay'), + value: 'wxpay', + }, + { + iconName: 'SiStripe', + label: `${t('Stripe')} (stripe)`, + name: t('Stripe'), + value: 'stripe', + }, + { + iconName: 'LuCreditCard', + label: 'Waffo Pancake (waffo_pancake)', + name: 'Waffo Pancake', + value: 'waffo_pancake', + }, + ] + const getPaymentTypeOption = (value: string) => + paymentTypeOptions.find((option) => option.value === value) const form = useForm({ resolver: zodResolver(paymentMethodDialogSchema), defaultValues: { name: '', type: '', - color: '', + icon: '', min_topup: '', }, }) - const colorValue = form.watch('color') - - const colorPreview = useMemo(() => { - if (!colorValue) return null - try { - // For CSS variables like rgba(var(--semi-blue-5), 1), we can't preview accurately - // but we can detect common patterns - if (colorValue.includes('var(--')) { - return null // Can't preview CSS variables reliably - } - return colorValue - } catch { - return null - } - }, [colorValue]) + const iconValue = form.watch('icon') useEffect(() => { if (editData) { - form.reset(editData) + form.reset({ + name: editData.name, + type: editData.type, + icon: editData.icon ?? getDefaultIconName(editData.type), + min_topup: editData.min_topup ?? '', + }) } else { form.reset({ name: '', type: '', - color: '', + icon: '', min_topup: '', }) } @@ -153,7 +146,9 @@ export function PaymentMethodDialog({ const data: PaymentMethodData = { name: values.name, type: values.type, - color: values.color, + } + if (values.icon && values.icon.trim() !== '') { + data.icon = values.icon.trim() } if (values.min_topup && values.min_topup.trim() !== '') { data.min_topup = values.min_topup @@ -215,19 +210,46 @@ export function PaymentMethodDialog({ name='type' render={({ field }) => ( - {t('Type')} + {t('Payment type key')} { + if (value === null) return + const currentIcon = form.getValues('icon')?.trim() + const currentName = form.getValues('name')?.trim() + const previousOption = getPaymentTypeOption(field.value) + const nextOption = getPaymentTypeOption(value) + + field.onChange(value) + if ( + nextOption?.iconName && + (!currentIcon || + currentIcon === previousOption?.iconName) + ) { + form.setValue('icon', nextOption.iconName, { + shouldDirty: true, + }) + } + if ( + nextOption?.name && + (!currentName || currentName === previousOption?.name) + ) { + form.setValue('name', nextOption.name, { + shouldDirty: true, + }) + } + }} + placeholder={t('Select or enter payment type key')} + searchPlaceholder={t('Search payment type keys...')} allowCustomValue /> - - {t('Select from presets or type custom identifier.')} + + {t( + 'Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.' + )} @@ -236,32 +258,30 @@ export function PaymentMethodDialog({ ( - {t('Color')} + {t('Icon')}
- - {colorPreview && ( -
)}
- {t('Select preset or enter custom CSS color value.')} + {t( + 'Enter a react-icons component name. Invalid names show no icon.' + )} diff --git a/web/default/src/features/system-settings/integrations/payment-methods-visual-editor.tsx b/web/default/src/features/system-settings/integrations/payment-methods-visual-editor.tsx index 965c2a07..c35cfdda 100644 --- a/web/default/src/features/system-settings/integrations/payment-methods-visual-editor.tsx +++ b/web/default/src/features/system-settings/integrations/payment-methods-visual-editor.tsx @@ -27,6 +27,7 @@ import { PopoverTrigger, } from '@/components/ui/popover' import { StaticDataTable } from '@/components/data-table' +import { ReactIconByName } from '@/components/react-icon-by-name' import { safeJsonParseWithValidation } from '../utils/json-parser' import { isArray } from '../utils/json-validators' import { @@ -39,47 +40,70 @@ type PaymentMethodsVisualEditorProps = { onChange: (value: string) => void } -const PAYMENT_TEMPLATES = [ - { - name: 'Alipay', - template: { - color: 'rgba(var(--semi-blue-5), 1)', - name: '支付宝', - type: 'alipay', - }, - }, - { - name: 'WeChat Pay', - template: { - color: 'rgba(var(--semi-green-5), 1)', - name: '微信', - type: 'wxpay', - }, - }, - { - name: 'Stripe', - template: { - color: 'rgba(var(--semi-green-5), 1)', - name: 'Stripe', - type: 'stripe', - }, - }, - { - name: 'Custom', - template: { - color: 'black', - min_topup: '50', - name: '自定义1', - type: 'custom1', - }, - }, -] +const PAYMENT_TYPE_ICON_NAMES: Record = { + alipay: 'SiAlipay', + stripe: 'SiStripe', + waffo_pancake: 'LuCreditCard', + wxpay: 'SiWechat', +} + +function getDefaultIconName(type: string) { + return PAYMENT_TYPE_ICON_NAMES[type] ?? '' +} + +function getEffectiveIconName(method: PaymentMethodData) { + return method.icon || getDefaultIconName(method.type) +} export function PaymentMethodsVisualEditor({ value, onChange, }: PaymentMethodsVisualEditorProps) { const { t } = useTranslation() + const paymentTemplates = [ + { + name: t('Epay Alipay'), + template: { + icon: getDefaultIconName('alipay'), + name: '支付宝', + type: 'alipay', + }, + }, + { + name: t('Epay WeChat Pay'), + template: { + icon: getDefaultIconName('wxpay'), + name: '微信', + type: 'wxpay', + }, + }, + { + name: t('Stripe'), + template: { + icon: getDefaultIconName('stripe'), + min_topup: '10', + name: 'Stripe', + type: 'stripe', + }, + }, + { + name: 'Waffo Pancake', + template: { + icon: getDefaultIconName('waffo_pancake'), + name: 'Waffo Pancake', + type: 'waffo_pancake', + }, + }, + { + name: t('Custom Epay method'), + template: { + icon: 'LuCreditCard', + min_topup: '50', + name: '自定义1', + type: 'custom1', + }, + }, + ] const [searchText, setSearchText] = useState('') const [dialogOpen, setDialogOpen] = useState(false) const [editData, setEditData] = useState(null) @@ -98,10 +122,11 @@ export function PaymentMethodsVisualEditor({ item !== null && 'name' in item && 'type' in item && - 'color' in item && typeof item.name === 'string' && typeof item.type === 'string' && - typeof item.color === 'string' + (!('icon' in item) || typeof item.icon === 'string') && + (!('min_topup' in item) || typeof item.min_topup === 'string') && + (!('color' in item) || typeof item.color === 'string') ) }, [value]) @@ -111,7 +136,8 @@ export function PaymentMethodsVisualEditor({ return paymentMethods.filter( (method) => method.name.toLowerCase().includes(lowerSearch) || - method.type.toLowerCase().includes(lowerSearch) + method.type.toLowerCase().includes(lowerSearch) || + getEffectiveIconName(method).toLowerCase().includes(lowerSearch) ) }, [paymentMethods, searchText]) @@ -202,14 +228,6 @@ export function PaymentMethodsVisualEditor({ } } - const getColorPreview = (color: string) => { - // For CSS variables, show a placeholder - if (color.includes('var(--')) { - return null - } - return color - } - return (
@@ -235,10 +253,10 @@ export function PaymentMethodsVisualEditor({

- {t('Quick insert common payment methods')} + {t('Quick insert payment entries')}

- {PAYMENT_TEMPLATES.map((item) => ( + {paymentTemplates.map((item) => ( -
- - {payMethodsVisualMode ? ( - - ) : ( -