Files
Genarrative/src/components/platform-entry/usePlatformProfileCenterController.ts
T
menghao 62e0fe94ef
Project CI / Repository checks (push) Successful in 1m2s
Project CI / Frontend tests (push) Successful in 3m5s
Project CI / Backend tests (push) Successful in 3m41s
Project CI / Native shell tests (push) Successful in 13m7s
资源卡依赖关系及类型分类预览 (#129)
完成资源卡按类型和按依赖分类展现的功能

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/129
Co-authored-by: menghao <mh18530625731@163.com>
Co-committed-by: menghao <mh18530625731@163.com>
2026-08-05 19:15:46 +08:00

1140 lines
37 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
import {
type ConfirmWechatProfileRechargeOrderResponse,
type ProfileRechargeCenterResponse,
type ProfileRechargeOrder,
type ProfileRechargeProduct,
type ProfileReferralInviteCenterResponse,
type ProfileWalletLedgerResponse,
type RedeemProfileRewardCodeResponse,
} from '../../../packages/shared/src/contracts/runtime';
import { clearStoredAccessToken } from '../../services/apiClient';
import { type AuthUser, startWechatBind } from '../../services/authService';
import {
getHostRuntime,
requestHostLogin,
requestHostPayment,
} from '../../services/host-bridge/hostBridge';
import {
resolveProfileRechargeProductPaymentChannel,
WECHAT_H5_PAYMENT_CHANNEL,
WECHAT_JSAPI_PAYMENT_CHANNEL,
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL,
WECHAT_NATIVE_PAYMENT_CHANNEL,
} from '../../services/payment/paymentPlatform';
import { redirectToPaymentUrl } from '../../services/payment/paymentRedirect';
import { requestWechatJsapiPayment } from '../../services/payment/wechatJsapiPayment';
import {
confirmWechatPlatformProfileRechargeOrder,
createPlatformProfileRechargeOrder,
getPlatformProfileRechargeCenter,
getPlatformProfileReferralInviteCenter,
getPlatformProfileWalletLedger,
redeemPlatformProfileReferralInviteCode,
redeemPlatformProfileRewardCode,
watchWechatPlatformProfileRechargeOrder,
} from '../../services/platform-entry/platformProfileClient';
import {
type CopyFeedbackState,
useCopyFeedback,
} from '../common/useCopyFeedback';
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
const WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS = [800, 1600] as const;
const WECHAT_NATIVE_WATCH_RETRY_DELAY_MS = 1000;
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
const WECHAT_PAY_RESULT_RECHECK_INTERVAL_MS = 250;
const WECHAT_PAY_RESULT_RECHECK_TIMEOUT_MS = 10000;
const WECHAT_RECHARGE_UNSUPPORTED_DEVICE_TEXT = '当前登录设备不支持充值';
const WECHAT_JSAPI_MISSING_IDENTITY_TEXT = '缺少微信';
export type ProfileReferralPanel = 'invite' | 'redeem' | 'community';
export type ProfilePopupPanel = ProfileReferralPanel;
type WechatPayResult = {
requestId: string;
orderId: string | null;
status: 'success' | 'cancel' | 'fail';
errorMessage: string | null;
};
type RechargePaymentResultKind =
| 'success'
| 'pending'
| 'cancel'
| 'failed'
| 'expired';
export type RechargePaymentResult = {
kind: RechargePaymentResultKind;
title: string;
message: string;
};
export type WechatRechargeOrderConfirmationState = {
orderId: string;
};
export type NativeWechatPaymentState =
PlatformProfileRechargeNativePaymentState;
function isWechatJsapiMissingIdentityError(error: unknown) {
return (
error instanceof Error &&
error.message.includes(WECHAT_JSAPI_MISSING_IDENTITY_TEXT)
);
}
function isWechatRechargeUnsupportedDeviceError(error: unknown) {
return (
error instanceof Error &&
error.message.includes(WECHAT_RECHARGE_UNSUPPORTED_DEVICE_TEXT)
);
}
type UsePlatformProfileCenterControllerArgs = {
activeTab: string;
isAuthenticated: boolean;
showRechargeEntry: boolean;
onRechargeSuccess?: () => void | Promise<void>;
requestLogin: () => void;
currentUser: AuthUser | null | undefined;
};
function readProfileInviteCodeFromLocationSearch(search: string) {
const params = new URLSearchParams(search);
for (const key of PROFILE_INVITE_QUERY_KEYS) {
const value = (params.get(key) ?? '')
.trim()
.replace(/[^0-9a-z]/giu, '')
.toUpperCase();
if (value) {
return value;
}
}
return '';
}
function clearWechatPayResultHash() {
if (typeof window === 'undefined') {
return;
}
const rawHash = window.location.hash.replace(/^#/, '');
if (!rawHash.includes('wx_pay_result=')) {
return;
}
const params = new URLSearchParams(rawHash);
params.delete('wx_pay_result');
const nextHash = params.toString();
const nextUrl = `${window.location.pathname}${window.location.search}${nextHash ? `#${nextHash}` : ''}`;
window.history.replaceState(null, '', nextUrl);
}
function readWechatPayResultFromHash(): WechatPayResult | null {
if (typeof window === 'undefined') {
return null;
}
const result = new URLSearchParams(
window.location.hash.replace(/^#/, ''),
).get('wx_pay_result');
if (!result) {
return null;
}
const [requestId = '', rawStatus = '', explicitOrderId = '', ...rawErrors] =
result.split(':');
const inferredOrderId = requestId
.replace(/^wechat_pay_/, '')
.replace(/_\d+$/, '')
.trim();
const orderId = explicitOrderId.trim() || inferredOrderId;
const status =
rawStatus === 'success'
? 'success'
: rawStatus === 'cancel'
? 'cancel'
: 'fail';
let errorMessage: string | null = null;
const rawError = rawErrors.join(':');
if (rawError) {
try {
errorMessage = decodeURIComponent(rawError);
} catch (_error) {
errorMessage = rawError;
}
}
return {
requestId,
orderId: orderId || null,
status,
errorMessage,
};
}
function waitWechatPayConfirmDelay(delayMs: number) {
return new Promise<void>((resolve) => {
window.setTimeout(resolve, delayMs);
});
}
function isWechatRechargeOrderTerminalForConfirmation(
order: Pick<ProfileRechargeOrder, 'status' | 'expirationCheckedAt'>,
) {
if (order.status === 'pending') {
return false;
}
if (order.status === 'expired' && !order.expirationCheckedAt) {
return false;
}
return true;
}
async function confirmWechatRechargeOrderUntilSettled(
orderId: string,
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
for (const delayMs of WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
}
try {
const streamedResponse =
await watchWechatPlatformProfileRechargeOrder(orderId);
return streamedResponse;
} catch {
return latestResponse;
}
}
async function confirmWechatRechargeOrderQuickly(
orderId: string,
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
for (const delayMs of WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
}
return latestResponse;
}
function buildRechargePaymentResultForOrder(
order: Pick<ProfileRechargeOrder, 'status' | 'expirationCheckedAt'>,
): RechargePaymentResult {
switch (order.status) {
case 'paid':
return {
kind: 'success',
title: '支付成功',
message: '已到账,泥点余额已刷新。',
};
case 'expired':
if (!order.expirationCheckedAt) {
return {
kind: 'pending',
title: '支付处理中',
message: '正在等待到账状态确认,请稍后查看泥点余额。',
};
}
return {
kind: 'expired',
title: '支付已过期',
message: '订单已超过支付时限,本次没有入账。',
};
case 'closed':
return {
kind: 'cancel',
title: '支付未完成',
message: '本次没有扣款,泥点余额未发生变化。',
};
case 'failed':
case 'refunded':
return {
kind: 'failed',
title: '支付未完成',
message: '微信支付没有完成,本次不会入账。',
};
case 'pending':
default:
return {
kind: 'pending',
title: '支付处理中',
message: '正在等待到账状态确认,请稍后查看泥点余额。',
};
}
}
export function usePlatformProfileCenterController({
activeTab,
isAuthenticated,
showRechargeEntry,
onRechargeSuccess,
requestLogin,
currentUser,
}: UsePlatformProfileCenterControllerArgs) {
// 中文注释:个人中心里的充值、任务、邀请码、账单等账户商业能力统一由这个 controller 托管,
// 页面层只消费状态与回调,不再直接堆叠一大组本地 state / effect / callback。
const [isRewardCodeOpen, setIsRewardCodeOpen] = useState(false);
const [rewardCodeInput, setRewardCodeInput] = useState('');
const [isSubmittingRewardCode, setIsSubmittingRewardCode] = useState(false);
const [rewardCodeError, setRewardCodeError] = useState<string | null>(null);
const [rewardCodeSuccess, setRewardCodeSuccess] = useState<string | null>(
null,
);
const [isRechargeOpen, setIsRechargeOpen] = useState(false);
const [rechargeCenter, setRechargeCenter] =
useState<ProfileRechargeCenterResponse | null>(null);
const [isLoadingRechargeCenter, setIsLoadingRechargeCenter] = useState(false);
const [rechargeError, setRechargeError] = useState<string | null>(null);
const [rechargePaymentResult, setRechargePaymentResult] =
useState<RechargePaymentResult | null>(null);
const [
wechatRechargeOrderConfirmationState,
setWechatRechargeOrderConfirmationState,
] = useState<WechatRechargeOrderConfirmationState | null>(null);
const [nativeWechatPayment, setNativeWechatPayment] =
useState<NativeWechatPaymentState | null>(null);
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
useState<string | null>(null);
const [isWalletLedgerOpen, setIsWalletLedgerOpen] = useState(false);
const [walletLedger, setWalletLedger] =
useState<ProfileWalletLedgerResponse | null>(null);
const [walletLedgerError, setWalletLedgerError] = useState<string | null>(
null,
);
const [isLoadingWalletLedger, setIsLoadingWalletLedger] = useState(false);
const [profilePopupPanel, setProfilePopupPanel] =
useState<ProfilePopupPanel | null>(null);
const [referralCenter, setReferralCenter] =
useState<ProfileReferralInviteCenterResponse | null>(null);
const [isLoadingReferral, setIsLoadingReferral] = useState(false);
const [isReferralCenterInitialized, setIsReferralCenterInitialized] =
useState(false);
const pendingProfileInviteCode = useMemo(
() =>
typeof window === 'undefined'
? ''
: readProfileInviteCodeFromLocationSearch(window.location.search),
[],
);
const promptedLoginForInviteQueryRef = useRef(false);
const autoOpenedInviteQueryRef = useRef(false);
const [referralRedeemCode, setReferralRedeemCode] = useState(
pendingProfileInviteCode,
);
const [isSubmittingReferralRedeem, setIsSubmittingReferralRedeem] =
useState(false);
const [referralError, setReferralError] = useState<string | null>(null);
const [referralSuccess, setReferralSuccess] = useState<string | null>(null);
const { copyState: inviteCopyState, copyText: copyInviteText } =
useCopyFeedback();
const pendingWechatRechargeOrderIdRef = useRef<string | null>(null);
const confirmingWechatRechargeOrderIdRef = useRef<string | null>(null);
const rechargeCenterReadRevisionRef = useRef(0);
const rechargeCenterReadAbortControllerRef =
useRef<AbortController | null>(null);
useEffect(() => {
return () => {
rechargeCenterReadRevisionRef.current += 1;
rechargeCenterReadAbortControllerRef.current?.abort();
rechargeCenterReadAbortControllerRef.current = null;
};
}, []);
// 中文注释:支持带邀请码 query 的直达场景,登录成功后自动打开兑换面板并复用同一套输入状态。
useEffect(() => {
if (!pendingProfileInviteCode || autoOpenedInviteQueryRef.current) {
return;
}
if (!currentUser) {
if (!promptedLoginForInviteQueryRef.current) {
promptedLoginForInviteQueryRef.current = true;
requestLogin();
}
return;
}
autoOpenedInviteQueryRef.current = true;
setReferralRedeemCode(pendingProfileInviteCode);
setReferralError(null);
setReferralSuccess(null);
setProfilePopupPanel('redeem');
}, [currentUser, pendingProfileInviteCode, requestLogin]);
const loadWalletLedger = useCallback(() => {
setWalletLedgerError(null);
setIsLoadingWalletLedger(true);
void getPlatformProfileWalletLedger()
.then(setWalletLedger)
.catch((error: unknown) => {
setWalletLedger(null);
setWalletLedgerError(
error instanceof Error ? error.message : '读取泥点账单失败',
);
})
.finally(() => setIsLoadingWalletLedger(false));
}, []);
const openWalletLedgerPanel = useCallback(() => {
setIsWalletLedgerOpen(true);
loadWalletLedger();
}, [loadWalletLedger]);
const applyRechargeCenter = useCallback(
(center: ProfileRechargeCenterResponse) => {
rechargeCenterReadRevisionRef.current += 1;
rechargeCenterReadAbortControllerRef.current?.abort();
rechargeCenterReadAbortControllerRef.current = null;
setIsLoadingRechargeCenter(false);
setRechargeError(null);
setRechargeCenter(center);
},
[],
);
const loadRechargeCenter = useCallback(() => {
const revision = ++rechargeCenterReadRevisionRef.current;
rechargeCenterReadAbortControllerRef.current?.abort();
const abortController = new AbortController();
rechargeCenterReadAbortControllerRef.current = abortController;
setRechargeError(null);
setIsLoadingRechargeCenter(true);
void getPlatformProfileRechargeCenter({ signal: abortController.signal })
.then((center) => {
if (
!abortController.signal.aborted &&
revision === rechargeCenterReadRevisionRef.current
) {
setRechargeCenter(center);
}
})
.catch((error: unknown) => {
if (
abortController.signal.aborted ||
revision !== rechargeCenterReadRevisionRef.current
) {
return;
}
setRechargeCenter(null);
setRechargeError(
error instanceof Error ? error.message : '读取泥点购买信息失败',
);
})
.finally(() => {
if (rechargeCenterReadAbortControllerRef.current === abortController) {
rechargeCenterReadAbortControllerRef.current = null;
}
if (revision === rechargeCenterReadRevisionRef.current) {
setIsLoadingRechargeCenter(false);
}
});
}, []);
const refreshRechargeState = useCallback(() => {
loadRechargeCenter();
setSubmittingRechargeProductId(null);
pendingWechatRechargeOrderIdRef.current = null;
confirmingWechatRechargeOrderIdRef.current = null;
setWechatRechargeOrderConfirmationState(null);
setNativeWechatPayment(null);
}, [loadRechargeCenter]);
const handleWechatPayResult = useCallback(() => {
const payResult = readWechatPayResultFromHash();
if (!payResult) {
return false;
}
if (
pendingWechatRechargeOrderIdRef.current &&
payResult.orderId &&
payResult.orderId !== pendingWechatRechargeOrderIdRef.current
) {
return false;
}
if (payResult.status === 'success') {
const orderId =
payResult.orderId || pendingWechatRechargeOrderIdRef.current;
if (!orderId) {
clearWechatPayResultHash();
return true;
}
if (confirmingWechatRechargeOrderIdRef.current === orderId) {
clearWechatPayResultHash();
return true;
}
confirmingWechatRechargeOrderIdRef.current = orderId;
setWechatRechargeOrderConfirmationState({ orderId });
setSubmittingRechargeProductId(null);
setRechargePaymentResult(null);
void confirmWechatRechargeOrderUntilSettled(orderId)
.then((response) => {
const result = buildRechargePaymentResultForOrder(response.order);
const isPaid = result.kind === 'success';
applyRechargeCenter(response.center);
pendingWechatRechargeOrderIdRef.current = null;
confirmingWechatRechargeOrderIdRef.current = null;
setWechatRechargeOrderConfirmationState(null);
setRechargePaymentResult(result);
if (isPaid) {
void onRechargeSuccess?.();
}
clearWechatPayResultHash();
})
.catch(() => {
confirmingWechatRechargeOrderIdRef.current = null;
setWechatRechargeOrderConfirmationState(null);
setRechargePaymentResult({
kind: 'pending',
title: '支付处理中',
message: '暂时没能确认到账状态,请稍后查看泥点余额。',
});
clearWechatPayResultHash();
});
} else if (payResult.status === 'cancel') {
setRechargePaymentResult({
kind: 'cancel',
title: '支付已取消',
message: '本次没有扣款,泥点余额未发生变化。',
});
setWechatRechargeOrderConfirmationState(null);
refreshRechargeState();
} else {
const detail = payResult.errorMessage
? `微信返回:${payResult.errorMessage}`
: '微信支付没有完成,本次不会入账。';
setRechargePaymentResult({
kind: 'failed',
title: '支付未完成',
message: detail,
});
setWechatRechargeOrderConfirmationState(null);
refreshRechargeState();
}
clearWechatPayResultHash();
return true;
}, [applyRechargeCenter, onRechargeSuccess, refreshRechargeState]);
const pollWechatPayResultFromHash = useCallback(
() => handleWechatPayResult(),
[handleWechatPayResult],
);
const confirmPendingWechatRechargeOrder = useCallback(() => {
if (nativeWechatPayment) {
return false;
}
const orderId = pendingWechatRechargeOrderIdRef.current;
if (!orderId || confirmingWechatRechargeOrderIdRef.current === orderId) {
return false;
}
confirmingWechatRechargeOrderIdRef.current = orderId;
setWechatRechargeOrderConfirmationState({ orderId });
setRechargePaymentResult(null);
void confirmWechatRechargeOrderUntilSettled(orderId)
.then((response) => {
const result = buildRechargePaymentResultForOrder(response.order);
const isPaid = result.kind === 'success';
applyRechargeCenter(response.center);
pendingWechatRechargeOrderIdRef.current = null;
confirmingWechatRechargeOrderIdRef.current = null;
setWechatRechargeOrderConfirmationState(null);
setSubmittingRechargeProductId(null);
setRechargePaymentResult(result);
if (isPaid) {
void onRechargeSuccess?.();
}
})
.catch(() => {
confirmingWechatRechargeOrderIdRef.current = null;
setWechatRechargeOrderConfirmationState(null);
setRechargePaymentResult({
kind: 'pending',
title: '支付处理中',
message: '暂时没能确认到账状态,请稍后查看泥点余额。',
});
});
return true;
}, [applyRechargeCenter, nativeWechatPayment, onRechargeSuccess]);
const openRechargeModal = useCallback(() => {
if (!currentUser) {
requestLogin();
return;
}
setIsRechargeOpen(true);
loadRechargeCenter();
}, [currentUser, loadRechargeCenter, requestLogin]);
const openRewardCodeModal = useCallback(() => {
setIsRewardCodeOpen(true);
setRewardCodeError(null);
setRewardCodeSuccess(null);
}, []);
const openRechargeOrRewardCodeModal = useCallback(() => {
if (showRechargeEntry) {
openRechargeModal();
return;
}
openRewardCodeModal();
}, [openRechargeModal, openRewardCodeModal, showRechargeEntry]);
const closeNativeWechatPayment = useCallback(() => {
setNativeWechatPayment((current) => {
if (current?.isConfirming) {
return current;
}
pendingWechatRechargeOrderIdRef.current = null;
return null;
});
}, []);
const buyRechargeProduct = useCallback(
(product: ProfileRechargeProduct) => {
if (submittingRechargeProductId) {
return;
}
const paymentChannel = resolveProfileRechargeProductPaymentChannel(
{ kind: product.kind },
{},
);
setSubmittingRechargeProductId(product.productId);
setRechargeError(null);
setRechargePaymentResult(null);
setWechatRechargeOrderConfirmationState(null);
setNativeWechatPayment(null);
void createPlatformProfileRechargeOrder(product.productId, paymentChannel)
.then(async (response) => {
if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) {
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
applyRechargeCenter(response.center);
const paymentHandled = await requestHostPayment({
payload: response.wechatMiniProgramPayParams,
orderId: response.order.orderId,
});
if (!paymentHandled) {
throw new Error('请在微信小程序内完成支付');
}
return;
}
if (paymentChannel === WECHAT_JSAPI_PAYMENT_CHANNEL) {
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
applyRechargeCenter(response.center);
setRechargePaymentResult({
kind: 'pending',
title: '正在打开微信支付',
message: '请在微信支付面板中完成支付。',
});
await requestWechatJsapiPayment(
response.wechatMiniProgramPayParams,
);
setRechargePaymentResult({
kind: 'pending',
title: '支付处理中',
message: '正在查询微信支付到账状态。',
});
void confirmWechatRechargeOrderUntilSettled(response.order.orderId)
.then((confirmResponse) => {
const result = buildRechargePaymentResultForOrder(
confirmResponse.order,
);
const isPaid = result.kind === 'success';
applyRechargeCenter(confirmResponse.center);
setRechargePaymentResult(result);
if (result.kind !== 'pending') {
pendingWechatRechargeOrderIdRef.current = null;
}
if (isPaid) {
void onRechargeSuccess?.();
}
})
.catch(() => {
setRechargePaymentResult({
kind: 'pending',
title: '等待微信确认',
message: '暂时没能确认到账状态,请稍后再试。',
});
});
return;
}
if (paymentChannel === WECHAT_H5_PAYMENT_CHANNEL) {
const h5Url = response.wechatH5Payment?.h5Url?.trim();
if (!h5Url) {
throw new Error('微信 H5 支付链接生成失败');
}
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
applyRechargeCenter(response.center);
setRechargePaymentResult({
kind: 'pending',
title: '正在打开微信支付',
message: '完成支付后返回页面确认到账状态。',
});
await redirectToPaymentUrl(h5Url);
return;
}
if (paymentChannel === WECHAT_NATIVE_PAYMENT_CHANNEL) {
const wechatNativePayment = response.wechatNativePayment;
const codeUrl = wechatNativePayment?.codeUrl?.trim();
const expiresAt = wechatNativePayment?.expiresAt?.trim();
if (!wechatNativePayment || !codeUrl || !expiresAt) {
throw new Error('微信 Native 支付链接生成失败');
}
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
applyRechargeCenter(response.center);
setNativeWechatPayment({
...wechatNativePayment,
codeUrl,
expiresAt,
orderId: response.order.orderId,
productTitle: response.order.productTitle,
amountCents: response.order.amountCents,
isConfirming: false,
});
setSubmittingRechargeProductId(null);
return;
}
throw new Error('充值支付渠道无效');
})
.catch((error: unknown) => {
pendingWechatRechargeOrderIdRef.current = null;
setNativeWechatPayment(null);
if (
paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL &&
getHostRuntime().kind === 'wechat_mini_program' &&
isWechatRechargeUnsupportedDeviceError(error)
) {
clearStoredAccessToken();
setSubmittingRechargeProductId(null);
setRechargeError(null);
setRechargePaymentResult({
kind: 'pending',
title: '需要重新登录',
message: '正在打开微信小程序登录,请重新登录后再支付。',
});
void requestHostLogin().catch((miniProgramLoginError: unknown) => {
setRechargePaymentResult(null);
setRechargeError(
miniProgramLoginError instanceof Error
? miniProgramLoginError.message
: '请在微信小程序内重新登录后再支付',
);
});
return;
}
if (
paymentChannel === WECHAT_JSAPI_PAYMENT_CHANNEL &&
isWechatJsapiMissingIdentityError(error)
) {
setSubmittingRechargeProductId(null);
setRechargePaymentResult({
kind: 'pending',
title: '需要微信授权',
message: '正在跳转微信授权,授权后请重新发起支付。',
});
void startWechatBind().catch((wechatLoginError: unknown) => {
setRechargePaymentResult(null);
setRechargeError(
wechatLoginError instanceof Error
? wechatLoginError.message
: '微信授权暂不可用,请稍后再试',
);
});
return;
}
setRechargeError(error instanceof Error ? error.message : '充值失败');
setSubmittingRechargeProductId(null);
});
},
[applyRechargeCenter, onRechargeSuccess, submittingRechargeProductId],
);
const confirmNativeWechatPayment = useCallback(() => {
if (!nativeWechatPayment || nativeWechatPayment.isConfirming) {
return;
}
setNativeWechatPayment((current) =>
current && current.orderId === nativeWechatPayment.orderId
? { ...current, isConfirming: true, confirmMessage: undefined }
: current,
);
void confirmWechatRechargeOrderQuickly(nativeWechatPayment.orderId)
.then((response) => {
if (
pendingWechatRechargeOrderIdRef.current !==
nativeWechatPayment.orderId
) {
return;
}
const result = buildRechargePaymentResultForOrder(response.order);
const isPaid = result.kind === 'success';
applyRechargeCenter(response.center);
if (result.kind !== 'pending') {
setNativeWechatPayment(null);
pendingWechatRechargeOrderIdRef.current = null;
setRechargePaymentResult(result);
if (isPaid) {
void onRechargeSuccess?.();
}
} else {
setNativeWechatPayment((current) =>
current && current.orderId === nativeWechatPayment.orderId
? {
...current,
isConfirming: false,
confirmMessage: '暂未确认到账,请确认付款完成后再点一次。',
}
: current,
);
}
})
.catch(() => {
setNativeWechatPayment((current) =>
current && current.orderId === nativeWechatPayment.orderId
? {
...current,
isConfirming: false,
confirmMessage: '暂时没能确认到账状态,请稍后再试。',
}
: current,
);
})
.finally(() => setSubmittingRechargeProductId(null));
}, [applyRechargeCenter, nativeWechatPayment, onRechargeSuccess]);
useEffect(() => {
const orderId = nativeWechatPayment?.orderId;
const expiresAtMs = Date.parse(nativeWechatPayment?.expiresAt ?? '');
if (!orderId || !Number.isFinite(expiresAtMs)) {
return undefined;
}
let cancelled = false;
const abortController = new AbortController();
const watchUntilSettled = async () => {
while (!cancelled && Date.now() < expiresAtMs) {
try {
const response = await watchWechatPlatformProfileRechargeOrder(
orderId,
{
signal: abortController.signal,
},
);
if (
cancelled ||
!response ||
pendingWechatRechargeOrderIdRef.current !== orderId
) {
return;
}
const result = buildRechargePaymentResultForOrder(response.order);
applyRechargeCenter(response.center);
if (result.kind === 'pending') {
await waitWechatPayConfirmDelay(WECHAT_NATIVE_WATCH_RETRY_DELAY_MS);
continue;
}
pendingWechatRechargeOrderIdRef.current = null;
if (confirmingWechatRechargeOrderIdRef.current === orderId) {
confirmingWechatRechargeOrderIdRef.current = null;
}
setNativeWechatPayment((current) =>
current?.orderId === orderId ? null : current,
);
setSubmittingRechargeProductId(null);
setRechargePaymentResult(result);
if (result.kind === 'success') {
void onRechargeSuccess?.();
}
return;
} catch {
if (cancelled || abortController.signal.aborted) {
return;
}
}
await waitWechatPayConfirmDelay(WECHAT_NATIVE_WATCH_RETRY_DELAY_MS);
}
};
void watchUntilSettled();
return () => {
cancelled = true;
abortController.abort();
};
}, [
nativeWechatPayment?.expiresAt,
nativeWechatPayment?.orderId,
applyRechargeCenter,
onRechargeSuccess,
]);
// 中文注释:H5 / 小程序支付返回页、页面恢复和 hash 轮询都统一走同一套到账确认逻辑,
// 避免页面组件自己感知微信支付细节。
useEffect(() => {
const handleHashChange = () => {
handleWechatPayResult();
};
const handleResume = () => {
if (
typeof document !== 'undefined' &&
document.visibilityState === 'hidden'
) {
return;
}
if (!handleWechatPayResult()) {
confirmPendingWechatRechargeOrder();
}
};
window.addEventListener('hashchange', handleHashChange);
window.addEventListener('focus', handleResume);
window.addEventListener('pageshow', handleResume);
document.addEventListener('visibilitychange', handleResume);
handleWechatPayResult();
return () => {
window.removeEventListener('hashchange', handleHashChange);
window.removeEventListener('focus', handleResume);
window.removeEventListener('pageshow', handleResume);
document.removeEventListener('visibilitychange', handleResume);
};
}, [confirmPendingWechatRechargeOrder, handleWechatPayResult]);
useEffect(() => {
if (!submittingRechargeProductId || wechatRechargeOrderConfirmationState) {
return undefined;
}
const startedAt = Date.now();
let timer: number | null = null;
const pollPayResult = () => {
if (pollWechatPayResultFromHash()) {
return;
}
if (Date.now() - startedAt >= WECHAT_PAY_RESULT_RECHECK_TIMEOUT_MS) {
return;
}
timer = window.setTimeout(
pollPayResult,
WECHAT_PAY_RESULT_RECHECK_INTERVAL_MS,
);
};
timer = window.setTimeout(
pollPayResult,
WECHAT_PAY_RESULT_RECHECK_INTERVAL_MS,
);
return () => {
if (timer !== null) {
window.clearTimeout(timer);
}
};
}, [
pollWechatPayResultFromHash,
submittingRechargeProductId,
wechatRechargeOrderConfirmationState,
]);
const loadReferralCenter = useCallback(() => {
setIsLoadingReferral(true);
setIsReferralCenterInitialized(false);
void getPlatformProfileReferralInviteCenter()
.then(setReferralCenter)
.catch((error: unknown) => {
setReferralCenter(null);
setReferralError(
error instanceof Error ? error.message : '读取邀请码失败',
);
})
.finally(() => {
setIsReferralCenterInitialized(true);
setIsLoadingReferral(false);
});
}, []);
useEffect(() => {
if (activeTab !== 'profile' || !isAuthenticated) {
setIsReferralCenterInitialized(false);
setReferralCenter(null);
return;
}
loadReferralCenter();
}, [activeTab, isAuthenticated, loadReferralCenter]);
const openProfilePopupPanel = useCallback(
(panel: ProfileReferralPanel) => {
setProfilePopupPanel(panel);
setReferralError(null);
setReferralSuccess(null);
if (panel === 'redeem') {
setReferralRedeemCode(pendingProfileInviteCode);
}
if (panel === 'community') {
return;
}
if (!isReferralCenterInitialized && !isLoadingReferral) {
loadReferralCenter();
}
},
[
isLoadingReferral,
isReferralCenterInitialized,
loadReferralCenter,
pendingProfileInviteCode,
],
);
const closeProfilePopupPanel = useCallback(() => {
setProfilePopupPanel(null);
}, []);
const copyInviteInfo = useCallback(() => {
if (!referralCenter?.inviteCode) {
return;
}
const inviteUrl =
typeof window === 'undefined'
? referralCenter.inviteLinkPath
: new URL(referralCenter.inviteLinkPath, window.location.origin).href;
void copyInviteText(`${referralCenter.inviteCode} ${inviteUrl}`).then(
(copied) => {
setReferralSuccess(copied ? '已复制' : '复制失败');
},
);
}, [copyInviteText, referralCenter]);
const submitReferralRedeemCode = useCallback(() => {
const inviteCode = referralRedeemCode
.trim()
.replace(/[^0-9a-z]/gi, '')
.toUpperCase();
if (isSubmittingReferralRedeem || !inviteCode) {
return;
}
setIsSubmittingReferralRedeem(true);
setReferralError(null);
setReferralSuccess(null);
void redeemPlatformProfileReferralInviteCode(inviteCode)
.then((response) => {
setReferralCenter(response.center);
setReferralRedeemCode('');
setReferralSuccess('已填写');
void onRechargeSuccess?.();
})
.catch((error: unknown) => {
setReferralError(
error instanceof Error ? error.message : '填写邀请码失败',
);
})
.finally(() => setIsSubmittingReferralRedeem(false));
}, [isSubmittingReferralRedeem, onRechargeSuccess, referralRedeemCode]);
const submitRewardCode = useCallback(() => {
if (isSubmittingRewardCode || !rewardCodeInput.trim()) {
return;
}
setIsSubmittingRewardCode(true);
setRewardCodeError(null);
setRewardCodeSuccess(null);
void redeemPlatformProfileRewardCode(rewardCodeInput)
.then((response: RedeemProfileRewardCodeResponse) => {
setRewardCodeInput('');
setRewardCodeSuccess(`已到账 ${response.amountGranted} 泥点`);
void onRechargeSuccess?.();
})
.catch((error: unknown) => {
setRewardCodeError(error instanceof Error ? error.message : '兑换失败');
})
.finally(() => setIsSubmittingRewardCode(false));
}, [isSubmittingRewardCode, onRechargeSuccess, rewardCodeInput]);
return {
closeNativeWechatPayment,
closeProfilePopupPanel,
confirmNativeWechatPayment,
inviteCopyState: inviteCopyState as CopyFeedbackState,
isLoadingRechargeCenter,
isLoadingReferral,
isLoadingWalletLedger,
isRechargeOpen,
isRewardCodeOpen,
isSubmittingReferralRedeem,
isSubmittingRewardCode,
isWalletLedgerOpen,
loadRechargeCenter,
loadReferralCenter,
loadWalletLedger,
nativeWechatPayment,
openProfilePopupPanel,
openRechargeOrRewardCodeModal,
openRewardCodeModal,
openWalletLedgerPanel,
profilePopupPanel,
rechargeCenter,
rechargeError,
rechargePaymentResult,
referralCenter,
referralError,
referralRedeemCode,
referralSuccess,
rewardCodeError,
rewardCodeInput,
rewardCodeSuccess,
setIsRechargeOpen,
setIsRewardCodeOpen,
setIsWalletLedgerOpen,
setRechargePaymentResult,
setReferralRedeemCode,
setRewardCodeInput,
showRechargeEntry,
submittingRechargeProductId,
submitReferralRedeemCode,
submitRewardCode,
walletLedger,
walletLedgerError,
wechatRechargeOrderConfirmationState,
buyRechargeProduct,
copyInviteInfo,
};
}