revert: remove fork-only changes from release sync
Revert payment/wechat, sora/claude-max cleanup, fork-only migrations, and cosmetic changes that were brought in by the release sync commit. Keep only channel-monitor related improvements: - PublicSettingsInjectionPayload named struct with drift test - ChannelMonitorRunner graceful shutdown in wire - image_output_price in SupportedModelChip - Simplified buildSelfNavItems in AppSidebar - Gateway WARN logs for 503 branches
This commit is contained in:
@@ -73,9 +73,42 @@
|
||||
|
||||
<!-- Config fields -->
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-700">
|
||||
<h4 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.payment.providerConfig') }}
|
||||
</h4>
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.payment.providerConfig') }}
|
||||
</h4>
|
||||
<HelpTooltip v-if="paymentGuide" trigger="click" width-class="w-80">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-5 w-5 items-center justify-center rounded-full border border-gray-300 text-[11px] font-semibold text-gray-400 transition-colors hover:border-primary-500 hover:text-primary-600 dark:border-dark-500 dark:text-gray-500 dark:hover:border-primary-400 dark:hover:text-primary-400"
|
||||
:aria-label="t('admin.settings.payment.paymentGuideTrigger')"
|
||||
:title="t('admin.settings.payment.paymentGuideTrigger')"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</template>
|
||||
<div class="space-y-3">
|
||||
<p class="font-medium text-white">{{ paymentGuide.summary }}</p>
|
||||
<div
|
||||
v-for="item in paymentGuide.items"
|
||||
:key="item.title"
|
||||
class="space-y-1.5 border-t border-white/10 pt-2 first:border-t-0 first:pt-0"
|
||||
>
|
||||
<p class="font-medium text-white">{{ item.title }}</p>
|
||||
<p><span class="text-gray-300">{{ t('admin.settings.payment.guideOpenLabel') }}</span>{{ item.open }}</p>
|
||||
<p><span class="text-gray-300">{{ t('admin.settings.payment.guideCallLabel') }}</span>{{ item.call }}</p>
|
||||
<p><span class="text-gray-300">{{ t('admin.settings.payment.guideFallbackLabel') }}</span>{{ item.fallback }}</p>
|
||||
</div>
|
||||
<p v-if="paymentGuide.note" class="border-t border-white/10 pt-2 text-[11px] text-gray-300">
|
||||
{{ paymentGuide.note }}
|
||||
</p>
|
||||
</div>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<p v-if="paymentGuide" class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ paymentGuide.summary }}
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div v-for="field in resolvedFields" :key="field.key">
|
||||
<label class="input-label">
|
||||
@@ -220,6 +253,7 @@
|
||||
import { reactive, computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import HelpTooltip from '@/components/common/HelpTooltip.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import type { SelectOption } from '@/components/common/Select.vue'
|
||||
import ToggleSwitch from './ToggleSwitch.vue'
|
||||
@@ -263,6 +297,19 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface PaymentGuideItem {
|
||||
title: string
|
||||
open: string
|
||||
call: string
|
||||
fallback: string
|
||||
}
|
||||
|
||||
interface PaymentGuide {
|
||||
summary: string
|
||||
items: PaymentGuideItem[]
|
||||
note?: string
|
||||
}
|
||||
|
||||
// --- Form state ---
|
||||
const form = reactive({
|
||||
name: '',
|
||||
@@ -314,6 +361,63 @@ const resolvedFields = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const paymentGuide = computed<PaymentGuide | null>(() => {
|
||||
if (form.provider_key === 'alipay') {
|
||||
return {
|
||||
summary: t('admin.settings.payment.alipayGuideSummary'),
|
||||
items: [
|
||||
{
|
||||
title: t('admin.settings.payment.alipayGuideFaceToFaceTitle'),
|
||||
open: t('admin.settings.payment.alipayGuideFaceToFaceOpen'),
|
||||
call: t('admin.settings.payment.alipayGuideFaceToFaceCall'),
|
||||
fallback: t('admin.settings.payment.alipayGuideFaceToFaceFallback'),
|
||||
},
|
||||
{
|
||||
title: t('admin.settings.payment.alipayGuidePagePayTitle'),
|
||||
open: t('admin.settings.payment.alipayGuidePagePayOpen'),
|
||||
call: t('admin.settings.payment.alipayGuidePagePayCall'),
|
||||
fallback: t('admin.settings.payment.alipayGuidePagePayFallback'),
|
||||
},
|
||||
{
|
||||
title: t('admin.settings.payment.alipayGuideWapTitle'),
|
||||
open: t('admin.settings.payment.alipayGuideWapOpen'),
|
||||
call: t('admin.settings.payment.alipayGuideWapCall'),
|
||||
fallback: t('admin.settings.payment.alipayGuideWapFallback'),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (form.provider_key === 'wxpay') {
|
||||
return {
|
||||
summary: t('admin.settings.payment.wxpayGuideSummary'),
|
||||
note: t('admin.settings.payment.wxpayGuideNote'),
|
||||
items: [
|
||||
{
|
||||
title: t('admin.settings.payment.wxpayGuideNativeTitle'),
|
||||
open: t('admin.settings.payment.wxpayGuideNativeOpen'),
|
||||
call: t('admin.settings.payment.wxpayGuideNativeCall'),
|
||||
fallback: t('admin.settings.payment.wxpayGuideNativeFallback'),
|
||||
},
|
||||
{
|
||||
title: t('admin.settings.payment.wxpayGuideJsapiTitle'),
|
||||
open: t('admin.settings.payment.wxpayGuideJsapiOpen'),
|
||||
call: t('admin.settings.payment.wxpayGuideJsapiCall'),
|
||||
fallback: t('admin.settings.payment.wxpayGuideJsapiFallback'),
|
||||
},
|
||||
{
|
||||
title: t('admin.settings.payment.wxpayGuideH5Title'),
|
||||
open: t('admin.settings.payment.wxpayGuideH5Open'),
|
||||
call: t('admin.settings.payment.wxpayGuideH5Call'),
|
||||
fallback: t('admin.settings.payment.wxpayGuideH5Fallback'),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const limitableTypes = computed(() => {
|
||||
// Stripe: single "stripe" entry (one set of shared limits)
|
||||
if (form.provider_key === 'stripe') {
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="scanHint" class="text-center text-sm text-gray-500 dark:text-gray-400">{{ scanHint }}</p>
|
||||
<button v-if="payUrl" class="btn btn-secondary text-sm" @click="reopenPopup">
|
||||
{{ t('payment.qr.openPayWindow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import PaymentProviderDialog from '@/components/payment/PaymentProviderDialog.vue'
|
||||
|
||||
const messages: Record<string, string> = {
|
||||
'admin.settings.payment.providerConfig': 'Credentials',
|
||||
'admin.settings.payment.paymentGuideTrigger': 'View payment guide',
|
||||
'admin.settings.payment.alipayGuideSummary': 'Desktop prefers QR precreate and falls back to cashier; mobile prefers WAP checkout.',
|
||||
'admin.settings.payment.wxpayGuideSummary': 'Desktop prefers Native QR; mobile routes to JSAPI or H5 based on browser context.',
|
||||
}
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => messages[key] ?? key,
|
||||
}),
|
||||
}))
|
||||
|
||||
function mountDialog() {
|
||||
return mount(PaymentProviderDialog, {
|
||||
props: {
|
||||
show: true,
|
||||
saving: false,
|
||||
editing: null,
|
||||
allKeyOptions: [
|
||||
{ value: 'alipay', label: 'Alipay' },
|
||||
{ value: 'wxpay', label: 'WeChat Pay' },
|
||||
{ value: 'stripe', label: 'Stripe' },
|
||||
],
|
||||
enabledKeyOptions: [
|
||||
{ value: 'alipay', label: 'Alipay' },
|
||||
{ value: 'wxpay', label: 'WeChat Pay' },
|
||||
],
|
||||
allPaymentTypes: [
|
||||
{ value: 'alipay', label: 'Alipay' },
|
||||
{ value: 'wxpay', label: 'WeChat Pay' },
|
||||
],
|
||||
redirectLabel: 'Redirect',
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: {
|
||||
template: '<div><slot /><slot name="footer" /></div>',
|
||||
},
|
||||
Select: {
|
||||
props: ['modelValue', 'options', 'disabled'],
|
||||
template: '<div />',
|
||||
},
|
||||
ToggleSwitch: {
|
||||
template: '<div />',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('PaymentProviderDialog payment guide', () => {
|
||||
it('shows no payment guide for providers without a flow guide', () => {
|
||||
const wrapper = mountDialog()
|
||||
|
||||
expect(wrapper.text()).not.toContain(messages['admin.settings.payment.alipayGuideSummary'])
|
||||
expect(wrapper.text()).not.toContain(messages['admin.settings.payment.wxpayGuideSummary'])
|
||||
expect(wrapper.find('button[title="View payment guide"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['alipay', 'admin.settings.payment.alipayGuideSummary'],
|
||||
['wxpay', 'admin.settings.payment.wxpayGuideSummary'],
|
||||
])('shows the payment guide summary for %s', async (providerKey, summaryKey) => {
|
||||
const wrapper = mountDialog()
|
||||
|
||||
;(wrapper.vm as unknown as { reset: (key: string) => void }).reset(providerKey)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain(messages[summaryKey])
|
||||
expect(wrapper.find('button[title="View payment guide"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -96,4 +96,36 @@ describe('PaymentStatusPanel', () => {
|
||||
expect(wrapper.text()).toContain('payment.result.success')
|
||||
expect(wrapper.emitted('success')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('shows reopen button in QR mode when payUrl is also available', async () => {
|
||||
const openSpy = vi.spyOn(window, 'open').mockReturnValue({ closed: false } as Window)
|
||||
|
||||
const wrapper = mount(PaymentStatusPanel, {
|
||||
props: {
|
||||
orderId: 42,
|
||||
qrCode: 'https://pay.example.com/qr/42',
|
||||
payUrl: 'https://pay.example.com/session/42',
|
||||
expiresAt: '2099-01-01T12:30:00Z',
|
||||
paymentType: 'alipay',
|
||||
orderType: 'balance',
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Icon: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('payment.qr.openPayWindow')
|
||||
|
||||
await wrapper.get('button.btn.btn-secondary.text-sm').trigger('click')
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
'https://pay.example.com/session/42',
|
||||
'paymentPopup',
|
||||
expect.any(String),
|
||||
)
|
||||
|
||||
openSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -190,12 +190,14 @@ describe('buildCreateOrderPayload', () => {
|
||||
paymentType: 'alipay_direct',
|
||||
orderType: 'balance',
|
||||
origin: 'https://app.example.com/',
|
||||
isMobile: true,
|
||||
isWechatBrowser: false,
|
||||
})).toEqual({
|
||||
amount: 88,
|
||||
payment_type: 'alipay',
|
||||
order_type: 'balance',
|
||||
return_url: 'https://app.example.com/payment/result',
|
||||
is_mobile: true,
|
||||
payment_source: 'hosted_redirect',
|
||||
})
|
||||
})
|
||||
@@ -207,6 +209,7 @@ describe('buildCreateOrderPayload', () => {
|
||||
orderType: 'subscription',
|
||||
planId: 7,
|
||||
origin: 'https://app.example.com',
|
||||
isMobile: false,
|
||||
isWechatBrowser: true,
|
||||
})).toEqual({
|
||||
amount: 128,
|
||||
@@ -214,6 +217,7 @@ describe('buildCreateOrderPayload', () => {
|
||||
order_type: 'subscription',
|
||||
plan_id: 7,
|
||||
return_url: 'https://app.example.com/payment/result',
|
||||
is_mobile: false,
|
||||
payment_source: 'wechat_in_app_resume',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,9 +12,9 @@ describe('PROVIDER_CONFIG_FIELDS.wxpay', () => {
|
||||
expect(findField('certSerial')?.optional).toBeFalsy()
|
||||
})
|
||||
|
||||
it('exposes optional mp and H5 metadata fields for WeChat-specific flows', () => {
|
||||
expect(findField('mpAppId')?.optional).toBe(true)
|
||||
expect(findField('h5AppName')?.optional).toBe(true)
|
||||
expect(findField('h5AppUrl')?.optional).toBe(true)
|
||||
it('only keeps the simplified visible credential set in the admin form', () => {
|
||||
expect(findField('mpAppId')).toBeUndefined()
|
||||
expect(findField('h5AppName')).toBeUndefined()
|
||||
expect(findField('h5AppUrl')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface BuildCreateOrderPayloadInput {
|
||||
orderType: OrderType
|
||||
planId?: number
|
||||
origin?: string
|
||||
isMobile: boolean
|
||||
isWechatBrowser: boolean
|
||||
}
|
||||
|
||||
@@ -106,6 +107,7 @@ export function buildCreateOrderPayload(input: BuildCreateOrderPayloadInput): Cr
|
||||
amount: input.amount,
|
||||
payment_type: visibleMethod,
|
||||
order_type: input.orderType,
|
||||
is_mobile: input.isMobile,
|
||||
payment_source: visibleMethod === 'wxpay' && input.isWechatBrowser
|
||||
? 'wechat_in_app_resume'
|
||||
: 'hosted_redirect',
|
||||
|
||||
@@ -96,15 +96,12 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
],
|
||||
wxpay: [
|
||||
{ key: 'appId', label: 'App ID', sensitive: false },
|
||||
{ key: 'mpAppId', label: '', sensitive: false, optional: true },
|
||||
{ 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: 'h5AppName', label: '', sensitive: false, optional: true },
|
||||
{ key: 'h5AppUrl', label: '', sensitive: false, optional: true },
|
||||
],
|
||||
stripe: [
|
||||
{ key: 'secretKey', label: '', sensitive: true },
|
||||
|
||||
Reference in New Issue
Block a user