89fd333702
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>
240 lines
7.8 KiB
TypeScript
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>
|
|
);
|
|
}
|