+
{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) => (