diff --git a/package.json b/package.json index fcc9c96..34efc4a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "dependencies": { "@prisma/adapter-pg": "7.4.1", "@prisma/client": "^7.4.2", - "@stripe/react-stripe-js": "^5.6.1", "@stripe/stripe-js": "^8.9.0", "next": "16.1.6", "pg": "^8.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12696ff..db97f81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@prisma/client': specifier: ^7.4.2 version: 7.4.2(prisma@7.4.1(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3) - '@stripe/react-stripe-js': - specifier: ^5.6.1 - version: 5.6.1(@stripe/stripe-js@8.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@stripe/stripe-js': specifier: ^8.9.0 version: 8.9.0 @@ -880,13 +877,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@stripe/react-stripe-js@5.6.1': - resolution: {integrity: sha512-5xBrjkGmFvKvpMod6VvpOaFaa67eRbmieKeFTePZyOr/sUXzm7A3YY91l330pS0usUst5PxTZDUZHWfOc0v1GA==} - peerDependencies: - '@stripe/stripe-js': '>=8.0.0 <9.0.0' - react: '>=16.8.0 <20.0.0' - react-dom: '>=16.8.0 <20.0.0' - '@stripe/stripe-js@8.9.0': resolution: {integrity: sha512-OJkXvUI5GAc56QdiSRimQDvWYEqn475J+oj8RzRtFTCPtkJNO2TWW619oDY+nn1ExR+2tCVTQuRQBbR4dRugww==} engines: {node: '>=12.16'} @@ -3630,13 +3620,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stripe/react-stripe-js@5.6.1(@stripe/stripe-js@8.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@stripe/stripe-js': 8.9.0 - prop-types: 15.8.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - '@stripe/stripe-js@8.9.0': {} '@swc/helpers@0.5.15': @@ -4428,8 +4411,8 @@ snapshots: '@next/eslint-plugin-next': 16.1.6 eslint: 9.39.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3(jiti@2.6.1)) @@ -4451,7 +4434,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4462,22 +4445,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -4488,7 +4471,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/src/app/pay/stripe-popup/page.tsx b/src/app/pay/stripe-popup/page.tsx index 7c08955..6012307 100644 --- a/src/app/pay/stripe-popup/page.tsx +++ b/src/app/pay/stripe-popup/page.tsx @@ -3,22 +3,20 @@ import { useSearchParams } from 'next/navigation'; import { useEffect, useState, useCallback, Suspense } from 'react'; -// Methods that can be confirmed directly without Payment Element -const DIRECT_CONFIRM_METHODS: Record = { - alipay: 'confirmAlipayPayment', -}; - function StripePopupContent() { const searchParams = useSearchParams(); const orderId = searchParams.get('order_id') || ''; - const clientSecret = searchParams.get('client_secret') || ''; - const pk = searchParams.get('pk') || ''; const amount = parseFloat(searchParams.get('amount') || '0') || 0; const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light'; const method = searchParams.get('method') || ''; const isDark = theme === 'dark'; - const hasMissingParams = !orderId || !clientSecret || !pk; + const isAlipay = method === 'alipay'; + // Sensitive data received via postMessage from parent, NOT from URL + const [credentials, setCredentials] = useState<{ + clientSecret: string; + publishableKey: string; + } | null>(null); const [stripeLoaded, setStripeLoaded] = useState(false); const [stripeSubmitting, setStripeSubmitting] = useState(false); const [stripeError, setStripeError] = useState(''); @@ -28,8 +26,6 @@ function StripePopupContent() { elements: import('@stripe/stripe-js').StripeElements; } | null>(null); - const directConfirmMethod = DIRECT_CONFIRM_METHODS[method]; - const buildReturnUrl = useCallback(() => { const returnUrl = new URL(window.location.href); returnUrl.pathname = '/pay/result'; @@ -40,12 +36,32 @@ function StripePopupContent() { return returnUrl.toString(); }, [orderId]); - // Initialize Stripe and auto-confirm for direct methods (e.g. Alipay) + // Listen for credentials from parent window via postMessage useEffect(() => { - if (hasMissingParams) return; + const handler = (event: MessageEvent) => { + if (event.origin !== window.location.origin) return; + if (event.data?.type !== 'STRIPE_POPUP_INIT') return; + const { clientSecret, publishableKey } = event.data; + if (clientSecret && publishableKey) { + setCredentials({ clientSecret, publishableKey }); + } + }; + window.addEventListener('message', handler); + // Signal parent that popup is ready to receive data + if (window.opener) { + window.opener.postMessage({ type: 'STRIPE_POPUP_READY' }, window.location.origin); + } + return () => window.removeEventListener('message', handler); + }, []); + + // Initialize Stripe once credentials are received + useEffect(() => { + if (!credentials) return; let cancelled = false; + const { clientSecret, publishableKey } = credentials; + import('@stripe/stripe-js').then(({ loadStripe }) => { - loadStripe(pk).then((stripe) => { + loadStripe(publishableKey).then((stripe) => { if (cancelled || !stripe) { if (!cancelled) { setStripeError('支付组件加载失败,请关闭窗口重试'); @@ -54,21 +70,18 @@ function StripePopupContent() { return; } - if (directConfirmMethod) { - // Direct confirm (e.g. Alipay) — immediately redirect, no Payment Element needed - const confirmFn = (stripe as unknown as Record)[directConfirmMethod]; - if (typeof confirmFn === 'function') { - confirmFn.call(stripe, clientSecret, { - return_url: buildReturnUrl(), - }).then((result: { error?: { message?: string } }) => { - if (cancelled) return; - if (result.error) { - setStripeError(result.error.message || '支付失败,请重试'); - setStripeLoaded(true); - } - // If no error, the page has already been redirected - }); - } + if (isAlipay) { + // Alipay: confirm directly and redirect, no Payment Element needed + stripe.confirmAlipayPayment(clientSecret, { + return_url: buildReturnUrl(), + }).then((result) => { + if (cancelled) return; + if (result.error) { + setStripeError(result.error.message || '支付失败,请重试'); + setStripeLoaded(true); + } + // If no error, the page has already been redirected + }); return; } @@ -85,9 +98,9 @@ function StripePopupContent() { }); }); return () => { cancelled = true; }; - }, [pk, clientSecret, isDark, directConfirmMethod, hasMissingParams, buildReturnUrl]); + }, [credentials, isDark, isAlipay, buildReturnUrl]); - // Mount Payment Element (only for non-direct methods) + // Mount Payment Element (only for non-alipay methods) const stripeContainerRef = useCallback( (node: HTMLDivElement | null) => { if (!node || !stripeLib) return; @@ -134,20 +147,24 @@ function StripePopupContent() { return () => clearTimeout(timer); }, [stripeSuccess]); - // Missing params — show error (after all hooks) - if (hasMissingParams) { + // Waiting for credentials from parent + if (!credentials) { return ( -
-
-

参数缺失

-

请从支付页面正常打开此窗口

+
+
+
+
+ + 正在初始化... + +
); } - // For direct confirm methods, show a loading/redirecting state - if (directConfirmMethod) { + // Alipay direct confirm: show loading/redirecting state + if (isAlipay) { return (
diff --git a/src/components/PaymentQRCode.tsx b/src/components/PaymentQRCode.tsx index 76adc68..4dea432 100644 --- a/src/components/PaymentQRCode.tsx +++ b/src/components/PaymentQRCode.tsx @@ -177,6 +177,7 @@ export default function PaymentQRCode({ const { stripe, elements } = stripeLib; const returnUrl = new URL(window.location.href); returnUrl.pathname = '/pay/result'; + returnUrl.search = ''; returnUrl.searchParams.set('order_id', orderId); returnUrl.searchParams.set('status', 'success'); @@ -202,12 +203,11 @@ export default function PaymentQRCode({ const handleOpenPopup = () => { if (!clientSecret || !stripePublishableKey) return; setPopupBlocked(false); + // Only pass display params in URL — sensitive data sent via postMessage const popupUrl = new URL(window.location.href); popupUrl.pathname = '/pay/stripe-popup'; popupUrl.search = ''; popupUrl.searchParams.set('order_id', orderId); - popupUrl.searchParams.set('client_secret', clientSecret); - popupUrl.searchParams.set('pk', stripePublishableKey); popupUrl.searchParams.set('amount', String(amount)); popupUrl.searchParams.set('theme', dark ? 'dark' : 'light'); popupUrl.searchParams.set('method', stripePaymentMethod); @@ -219,7 +219,19 @@ export default function PaymentQRCode({ ); if (!popup || popup.closed) { setPopupBlocked(true); + return; } + // Send sensitive data via postMessage after popup loads + const onReady = (event: MessageEvent) => { + if (event.source !== popup || event.data?.type !== 'STRIPE_POPUP_READY') return; + window.removeEventListener('message', onReady); + popup.postMessage({ + type: 'STRIPE_POPUP_INIT', + clientSecret, + publishableKey: stripePublishableKey, + }, window.location.origin); + }; + window.addEventListener('message', onReady); }; useEffect(() => {