Merge branch 'main' into rebuild/auth-identity-foundation
This commit is contained in:
@@ -88,13 +88,24 @@
|
||||
v-model="config[field.key]"
|
||||
rows="3"
|
||||
class="input font-mono text-xs"
|
||||
autocomplete="new-password"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
spellcheck="false"
|
||||
:placeholder="editing ? t('admin.accounts.leaveEmptyToKeep') : ''"
|
||||
/>
|
||||
<div v-else-if="field.sensitive" class="relative">
|
||||
<input
|
||||
:type="visibleFields[field.key] ? 'text' : 'password'"
|
||||
v-model="config[field.key]"
|
||||
class="input pr-10"
|
||||
:placeholder="field.defaultValue || ''"
|
||||
autocomplete="new-password"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
spellcheck="false"
|
||||
:placeholder="editing ? t('admin.accounts.leaveEmptyToKeep') : (field.defaultValue || '')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -398,9 +409,12 @@ function handleSave() {
|
||||
emitValidationError(t('admin.settings.payment.validationNameRequired'))
|
||||
return
|
||||
}
|
||||
// Validate required config fields — all non-optional fields must be filled
|
||||
// Validate required config fields — all non-optional fields must be filled.
|
||||
// In edit mode, sensitive fields may be left blank to preserve the stored
|
||||
// value (backend merges blanks by preserving the existing secret).
|
||||
for (const f of PROVIDER_CONFIG_FIELDS[form.provider_key] || []) {
|
||||
if (f.optional) continue
|
||||
if (props.editing && f.sensitive) continue
|
||||
const val = (config[f.key] || '').trim()
|
||||
if (!val) {
|
||||
const label = f.label || t(`admin.settings.payment.field_${f.key}`)
|
||||
@@ -412,8 +426,6 @@ function handleSave() {
|
||||
const filteredConfig: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (!v || !v.trim()) continue
|
||||
// Skip masked values — backend keeps existing credentials
|
||||
if (v === '••••••••') continue
|
||||
filteredConfig[k] = v
|
||||
}
|
||||
|
||||
@@ -470,7 +482,8 @@ function loadProvider(provider: ProviderInstance) {
|
||||
form.refund_enabled = provider.refund_enabled
|
||||
form.allow_user_refund = provider.allow_user_refund
|
||||
clearConfig()
|
||||
// Pre-fill config from API response (non-sensitive in cleartext, sensitive masked as ••••••••)
|
||||
// Pre-fill config from API response. Backend omits sensitive fields entirely,
|
||||
// so those inputs stay blank — submitting blank preserves the stored secret.
|
||||
if (provider.config) {
|
||||
for (const [k, v] of Object.entries(provider.config)) {
|
||||
// Skip notifyUrl/returnUrl — they are derived from callbackBaseUrl
|
||||
|
||||
@@ -78,8 +78,8 @@ import Icon from '@/components/icons/Icon.vue'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import QRCode from 'qrcode'
|
||||
import alipayIcon from '@/assets/icons/alipay.svg'
|
||||
@@ -147,7 +147,7 @@ function getLogoForType(): string | null {
|
||||
|
||||
function reopenPopup() {
|
||||
if (props.payUrl) {
|
||||
window.open(props.payUrl, 'paymentPopup', POPUP_WINDOW_FEATURES)
|
||||
window.open(props.payUrl, 'paymentPopup', getPaymentPopupFeatures())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ async function handleCancel() {
|
||||
cleanup()
|
||||
emit('close')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import QRCode from 'qrcode'
|
||||
@@ -200,7 +200,7 @@ function isSuccessStatus(status: string | null | undefined): boolean {
|
||||
|
||||
function reopenPopup() {
|
||||
if (props.payUrl) {
|
||||
const win = window.open(props.payUrl, 'paymentPopup', POPUP_WINDOW_FEATURES)
|
||||
const win = window.open(props.payUrl, 'paymentPopup', getPaymentPopupFeatures())
|
||||
if (!win || win.closed) {
|
||||
window.location.href = props.payUrl
|
||||
}
|
||||
@@ -257,7 +257,7 @@ async function handleCancel() {
|
||||
cleanup()
|
||||
setOutcome('cancelled')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { STRIPE_POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import type { Stripe, StripeElements } from '@stripe/stripe-js'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
@@ -132,7 +132,7 @@ onMounted(async () => {
|
||||
selectedType.value = event.value.type
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
initError.value = extractApiErrorMessage(err, t('payment.stripeLoadFailed'))
|
||||
initError.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.stripeLoadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -151,7 +151,7 @@ async function handlePay() {
|
||||
amount: String(props.payAmount),
|
||||
},
|
||||
}).href
|
||||
const popup = window.open(popupUrl, 'paymentPopup', STRIPE_POPUP_WINDOW_FEATURES)
|
||||
const popup = window.open(popupUrl, 'paymentPopup', getPaymentPopupFeatures())
|
||||
|
||||
const onReady = (event: MessageEvent) => {
|
||||
if (event.source !== popup || event.data?.type !== 'STRIPE_POPUP_READY') return
|
||||
@@ -186,7 +186,7 @@ async function handlePay() {
|
||||
emit('success')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
error.value = extractApiErrorMessage(err, t('payment.result.failed'))
|
||||
error.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.result.failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -199,7 +199,7 @@ async function handleCancel() {
|
||||
await paymentAPI.cancelOrder(props.orderId)
|
||||
emit('back')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -43,11 +43,24 @@ export const METHOD_ORDER = ['alipay', 'alipay_direct', 'wxpay', 'wxpay_direct',
|
||||
export const PAYMENT_MODE_QRCODE = 'qrcode'
|
||||
export const PAYMENT_MODE_POPUP = 'popup'
|
||||
|
||||
/** Window features for payment popup windows */
|
||||
export const POPUP_WINDOW_FEATURES = 'width=1000,height=750,left=100,top=80,scrollbars=yes,resizable=yes'
|
||||
/** Preferred popup size for payment gateways. Alipay's standard checkout
|
||||
* (QR + account login panel) needs ~1200×900 to render without any scrolling. */
|
||||
const PAYMENT_POPUP_PREFERRED_WIDTH = 1250
|
||||
const PAYMENT_POPUP_PREFERRED_HEIGHT = 900
|
||||
|
||||
/** Wider popup for Stripe redirect methods (Alipay checkout page needs ~1200px) */
|
||||
export const STRIPE_POPUP_WINDOW_FEATURES = 'width=1250,height=780,left=80,top=60,scrollbars=yes,resizable=yes'
|
||||
/** Build a window.open features string sized to fit within the current screen
|
||||
* while preferring the above dimensions. Centers the popup on the available
|
||||
* work area so nothing is clipped on smaller laptop displays. */
|
||||
export function getPaymentPopupFeatures(): string {
|
||||
const screen = typeof window !== 'undefined' ? window.screen : null
|
||||
const availW = screen?.availWidth ?? PAYMENT_POPUP_PREFERRED_WIDTH
|
||||
const availH = screen?.availHeight ?? PAYMENT_POPUP_PREFERRED_HEIGHT
|
||||
const width = Math.min(PAYMENT_POPUP_PREFERRED_WIDTH, availW - 40)
|
||||
const height = Math.min(PAYMENT_POPUP_PREFERRED_HEIGHT, availH - 40)
|
||||
const left = Math.max(0, Math.floor((availW - width) / 2))
|
||||
const top = Math.max(0, Math.floor((availH - height) / 2))
|
||||
return `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`
|
||||
}
|
||||
|
||||
/** Webhook paths for each provider (relative to origin). */
|
||||
export const WEBHOOK_PATHS: Record<string, string> = {
|
||||
@@ -87,9 +100,9 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
{ key: 'mchId', label: '', sensitive: false },
|
||||
{ key: 'privateKey', label: '', sensitive: true },
|
||||
{ key: 'apiV3Key', label: '', sensitive: true },
|
||||
{ key: 'certSerial', label: '', sensitive: false },
|
||||
{ key: 'publicKey', label: '', sensitive: true },
|
||||
{ key: 'publicKeyId', label: '', sensitive: false },
|
||||
{ key: 'certSerial', label: '', sensitive: false },
|
||||
{ key: 'h5AppName', label: '', sensitive: false, optional: true },
|
||||
{ key: 'h5AppUrl', label: '', sensitive: false, optional: true },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user