feat: drive visible payment methods from enabled providers
This commit is contained in:
@@ -4160,73 +4160,6 @@
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<div
|
||||
v-for="visibleMethod in paymentVisibleMethodCards"
|
||||
:key="visibleMethod.key"
|
||||
class="rounded-lg border border-gray-200 p-4 dark:border-dark-700"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label
|
||||
class="font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
{{
|
||||
t("admin.settings.paymentVisibleMethods.methodLabel", {
|
||||
title: visibleMethod.title,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<p
|
||||
class="mt-1 text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{{
|
||||
t("admin.settings.paymentVisibleMethods.methodHint")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="
|
||||
getPaymentVisibleMethodEnabled(visibleMethod.key)
|
||||
"
|
||||
@update:model-value="
|
||||
setPaymentVisibleMethodEnabled(
|
||||
visibleMethod.key,
|
||||
$event,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="input-label">
|
||||
{{ t("admin.settings.paymentVisibleMethods.sourceLabel") }}
|
||||
</label>
|
||||
<Select
|
||||
:model-value="
|
||||
getPaymentVisibleMethodSource(visibleMethod.key)
|
||||
"
|
||||
:options="
|
||||
getPaymentVisibleMethodSourceSelectOptions(
|
||||
visibleMethod.key,
|
||||
)
|
||||
"
|
||||
@update:model-value="
|
||||
setPaymentVisibleMethodSource(
|
||||
visibleMethod.key,
|
||||
$event,
|
||||
)
|
||||
"
|
||||
:placeholder="visibleMethod.key"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-400">
|
||||
{{
|
||||
t("admin.settings.paymentVisibleMethods.sourceHint")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 5: Help image + text -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
@@ -4742,15 +4675,12 @@ import {
|
||||
buildAuthSourceDefaultsState,
|
||||
defaultWeChatConnectScopesForMode,
|
||||
deriveWeChatConnectStoredMode,
|
||||
getPaymentVisibleMethodSourceOptions,
|
||||
normalizePaymentVisibleMethodSource,
|
||||
normalizeDefaultSubscriptionSettings,
|
||||
resolveWeChatConnectModeCapabilities,
|
||||
} from "@/api/admin/settings";
|
||||
import type {
|
||||
AuthSourceDefaultsState,
|
||||
AuthSourceType,
|
||||
PaymentVisibleMethod,
|
||||
SystemSettings,
|
||||
UpdateSettingsRequest,
|
||||
DefaultSubscriptionSetting,
|
||||
@@ -4777,6 +4707,7 @@ import { useClipboard } from "@/composables/useClipboard";
|
||||
import { extractApiErrorMessage } from "@/utils/apiError";
|
||||
import { useAppStore } from "@/stores";
|
||||
import { useAdminSettingsStore } from "@/stores/adminSettings";
|
||||
import { normalizeVisibleMethod } from "@/components/payment/paymentFlow";
|
||||
import {
|
||||
isRegistrationEmailSuffixDomainValid,
|
||||
normalizeRegistrationEmailSuffixDomain,
|
||||
@@ -4788,10 +4719,6 @@ const { t, locale } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const adminSettingsStore = useAdminSettingsStore();
|
||||
|
||||
function localText(zh: string, en: string): string {
|
||||
return locale.value.startsWith("zh") ? zh : en;
|
||||
}
|
||||
|
||||
type SettingsTab =
|
||||
| "general"
|
||||
| "security"
|
||||
@@ -4908,10 +4835,6 @@ type SettingsForm = Omit<
|
||||
wechat_connect_mobile_enabled: boolean;
|
||||
oidc_connect_client_secret: string;
|
||||
force_email_on_third_party_signup: boolean;
|
||||
payment_visible_method_alipay_source: string;
|
||||
payment_visible_method_wxpay_source: string;
|
||||
payment_visible_method_alipay_enabled: boolean;
|
||||
payment_visible_method_wxpay_enabled: boolean;
|
||||
openai_advanced_scheduler_enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -4957,10 +4880,6 @@ const form = reactive<SettingsForm>({
|
||||
payment_cancel_rate_limit_window: 1,
|
||||
payment_cancel_rate_limit_unit: "day",
|
||||
payment_cancel_rate_limit_window_mode: "rolling",
|
||||
payment_visible_method_alipay_source: "",
|
||||
payment_visible_method_wxpay_source: "",
|
||||
payment_visible_method_alipay_enabled: false,
|
||||
payment_visible_method_wxpay_enabled: false,
|
||||
table_default_page_size: tablePageSizeDefault,
|
||||
table_page_size_options: [10, 20, 50, 100],
|
||||
custom_menu_items: [] as Array<{
|
||||
@@ -5099,86 +5018,6 @@ const authSourceDefaultsMeta = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const paymentVisibleMethodCards = computed(() => [
|
||||
{
|
||||
key: "alipay" as const,
|
||||
title: t("payment.methods.alipay"),
|
||||
enabledField: "payment_visible_method_alipay_enabled" as const,
|
||||
sourceField: "payment_visible_method_alipay_source" as const,
|
||||
},
|
||||
{
|
||||
key: "wxpay" as const,
|
||||
title: t("payment.methods.wxpay"),
|
||||
enabledField: "payment_visible_method_wxpay_enabled" as const,
|
||||
sourceField: "payment_visible_method_wxpay_source" as const,
|
||||
},
|
||||
]);
|
||||
|
||||
function getPaymentVisibleMethodEnabled(method: "alipay" | "wxpay"): boolean {
|
||||
return method === "alipay"
|
||||
? form.payment_visible_method_alipay_enabled
|
||||
: form.payment_visible_method_wxpay_enabled;
|
||||
}
|
||||
|
||||
function setPaymentVisibleMethodEnabled(
|
||||
method: "alipay" | "wxpay",
|
||||
enabled: boolean,
|
||||
) {
|
||||
if (method === "alipay") {
|
||||
form.payment_visible_method_alipay_enabled = enabled;
|
||||
return;
|
||||
}
|
||||
form.payment_visible_method_wxpay_enabled = enabled;
|
||||
}
|
||||
|
||||
function getPaymentVisibleMethodSource(method: "alipay" | "wxpay"): string {
|
||||
return method === "alipay"
|
||||
? form.payment_visible_method_alipay_source
|
||||
: form.payment_visible_method_wxpay_source;
|
||||
}
|
||||
|
||||
function getPaymentVisibleMethodSourceSelectOptions(
|
||||
method: PaymentVisibleMethod,
|
||||
) {
|
||||
return getPaymentVisibleMethodSourceOptions(method).map((option) => ({
|
||||
value: option.value,
|
||||
label: localText(option.labelZh, option.labelEn),
|
||||
}));
|
||||
}
|
||||
|
||||
function setPaymentVisibleMethodSource(
|
||||
method: "alipay" | "wxpay",
|
||||
source: string | number | boolean | null,
|
||||
) {
|
||||
const normalized = normalizePaymentVisibleMethodSource(method, source);
|
||||
if (method === "alipay") {
|
||||
form.payment_visible_method_alipay_source = normalized;
|
||||
return;
|
||||
}
|
||||
form.payment_visible_method_wxpay_source = normalized;
|
||||
}
|
||||
|
||||
function validatePaymentVisibleMethodSelections(): boolean {
|
||||
for (const visibleMethod of paymentVisibleMethodCards.value) {
|
||||
if (!getPaymentVisibleMethodEnabled(visibleMethod.key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (getPaymentVisibleMethodSource(visibleMethod.key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
appStore.showError(
|
||||
t("admin.settings.paymentVisibleMethods.sourceRequiredError", {
|
||||
title: visibleMethod.title,
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Proxies for web search emulation ProxySelector
|
||||
const webSearchProxies = ref<Proxy[]>([]);
|
||||
|
||||
@@ -5660,16 +5499,6 @@ async function loadSettings() {
|
||||
form.default_subscriptions = normalizeDefaultSubscriptionSettings(
|
||||
settings.default_subscriptions,
|
||||
);
|
||||
form.payment_visible_method_alipay_source =
|
||||
normalizePaymentVisibleMethodSource(
|
||||
"alipay",
|
||||
settings.payment_visible_method_alipay_source,
|
||||
);
|
||||
form.payment_visible_method_wxpay_source =
|
||||
normalizePaymentVisibleMethodSource(
|
||||
"wxpay",
|
||||
settings.payment_visible_method_wxpay_source,
|
||||
);
|
||||
registrationEmailSuffixWhitelistTags.value =
|
||||
normalizeRegistrationEmailSuffixDomains(
|
||||
settings.registration_email_suffix_whitelist,
|
||||
@@ -5873,7 +5702,6 @@ async function saveSettings() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL fields — novalidate disables browser-native checks, so we validate here
|
||||
const isValidHttpUrl = (url: string): boolean => {
|
||||
if (!url) return true;
|
||||
@@ -6028,18 +5856,6 @@ async function saveSettings() {
|
||||
payment_cancel_rate_limit_unit: form.payment_cancel_rate_limit_unit,
|
||||
payment_cancel_rate_limit_window_mode:
|
||||
form.payment_cancel_rate_limit_window_mode,
|
||||
payment_visible_method_alipay_source: normalizePaymentVisibleMethodSource(
|
||||
"alipay",
|
||||
form.payment_visible_method_alipay_source,
|
||||
),
|
||||
payment_visible_method_wxpay_source: normalizePaymentVisibleMethodSource(
|
||||
"wxpay",
|
||||
form.payment_visible_method_wxpay_source,
|
||||
),
|
||||
payment_visible_method_alipay_enabled:
|
||||
form.payment_visible_method_alipay_enabled,
|
||||
payment_visible_method_wxpay_enabled:
|
||||
form.payment_visible_method_wxpay_enabled,
|
||||
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
|
||||
// Balance & quota notification
|
||||
balance_low_notify_enabled: form.balance_low_notify_enabled,
|
||||
@@ -6062,16 +5878,6 @@ async function saveSettings() {
|
||||
}
|
||||
}
|
||||
Object.assign(authSourceDefaults, buildAuthSourceDefaultsState(updated));
|
||||
form.payment_visible_method_alipay_source =
|
||||
normalizePaymentVisibleMethodSource(
|
||||
"alipay",
|
||||
updated.payment_visible_method_alipay_source,
|
||||
);
|
||||
form.payment_visible_method_wxpay_source =
|
||||
normalizePaymentVisibleMethodSource(
|
||||
"wxpay",
|
||||
updated.payment_visible_method_wxpay_source,
|
||||
);
|
||||
registrationEmailSuffixWhitelistTags.value =
|
||||
normalizeRegistrationEmailSuffixDomains(
|
||||
updated.registration_email_suffix_whitelist,
|
||||
@@ -6588,8 +6394,98 @@ const cancelRateLimitModeOptions = computed(() => [
|
||||
|
||||
const paymentErrorMap = computed(() => ({
|
||||
PENDING_ORDERS: t("payment.errors.PENDING_ORDERS"),
|
||||
PAYMENT_PROVIDER_CONFLICT: t("payment.errors.PAYMENT_PROVIDER_CONFLICT"),
|
||||
}));
|
||||
|
||||
type ProviderEnablementCandidate = Pick<
|
||||
ProviderInstance,
|
||||
"id" | "provider_key" | "supported_types" | "enabled" | "name"
|
||||
>;
|
||||
|
||||
function getProviderVisibleMethods(
|
||||
provider: ProviderEnablementCandidate,
|
||||
): Array<"alipay" | "wxpay"> {
|
||||
if (!provider.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const supportedTypes = Array.isArray(provider.supported_types)
|
||||
? provider.supported_types
|
||||
: [];
|
||||
const methods = new Set<"alipay" | "wxpay">();
|
||||
const addMethod = (type: string) => {
|
||||
const method = normalizeVisibleMethod(type);
|
||||
if (method === "alipay" || method === "wxpay") {
|
||||
methods.add(method);
|
||||
}
|
||||
};
|
||||
|
||||
if (provider.provider_key === "alipay") {
|
||||
if (supportedTypes.length === 0) {
|
||||
methods.add("alipay");
|
||||
} else {
|
||||
supportedTypes.forEach((type) => {
|
||||
if (normalizeVisibleMethod(type) === "alipay") {
|
||||
methods.add("alipay");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (provider.provider_key === "wxpay") {
|
||||
if (supportedTypes.length === 0) {
|
||||
methods.add("wxpay");
|
||||
} else {
|
||||
supportedTypes.forEach((type) => {
|
||||
if (normalizeVisibleMethod(type) === "wxpay") {
|
||||
methods.add("wxpay");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (provider.provider_key === "easypay") {
|
||||
supportedTypes.forEach(addMethod);
|
||||
}
|
||||
|
||||
return Array.from(methods);
|
||||
}
|
||||
|
||||
function findProviderEnablementConflict(
|
||||
candidate: ProviderEnablementCandidate,
|
||||
): { method: "alipay" | "wxpay"; conflicting: ProviderInstance } | null {
|
||||
const claimedMethods = getProviderVisibleMethods(candidate);
|
||||
if (claimedMethods.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const other of providers.value) {
|
||||
if (other.id === candidate.id || !other.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const otherMethods = getProviderVisibleMethods(other);
|
||||
const matchedMethod = claimedMethods.find((method) =>
|
||||
otherMethods.includes(method),
|
||||
);
|
||||
if (matchedMethod) {
|
||||
return {
|
||||
method: matchedMethod,
|
||||
conflicting: other,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function showProviderEnablementConflict(
|
||||
conflict: { method: "alipay" | "wxpay"; conflicting: ProviderInstance },
|
||||
) {
|
||||
appStore.showError(
|
||||
t("admin.settings.payment.enableConflict", {
|
||||
method: t(`payment.methods.${conflict.method}`),
|
||||
provider: conflict.conflicting.name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadProviders() {
|
||||
providersLoading.value = true;
|
||||
try {
|
||||
@@ -6619,6 +6515,21 @@ function openEditProvider(provider: ProviderInstance) {
|
||||
async function handleSaveProvider(payload: Partial<ProviderInstance>) {
|
||||
providerSaving.value = true;
|
||||
try {
|
||||
const candidate: ProviderEnablementCandidate = {
|
||||
id: editingProvider.value?.id ?? 0,
|
||||
provider_key:
|
||||
payload.provider_key ?? editingProvider.value?.provider_key ?? "",
|
||||
supported_types:
|
||||
payload.supported_types ?? editingProvider.value?.supported_types ?? [],
|
||||
enabled: payload.enabled ?? editingProvider.value?.enabled ?? false,
|
||||
name: payload.name ?? editingProvider.value?.name ?? "",
|
||||
};
|
||||
const conflict = findProviderEnablementConflict(candidate);
|
||||
if (conflict) {
|
||||
showProviderEnablementConflict(conflict);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingProvider.value) {
|
||||
await adminAPI.payment.updateProvider(editingProvider.value.id, payload);
|
||||
} else {
|
||||
@@ -6647,6 +6558,20 @@ async function handleToggleField(
|
||||
else if (field === "refund_enabled") newValue = !provider.refund_enabled;
|
||||
else newValue = !provider.allow_user_refund;
|
||||
|
||||
if (field === "enabled" && newValue) {
|
||||
const conflict = findProviderEnablementConflict({
|
||||
id: provider.id,
|
||||
provider_key: provider.provider_key,
|
||||
supported_types: provider.supported_types,
|
||||
enabled: true,
|
||||
name: provider.name,
|
||||
});
|
||||
if (conflict) {
|
||||
showProviderEnablementConflict(conflict);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload: Record<string, boolean> = { [field]: newValue };
|
||||
// Cascade: turning off refund_enabled also turns off allow_user_refund
|
||||
if (field === "refund_enabled" && !newValue) {
|
||||
@@ -6654,13 +6579,7 @@ async function handleToggleField(
|
||||
}
|
||||
try {
|
||||
await adminAPI.payment.updateProvider(provider.id, payload);
|
||||
if (field === "enabled") provider.enabled = newValue;
|
||||
else if (field === "refund_enabled") {
|
||||
provider.refund_enabled = newValue;
|
||||
if (!newValue) provider.allow_user_refund = false;
|
||||
} else {
|
||||
provider.allow_user_refund = newValue;
|
||||
}
|
||||
await loadProviders();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(err, t("common.error"), paymentErrorMap.value),
|
||||
@@ -6672,11 +6591,22 @@ async function handleToggleType(provider: ProviderInstance, type: string) {
|
||||
const updated = provider.supported_types.includes(type)
|
||||
? provider.supported_types.filter((t) => t !== type)
|
||||
: [...provider.supported_types, type];
|
||||
const conflict = findProviderEnablementConflict({
|
||||
id: provider.id,
|
||||
provider_key: provider.provider_key,
|
||||
supported_types: updated,
|
||||
enabled: provider.enabled,
|
||||
name: provider.name,
|
||||
});
|
||||
if (conflict) {
|
||||
showProviderEnablementConflict(conflict);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await adminAPI.payment.updateProvider(provider.id, {
|
||||
supported_types: updated,
|
||||
} as any);
|
||||
provider.supported_types = updated;
|
||||
await loadProviders();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(err, t("common.error"), paymentErrorMap.value),
|
||||
@@ -6700,11 +6630,7 @@ async function handleReorderProviders(
|
||||
} as Partial<ProviderInstance>),
|
||||
),
|
||||
);
|
||||
// Update local state to match new order
|
||||
for (const u of updates) {
|
||||
const p = providers.value.find((p) => p.id === u.id);
|
||||
if (p) p.sort_order = u.sort_order;
|
||||
}
|
||||
await loadProviders();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||
loadProviders();
|
||||
|
||||
Reference in New Issue
Block a user