Files
dukang/apps/h5-user/src/pages/PayPage.tsx
T
jacy 89fd333702 feat(analytics): persona logging upgrade and Sentry system config
Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 22:30:02 +08:00

240 lines
7.8 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import type { WechatPayOrderResult } from '@dukang/shared-types';
import SubPageHeader from '../components/SubPageHeader';
import PhoneVerifySheet from '../components/PhoneVerifySheet';
import { request } from '../lib/api';
import { buildOrderConfirmUrl } from '../lib/navigation';
import {
authorizeWechatForPay,
fetchClientConfig,
fetchUserProfile,
isWechatAuthRequiredError,
needsWechatAuthForPay,
saveWechatLoginResult,
} from '../lib/pay-wechat';
import { applyWechatLoginResult, handleWechatAuthCallback } from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin';
import { usePageView } from '../lib/usePageView';
import { track } from '../lib/analytics';
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitOrderPaid(orderId: string, maxAttempts = 15) {
for (let i = 0; i < maxAttempts; i += 1) {
const order = await request<{ payStatus?: string }>('USER_H5', `/trade/orders/${orderId}`);
if (order.payStatus === 'PAID') return true;
await sleep(2000);
}
return false;
}
export default function PayPage() {
const [params] = useSearchParams();
const orderId = params.get('orderId') || '';
usePageView('pay_page_view', { orderId });
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [authLoading, setAuthLoading] = useState(false);
const [mockMode, setMockMode] = useState(true);
const [needsWechatAuth, setNeedsWechatAuth] = useState(false);
const [msg, setMsg] = useState('');
const [showBindPhone, setShowBindPhone] = useState(false);
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
const [orderNo, setOrderNo] = useState('');
const [deliveryType, setDeliveryType] = useState('');
const refreshPayReadiness = useCallback(async () => {
try {
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
setMockMode(config.mockPay);
setNeedsWechatAuth(needsWechatAuthForPay(config, profile));
return profile;
} catch {
return null;
}
}, []);
const handleWechatLoginResult = useCallback(
async (result: Parameters<typeof applyWechatLoginResult>[0]) => {
if (result.needBindPhone && result.wxSessionKey) {
setWxSessionKey(result.wxSessionKey);
setShowBindPhone(true);
setMsg('');
return;
}
if (saveWechatLoginResult(result)) {
setMsg('');
await refreshPayReadiness();
}
},
[refreshPayReadiness],
);
useEffect(() => {
refreshPayReadiness();
}, [refreshPayReadiness]);
useEffect(() => {
if (!orderId) {
setOrderNo('');
return;
}
request<{ orderNo?: string; deliveryType?: string }>('USER_H5', `/trade/orders/${orderId}`)
.then((order) => {
setOrderNo(order.orderNo || '');
setDeliveryType(order.deliveryType || '');
})
.catch(() => {
setOrderNo('');
setDeliveryType('');
});
}, [orderId]);
useEffect(() => {
if (!isWechatEnv()) return;
handleWechatAuthCallback()
.then((result) => {
if (result) void handleWechatLoginResult(result);
})
.catch((e) => setMsg(e instanceof Error ? e.message : '微信授权失败'));
}, [handleWechatLoginResult]);
function goBackConfirm() {
navigate(
buildOrderConfirmUrl({
productId: params.get('productId'),
qty: params.get('qty'),
addressId: params.get('addressId'),
cross: params.get('cross'),
}),
);
}
async function wechatAuthorize() {
setAuthLoading(true);
setMsg('');
try {
if (!isWechatEnv()) {
setMsg('请在微信内打开以授权微信支付');
return;
}
const result = await authorizeWechatForPay();
if (result) await handleWechatLoginResult(result);
} catch (e) {
setMsg(e instanceof Error ? e.message : '微信授权失败');
} finally {
setAuthLoading(false);
}
}
async function pay() {
if (needsWechatAuth) {
setMsg('请先完成微信授权后再支付');
return;
}
setLoading(true);
setMsg('');
try {
const result = await request<WechatPayOrderResult>('USER_H5', `/trade/orders/${orderId}/pay`, {
method: 'POST',
});
if (result.mode === 'jsapi' && result.prepay) {
setMockMode(false);
const { weixinSdk } = await import('../lib/weixin');
await weixinSdk.pay(result.prepay);
const paid = await waitOrderPaid(orderId);
if (!paid) {
alert('支付结果确认中,请稍后在订单列表查看');
}
navigate(
deliveryType === 'ON_SITE_PICKUP' ? '/orders?tab=completed' : '/orders?tab=paid',
);
return;
}
navigate(deliveryType === 'ON_SITE_PICKUP' ? '/orders?tab=completed' : '/orders?tab=paid');
} catch (e) {
if (isWechatAuthRequiredError(e)) {
setNeedsWechatAuth(true);
setMsg('微信支付需要先完成微信授权');
return;
}
setMsg(e instanceof Error ? e.message : '支付失败');
track('pay_fail', {
orderId,
failReason: e instanceof Error ? e.message : '支付失败',
pagePath: '/pay',
});
} finally {
setLoading(false);
}
}
return (
<div className="page-no-tab pay-page">
<SubPageHeader title="微信支付" onBack={goBackConfirm} />
<div className="pay-body sub-page-body">
<div className="pay-icon">
<span className="material-symbols-outlined">account_balance_wallet</span>
</div>
<p className="headline-lg text-primary">
{needsWechatAuth ? '授权微信后可支付' : mockMode ? (isWechatEnv() ? '微信支付' : 'Mock 微信支付') : '微信支付'}
</p>
<p className="text-muted body-md" style={{ marginTop: 8 }}>
{needsWechatAuth
? '使用微信支付前,需先授权微信账号以完成付款'
: mockMode
? 'preV1 Mock 模式:点击确认即完成;开启真实支付后将调起微信收银台'
: '请在微信内完成支付,支付成功后自动跳转'}
</p>
{needsWechatAuth && (
<div className="pay-wechat-auth-card">
<p className="pay-wechat-auth-title">尚未授权微信</p>
<p className="pay-wechat-auth-desc">授权后可安全调起微信支付,不会重复扣款</p>
<button
type="button"
className="login-wechat-btn pay-wechat-auth-btn"
disabled={authLoading}
onClick={wechatAuthorize}
>
<span className="material-symbols-outlined login-wechat-icon">chat</span>
<span>{authLoading ? '授权中...' : '微信一键授权'}</span>
</button>
</div>
)}
{msg && <p className="pay-wechat-auth-msg">{msg}</p>}
{orderNo && <p className="label-md text-muted" style={{ marginTop: 24 }}>订单号 {orderNo}</p>}
</div>
<div className="page-actions">
<button
type="button"
className="btn btn-primary btn-block"
disabled={loading || !orderId || needsWechatAuth}
onClick={pay}
>
{loading ? '支付中...' : needsWechatAuth ? '请先授权微信' : '确认支付'}
</button>
</div>
<PhoneVerifySheet
open={showBindPhone}
mode="wechat_bind_phone"
wxSessionKey={wxSessionKey ?? undefined}
onClose={() => {
setShowBindPhone(false);
setWxSessionKey(null);
}}
onSuccess={() => {
void refreshPayReadiness();
}}
/>
</div>
);
}