Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
b45ec4b56e
59
CHANGELOG.md
59
CHANGELOG.md
@ -19,6 +19,56 @@ This document records all notable changes to ShadCN Admin.
|
||||
---
|
||||
|
||||
|
||||
## [1.3.10](https://github.com/perfect-panel/frontend/compare/v1.3.9...v1.3.10) (2026-02-10)
|
||||
|
||||
### 🐛 Bug Fixes / 问题修复
|
||||
|
||||
* **admin:** persist reset-password Turnstile verify setting (Fixes [#10](https://github.com/perfect-panel/frontend/issues/10)) ([8831c9b](https://github.com/perfect-panel/frontend/commit/8831c9be9c847f9873634112ce614576325d6d3a))
|
||||
|
||||
## [1.3.9](https://github.com/perfect-panel/frontend/compare/v1.3.8...v1.3.9) (2026-02-08)
|
||||
|
||||
### 🐛 Bug Fixes / 问题修复
|
||||
|
||||
* **auth:** support OAuth callbacks with hash router (Fixes [#9](https://github.com/perfect-panel/frontend/issues/9)) ([fee44fa](https://github.com/perfect-panel/frontend/commit/fee44fa1b22bfc6077a758d2cf82cc32840dadd5))
|
||||
* **user:** mobile announcement close + payment method selection (Fixes [#8](https://github.com/perfect-panel/frontend/issues/8)) ([b32ba55](https://github.com/perfect-panel/frontend/commit/b32ba55ab8b0af3557a282ff02563d04335106f0))
|
||||
* **user:** satisfy biome check for oauth/payment UI ([6e3ef8a](https://github.com/perfect-panel/frontend/commit/6e3ef8ab7ed5903d50ec43ba60544fd88d779c3e))
|
||||
|
||||
## [1.3.8](https://github.com/perfect-panel/frontend/compare/v1.3.7...v1.3.8) (2026-02-06)
|
||||
|
||||
### 🐛 Bug Fixes / 问题修复
|
||||
|
||||
* **payment:** satisfy img size lint rule ([30a6967](https://github.com/perfect-panel/frontend/commit/30a6967187d21d38d602a92c03e631ebad931767))
|
||||
* **payment:** use native WeChat Pay QR data ([160e6da](https://github.com/perfect-panel/frontend/commit/160e6da3f66691580752c65063e93c038b7a5547))
|
||||
|
||||
## [1.3.7](https://github.com/perfect-panel/frontend/compare/v1.3.6...v1.3.7) (2026-01-27)
|
||||
|
||||
### 🐛 Bug Fixes / 问题修复
|
||||
|
||||
* **admin:** refresh user list after balance update ([7e1d574](https://github.com/perfect-panel/frontend/commit/7e1d5746d46dd9bf33b1971f71f809afcdba4bbe))
|
||||
* **build:** ensure version.lock directory exists + fix merge typo ([052a0a4](https://github.com/perfect-panel/frontend/commit/052a0a420921cffc09c81843d37081d05b4c4717))
|
||||
* **ci:** avoid void|Promise union for refetch ([6dd743b](https://github.com/perfect-panel/frontend/commit/6dd743bbf4f383249e97549ed0ed0417b6f5609d))
|
||||
* **i18n:** remove duplicate locale keys ([ed3f6cb](https://github.com/perfect-panel/frontend/commit/ed3f6cb737e0e5b3b57e4149fce3be6e46b0c5eb))
|
||||
|
||||
### 🔧 Chores / 其他变更
|
||||
|
||||
* **openapi:** regenerate services & patch axios requestType typing ([0ec4f84](https://github.com/perfect-panel/frontend/commit/0ec4f84fa9d4d94048b8219896c3d8fb1d8d2175))
|
||||
* **release:** Release 1.2.4-dev.2 / 发布版本 1.2.4-dev.2 [skip ci] ([98676fa](https://github.com/perfect-panel/frontend/commit/98676fa27c74eefc9c0fe2d92a549d9e5d7d7fd6))
|
||||
* **release:** Release 1.3.7-dev.1 / 发布版本 1.3.7-dev.1 [skip ci] ([852b2b8](https://github.com/perfect-panel/frontend/commit/852b2b84ee926a2dc35a7c8380bf41a74ed8bbbf))
|
||||
|
||||
## [1.3.7-dev.1](https://github.com/perfect-panel/frontend/compare/v1.3.6...v1.3.7-dev.1) (2026-01-27)
|
||||
|
||||
### 🐛 Bug Fixes / 问题修复
|
||||
|
||||
* **admin:** refresh user list after balance update ([7e1d574](https://github.com/perfect-panel/frontend/commit/7e1d5746d46dd9bf33b1971f71f809afcdba4bbe))
|
||||
* **build:** ensure version.lock directory exists + fix merge typo ([052a0a4](https://github.com/perfect-panel/frontend/commit/052a0a420921cffc09c81843d37081d05b4c4717))
|
||||
* **ci:** avoid void|Promise union for refetch ([6dd743b](https://github.com/perfect-panel/frontend/commit/6dd743bbf4f383249e97549ed0ed0417b6f5609d))
|
||||
* **i18n:** remove duplicate locale keys ([ed3f6cb](https://github.com/perfect-panel/frontend/commit/ed3f6cb737e0e5b3b57e4149fce3be6e46b0c5eb))
|
||||
|
||||
### 🔧 Chores / 其他变更
|
||||
|
||||
* **openapi:** regenerate services & patch axios requestType typing ([0ec4f84](https://github.com/perfect-panel/frontend/commit/0ec4f84fa9d4d94048b8219896c3d8fb1d8d2175))
|
||||
* **release:** Release 1.2.4-dev.2 / 发布版本 1.2.4-dev.2 [skip ci] ([98676fa](https://github.com/perfect-panel/frontend/commit/98676fa27c74eefc9c0fe2d92a549d9e5d7d7fd6))
|
||||
|
||||
## [1.3.6](https://github.com/perfect-panel/frontend/compare/v1.3.5...v1.3.6) (2025-12-30)
|
||||
|
||||
### 🐛 Bug Fixes / 问题修复
|
||||
@ -92,13 +142,20 @@ This document records all notable changes to ShadCN Admin.
|
||||
* Remove the system log dialog component from the system version card ([71cb827](https://github.com/perfect-panel/frontend/commit/71cb827918ee3250f0c9d06d46d876ce6799b8ac))
|
||||
* Update invite link format in auth forms and sidebar to include hash fragment for routing. ([7a8c010](https://github.com/perfect-panel/frontend/commit/7a8c0102958a859c9e7476810d5c9b822f882692))
|
||||
|
||||
## [1.2.4-dev.2](https://github.com/perfect-panel/frontend/compare/v1.2.4-dev.1...v1.2.4-dev.2) (2026-01-27)
|
||||
|
||||
### 🐛 Bug Fixes / 问题修复
|
||||
|
||||
* **admin:** refresh user list after balance update ([7e1d574](https://github.com/perfect-panel/frontend/commit/7e1d5746d46dd9bf33b1971f71f809afcdba4bbe))
|
||||
* **ci:** avoid void|Promise union for refetch ([6dd743b](https://github.com/perfect-panel/frontend/commit/6dd743bbf4f383249e97549ed0ed0417b6f5609d))
|
||||
|
||||
### 📚 Documentation / 文档更新
|
||||
|
||||
* Add one-click installation script for PPanel with Docker support ([912c5c4](https://github.com/perfect-panel/frontend/commit/912c5c4cb63eeb0ecbc33bef6b31bd50d83d6491))
|
||||
|
||||
### 🔧 Chores / 其他变更
|
||||
|
||||
* **release:** Release 1.2.4-dev.1 / 发布版本 1.2.4-dev.1 [skip ci] ([62d45bb](https://github.com/perfect-panel/frontend/commit/62d45bbac17fab9656c1cce029a379ce8bb757d6))
|
||||
* **openapi:** regenerate services & patch axios requestType typing ([0ec4f84](https://github.com/perfect-panel/frontend/commit/0ec4f84fa9d4d94048b8219896c3d8fb1d8d2175))
|
||||
|
||||
## [1.2.4-dev.1](https://github.com/perfect-panel/frontend/compare/v1.2.3...v1.2.4-dev.1) (2025-12-22)
|
||||
|
||||
|
||||
@ -73,6 +73,14 @@
|
||||
"referrerUserId": "Referrer User ID",
|
||||
"remove": "Remove",
|
||||
"resetLogs": "Reset Logs",
|
||||
"resetTraffic": "Reset Traffic",
|
||||
"toggleStatus": "Toggle Status",
|
||||
"resetSubscriptionToken": "Reset Token",
|
||||
"resetSubscriptionTokenDescription": "This will reset the subscription token. Old links will become invalid.",
|
||||
"resetSubscriptionTraffic": "Reset Traffic",
|
||||
"resetSubscriptionTrafficDescription": "This will reset the subscription traffic counters.",
|
||||
"toggleSubscriptionStatus": "Toggle Status",
|
||||
"toggleSubscriptionStatusDescription": "This will toggle the subscription status.",
|
||||
"resetTime": "Reset Time",
|
||||
"resetToken": "Reset Subscription Address",
|
||||
"resetTokenDescription": "This will reset the subscription address and regenerate a new token.",
|
||||
|
||||
@ -73,6 +73,14 @@
|
||||
"referrerUserId": "推荐人用户 ID",
|
||||
"remove": "移除",
|
||||
"resetLogs": "重置日志",
|
||||
"resetTraffic": "重置流量",
|
||||
"toggleStatus": "切换状态",
|
||||
"resetSubscriptionToken": "重置令牌",
|
||||
"resetSubscriptionTokenDescription": "将重置订阅令牌,旧订阅链接会失效。",
|
||||
"resetSubscriptionTraffic": "重置流量",
|
||||
"resetSubscriptionTrafficDescription": "将重置该订阅的流量统计。",
|
||||
"toggleSubscriptionStatus": "切换状态",
|
||||
"toggleSubscriptionStatusDescription": "将切换该订阅的启用/停用状态。",
|
||||
"resetTime": "重置时间",
|
||||
"resetToken": "重置订阅地址",
|
||||
"resetTokenDescription": "这将重置订阅地址并重新生成新的令牌。",
|
||||
|
||||
@ -77,6 +77,7 @@ const defaultValues = {
|
||||
purchase_with_discount: false,
|
||||
reset_cycle: 0,
|
||||
renewal_reset: false,
|
||||
show_original_price: false,
|
||||
deduction_mode: "auto",
|
||||
};
|
||||
|
||||
@ -837,6 +838,41 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="show_original_price"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
{t(
|
||||
"form.showOriginalPrice",
|
||||
"Show Original Price"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"form.showOriginalPriceDescription",
|
||||
"Display original price in the storefront"
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={!!field.value}
|
||||
onCheckedChange={(value) =>
|
||||
form.setValue(field.name, value)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allow_deduction"
|
||||
|
||||
@ -37,7 +37,7 @@ const verifySchema = z.object({
|
||||
turnstile_secret: z.string().optional(),
|
||||
enable_register_verify: z.boolean().optional(),
|
||||
enable_login_verify: z.boolean().optional(),
|
||||
enable_password_verify: z.boolean().optional(),
|
||||
enable_reset_password_verify: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type VerifyFormData = z.infer<typeof verifySchema>;
|
||||
@ -63,7 +63,7 @@ export default function VerifyConfig() {
|
||||
turnstile_secret: "",
|
||||
enable_register_verify: false,
|
||||
enable_login_verify: false,
|
||||
enable_password_verify: false,
|
||||
enable_reset_password_verify: false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -243,7 +243,7 @@ export default function VerifyConfig() {
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enable_password_verify"
|
||||
name="enable_reset_password_verify"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
|
||||
@ -69,7 +69,11 @@ export default function User() {
|
||||
action={ref}
|
||||
actions={{
|
||||
render: (row) => [
|
||||
<ProfileSheet key="profile" userId={row.id} />,
|
||||
<ProfileSheet
|
||||
key="profile"
|
||||
onUpdated={() => ref.current?.refresh()}
|
||||
userId={row.id}
|
||||
/>,
|
||||
<SubscriptionSheet key="subscription" userId={row.id} />,
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
@ -308,7 +312,13 @@ export default function User() {
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileSheet({ userId }: { userId: number }) {
|
||||
function ProfileSheet({
|
||||
userId,
|
||||
onUpdated,
|
||||
}: {
|
||||
userId: number;
|
||||
onUpdated?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("user");
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: user, refetch } = useQuery({
|
||||
@ -319,6 +329,12 @@ function ProfileSheet({ userId }: { userId: number }) {
|
||||
return data.data as API.User;
|
||||
},
|
||||
});
|
||||
|
||||
const refetchAll = async () => {
|
||||
await refetch();
|
||||
onUpdated?.();
|
||||
return Promise.resolve();
|
||||
};
|
||||
return (
|
||||
<Sheet onOpenChange={setOpen} open={open}>
|
||||
<SheetTrigger asChild>
|
||||
@ -348,13 +364,13 @@ function ProfileSheet({ userId }: { userId: number }) {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="mt-0" value="basic">
|
||||
<BasicInfoForm refetch={refetch as any} user={user} />
|
||||
<BasicInfoForm refetch={refetchAll} user={user} />
|
||||
</TabsContent>
|
||||
<TabsContent className="mt-0" value="notify">
|
||||
<NotifySettingsForm refetch={refetch as any} user={user} />
|
||||
<NotifySettingsForm refetch={refetchAll} user={user} />
|
||||
</TabsContent>
|
||||
<TabsContent className="mt-0" value="auth">
|
||||
<AuthMethodsForm refetch={refetch as any} user={user} />
|
||||
<AuthMethodsForm refetch={refetchAll} user={user} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
|
||||
@ -21,7 +21,7 @@ export function AuthMethodsForm({
|
||||
refetch,
|
||||
}: {
|
||||
user: API.User;
|
||||
refetch: () => void;
|
||||
refetch: () => Promise<unknown>;
|
||||
}) {
|
||||
const { t } = useTranslation("user");
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ export function BasicInfoForm({
|
||||
refetch,
|
||||
}: {
|
||||
user: API.User;
|
||||
refetch: () => void;
|
||||
refetch: () => Promise<unknown>;
|
||||
}) {
|
||||
const { t } = useTranslation("user");
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ export function NotifySettingsForm({
|
||||
refetch,
|
||||
}: {
|
||||
user: API.User;
|
||||
refetch: () => void;
|
||||
refetch: () => Promise<unknown>;
|
||||
}) {
|
||||
const { t } = useTranslation("user");
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { devtools } from "@tanstack/devtools-vite";
|
||||
@ -18,6 +18,8 @@ function versionLockPlugin(): Plugin {
|
||||
);
|
||||
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
|
||||
const version = rootPkg.version || "0.0.0";
|
||||
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
writeFileSync(`${distDir}/version.lock`, version);
|
||||
},
|
||||
};
|
||||
|
||||
29
apps/user/public/oauth/google/index.html
Normal file
29
apps/user/public/oauth/google/index.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OAuth Redirect</title>
|
||||
<meta http-equiv="refresh" content="0; url=/#/auth">
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
// Providers redirect to a path without URL fragments (#). Our app uses hash routing.
|
||||
// Bridge /oauth/<provider>[/]?... -> /#/oauth/<provider>?...
|
||||
(() => {
|
||||
try {
|
||||
// Normalize trailing slash so /oauth/google/ and /oauth/google both map to the same route.
|
||||
let path = window.location.pathname || "/";
|
||||
path = path.replace(/\/$/, "");
|
||||
|
||||
const search = window.location.search || "";
|
||||
const target = `/#${path}${search}`;
|
||||
window.location.replace(target);
|
||||
} catch (_e) {
|
||||
window.location.replace("/#/auth");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>Redirecting…</body>
|
||||
</html>
|
||||
28
apps/user/public/oauth/telegram/index.html
Normal file
28
apps/user/public/oauth/telegram/index.html
Normal file
@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OAuth Redirect</title>
|
||||
<meta http-equiv="refresh" content="0; url=/#/auth">
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
// Providers redirect to a path without URL fragments (#). Our app uses hash routing.
|
||||
// Bridge /oauth/<provider>[/]?... -> /#/oauth/<provider>?...
|
||||
(() => {
|
||||
try {
|
||||
let path = window.location.pathname || "/";
|
||||
path = path.replace(/\/$/, "");
|
||||
|
||||
const search = window.location.search || "";
|
||||
const target = `/#${path}${search}`;
|
||||
window.location.replace(target);
|
||||
} catch (_e) {
|
||||
window.location.replace("/#/auth");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>Redirecting…</body>
|
||||
</html>
|
||||
@ -35,7 +35,10 @@ export function OAuthMethods() {
|
||||
onClick={async () => {
|
||||
const { data } = await oAuthLogin({
|
||||
method,
|
||||
redirect: `${window.location.origin}/oauth/${method}`,
|
||||
// OAuth providers disallow URL fragments (#) in redirect URIs.
|
||||
// Use a real path (with trailing slash so static hosting can serve /oauth/<provider>/index.html)
|
||||
// which then bridges into our hash-router at /#/oauth/<provider>.
|
||||
redirect: `${window.location.origin}/oauth/${method}/`,
|
||||
});
|
||||
if (data.data?.redirect) {
|
||||
window.location.href = data.data?.redirect;
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { getAvailablePaymentMethods } from "@workspace/ui/services/user/portal";
|
||||
import type React from "react";
|
||||
import { memo } from "react";
|
||||
import { memo, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface PaymentMethodsProps {
|
||||
@ -30,12 +30,21 @@ const PaymentMethods: React.FC<PaymentMethodsProps> = ({
|
||||
queryFn: async () => {
|
||||
const { data } = await getAvailablePaymentMethods();
|
||||
const list = data.data?.list || [];
|
||||
const methods = balance ? list : list.filter((item) => item.id !== -1);
|
||||
const defaultMethod = methods.find((item) => item.id)?.id;
|
||||
if (defaultMethod) onChange(defaultMethod);
|
||||
return methods;
|
||||
return balance ? list : list.filter((item) => item.id !== -1);
|
||||
},
|
||||
});
|
||||
|
||||
// Only set a default when the current value is not a valid option.
|
||||
// This avoids resetting the user's selection on refetch (common on mobile).
|
||||
// Prefer non-balance methods when possible.
|
||||
useEffect(() => {
|
||||
if (!data || data.length === 0) return;
|
||||
const valid = data.some((m) => String(m.id) === String(value));
|
||||
if (valid) return;
|
||||
|
||||
const preferred = data.find((m) => m.id !== -1)?.id ?? data[0]!.id;
|
||||
onChange(preferred);
|
||||
}, [data, onChange, value]);
|
||||
return (
|
||||
<>
|
||||
<div className="font-semibold">
|
||||
@ -44,7 +53,6 @@ const PaymentMethods: React.FC<PaymentMethodsProps> = ({
|
||||
<RadioGroup
|
||||
className="grid grid-cols-2 gap-2 md:grid-cols-5"
|
||||
onValueChange={(val) => {
|
||||
console.log(val);
|
||||
onChange(Number(val));
|
||||
}}
|
||||
value={String(value)}
|
||||
|
||||
@ -11,12 +11,14 @@ import Empty from "@workspace/ui/composed/empty";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import { Markdown } from "@workspace/ui/composed/markdown";
|
||||
import { queryAnnouncement } from "@workspace/ui/services/user/announcement";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
|
||||
export default function Announcement({ type }: { type: "popup" | "pinned" }) {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { user } = useGlobalStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ["announcement", type],
|
||||
@ -37,12 +39,16 @@ export default function Announcement({ type }: { type: "popup" | "pinned" }) {
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (type === "popup" && !!data) setOpen(true);
|
||||
}, [data, type]);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
if (type === "popup") {
|
||||
return (
|
||||
<Dialog defaultOpen={!!data}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<Dialog onOpenChange={setOpen} open={open}>
|
||||
<DialogContent className="max-h-[85vh] overflow-auto sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{data.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@ -276,6 +276,9 @@ const CheckoutForm: React.FC<Omit<StripePaymentProps, "publishable_key">> = ({
|
||||
const stripe = useStripe();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
||||
const [qrCodeImageDataUrl, setQrCodeImageDataUrl] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const { t } = useTranslation("payment");
|
||||
const qrCodeMap: Record<string, string> = {
|
||||
@ -328,12 +331,21 @@ const CheckoutForm: React.FC<Omit<StripePaymentProps, "publishable_key">> = ({
|
||||
|
||||
if (paymentIntent?.status === "requires_action") {
|
||||
const nextAction = paymentIntent.next_action as any;
|
||||
const qrUrl =
|
||||
method === "alipay"
|
||||
? nextAction?.alipay_handle_redirect?.url
|
||||
: nextAction?.wechat_pay_display_qr_code?.image_url_svg;
|
||||
// Stripe returns multiple WeChat QR-related fields.
|
||||
// For native WeChat pay experience we should prefer the protocol data (weixin://...).
|
||||
// Fallback to the provided base64 image if present.
|
||||
if (method === "alipay") {
|
||||
const qrUrl = nextAction?.alipay_handle_redirect?.url;
|
||||
setQrCodeUrl(qrUrl || null);
|
||||
setQrCodeImageDataUrl(null);
|
||||
} else {
|
||||
const wechat = nextAction?.wechat_pay_display_qr_code;
|
||||
const data = wechat?.data; // e.g. weixin://wxpay/bizpayurl?pr=...
|
||||
const imageDataUrl = wechat?.image_data_url; // data:image/png;base64,...
|
||||
|
||||
setQrCodeUrl(qrUrl || null);
|
||||
setQrCodeUrl(data || null);
|
||||
setQrCodeImageDataUrl(data ? null : imageDataUrl || null);
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
handleError(t("stripe.error", "An error occurred"));
|
||||
@ -348,18 +360,30 @@ const CheckoutForm: React.FC<Omit<StripePaymentProps, "publishable_key">> = ({
|
||||
<div className="min-w-80 text-left">
|
||||
<CardPaymentForm clientSecret={client_secret} onError={handleError} />
|
||||
</div>
|
||||
) : qrCodeUrl ? (
|
||||
) : qrCodeUrl || qrCodeImageDataUrl ? (
|
||||
<>
|
||||
<QRCodeCanvas
|
||||
imageSettings={{
|
||||
src: `./assets/payment/${method}.svg`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
excavate: true,
|
||||
}}
|
||||
size={208}
|
||||
value={qrCodeUrl}
|
||||
/>
|
||||
{qrCodeImageDataUrl ? (
|
||||
<img
|
||||
alt={
|
||||
qrCodeMap[method] || t(`qrcode.${method}`, `Scan with ${method}`)
|
||||
}
|
||||
className="mx-auto h-[208px] w-[208px]"
|
||||
height={208}
|
||||
src={qrCodeImageDataUrl}
|
||||
width={208}
|
||||
/>
|
||||
) : (
|
||||
<QRCodeCanvas
|
||||
imageSettings={{
|
||||
src: `./assets/payment/${method}.svg`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
excavate: true,
|
||||
}}
|
||||
size={208}
|
||||
value={qrCodeUrl!}
|
||||
/>
|
||||
)}
|
||||
<p className="mt-4 text-center text-muted-foreground">
|
||||
{qrCodeMap[method] || t(`qrcode.${method}`, `Scan with ${method}`)}
|
||||
</p>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.10",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/perfect-panel/frontend",
|
||||
"bugs": {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
@ -38,6 +39,7 @@ export async function appleLoginCallback(
|
||||
{
|
||||
method: "POST",
|
||||
data: formData,
|
||||
requestType: "form",
|
||||
...(options || {}),
|
||||
}
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import request from "@workspace/ui/lib/request";
|
||||
|
||||
|
||||
8
packages/ui/src/typings.d.ts
vendored
8
packages/ui/src/typings.d.ts
vendored
@ -8,3 +8,11 @@ declare global {
|
||||
i18n: typeof i18n;
|
||||
}
|
||||
}
|
||||
|
||||
// openapi2ts 生成的 request 参数里可能会包含 requestType(umi-request 风格)。
|
||||
// 我们的 request 基于 axios:这里做一个类型补丁,避免 TS 报错。
|
||||
declare module "axios" {
|
||||
export interface AxiosRequestConfig<D = any> {
|
||||
requestType?: string;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user