Files
Genarrative/src/components/platform-entry/usePlatformProfileCenterController.ts
T
kdletters 2de9b79e69 修复充值过期补偿确认竞态
调整微信充值确认终态判断,未完成补偿查单的 expired 不再提前结束。

让 SSE 客户端和前端确认流程等待补偿后的 paid 或已检查 expired。

补充 unchecked expired 到 paid 的回归测试。
2026-07-10 02:09:59 +08:00

1171 lines
38 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
type ConfirmWechatProfileRechargeOrderResponse,
type ProfileRechargeCenterResponse,
type ProfileRechargeOrder,
type ProfileRechargeProduct,
type ProfileReferralInviteCenterResponse,
type ProfileTaskCenterResponse,
type ProfileWalletLedgerResponse,
type RedeemProfileRewardCodeResponse,
type WechatNativePayment,
} from '../../../packages/shared/src/contracts/runtime';
import {
clearStoredAccessToken,
refreshStoredAccessToken,
} from '../../services/apiClient';
import { startWechatBind, type AuthUser } 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 {
claimRpgProfileTaskReward,
confirmWechatRpgProfileRechargeOrder,
createRpgProfileRechargeOrder,
getRpgProfileRechargeCenter,
getRpgProfileReferralInviteCenter,
getRpgProfileTasks,
getRpgProfileWalletLedger,
redeemRpgProfileReferralInviteCode,
redeemRpgProfileRewardCode,
watchWechatRpgProfileRechargeOrder,
} from '../../services/rpg-entry/rpgProfileClient';
import {
type CopyFeedbackState,
useCopyFeedback,
} from '../common/useCopyFeedback';
const PROFILE_TASK_DAY_MS = 24 * 60 * 60 * 1000;
const PROFILE_TASK_BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
const PROFILE_TASK_MIN_RESET_DELAY_MS = 1000;
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
const WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS = [800, 1600] as const;
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;
export type RechargeTab = 'points' | 'membership';
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 = WechatNativePayment & {
orderId: string;
productTitle: string;
amountCents: number;
isConfirming: boolean;
confirmMessage?: string;
};
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;
profileTaskRefreshKey?: number;
onRechargeSuccess?: () => void | Promise<void>;
requestLogin: () => void;
currentUser: AuthUser | null | undefined;
};
function getDelayUntilNextProfileTaskReset(nowMs = Date.now()) {
const shiftedNow = nowMs + PROFILE_TASK_BEIJING_OFFSET_MS;
const nextDayStart =
Math.floor(shiftedNow / PROFILE_TASK_DAY_MS) * PROFILE_TASK_DAY_MS +
PROFILE_TASK_DAY_MS;
const nextResetAt = nextDayStart - PROFILE_TASK_BEIJING_OFFSET_MS;
return Math.max(PROFILE_TASK_MIN_RESET_DELAY_MS, nextResetAt - nowMs);
}
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 confirmWechatRpgProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
for (const delayMs of WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
}
try {
const streamedResponse = await watchWechatRpgProfileRechargeOrder(orderId);
return streamedResponse;
} catch {
return latestResponse;
}
}
async function confirmWechatRechargeOrderQuickly(
orderId: string,
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
return latestResponse;
}
for (const delayMs of WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
latestResponse = await confirmWechatRpgProfileRechargeOrder(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,
profileTaskRefreshKey = 0,
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 [activeRechargeTab, setActiveRechargeTab] =
useState<RechargeTab>('points');
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 [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false);
const [taskCenter, setTaskCenter] =
useState<ProfileTaskCenterResponse | null>(null);
const [taskCenterError, setTaskCenterError] = useState<string | null>(null);
const [isLoadingTaskCenter, setIsLoadingTaskCenter] = useState(false);
const taskCenterRequestIdRef = useRef(0);
const [claimingTaskId, setClaimingTaskId] = useState<string | null>(null);
const [taskClaimSuccess, setTaskClaimSuccess] = useState<string | null>(null);
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);
// 中文注释:支持带邀请码 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 getRpgProfileWalletLedger()
.then(setWalletLedger)
.catch((error: unknown) => {
setWalletLedger(null);
setWalletLedgerError(
error instanceof Error ? error.message : '读取泥点账单失败',
);
})
.finally(() => setIsLoadingWalletLedger(false));
}, []);
const openWalletLedgerPanel = useCallback(() => {
setIsWalletLedgerOpen(true);
loadWalletLedger();
}, [loadWalletLedger]);
const loadRechargeCenter = useCallback(() => {
setRechargeError(null);
setIsLoadingRechargeCenter(true);
void getRpgProfileRechargeCenter()
.then(setRechargeCenter)
.catch((error: unknown) => {
setRechargeCenter(null);
setRechargeError(
error instanceof Error ? error.message : '读取账户充值失败',
);
})
.finally(() => 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';
setRechargeCenter(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;
}, [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';
setRechargeCenter(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;
}, [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 createRpgProfileRechargeOrder(product.productId, paymentChannel)
.then(async (response) => {
if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) {
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
setRechargeCenter(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;
setRechargeCenter(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';
setRechargeCenter(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;
setRechargeCenter(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;
setRechargeCenter(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);
});
},
[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) => {
const result = buildRechargePaymentResultForOrder(response.order);
const isPaid = result.kind === 'success';
setRechargeCenter(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));
}, [nativeWechatPayment, 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 loadTaskCenter = useCallback(() => {
const requestId = ++taskCenterRequestIdRef.current;
setTaskCenterError(null);
setIsLoadingTaskCenter(true);
void getRpgProfileTasks()
.then((center) => {
if (requestId === taskCenterRequestIdRef.current) {
setTaskCenter(center);
}
})
.catch((error: unknown) => {
if (requestId !== taskCenterRequestIdRef.current) {
return;
}
setTaskCenter(null);
setTaskCenterError(
error instanceof Error ? error.message : '读取每日任务失败',
);
})
.finally(() => {
if (requestId === taskCenterRequestIdRef.current) {
setIsLoadingTaskCenter(false);
}
});
}, []);
useEffect(() => {
if (activeTab !== 'profile' || !isAuthenticated) {
taskCenterRequestIdRef.current += 1;
setTaskCenter(null);
setTaskCenterError(null);
return;
}
loadTaskCenter();
}, [activeTab, isAuthenticated, loadTaskCenter, profileTaskRefreshKey]);
useEffect(() => {
if (activeTab !== 'profile' || !isAuthenticated) {
return undefined;
}
// 中文注释:每日任务重置依赖北京时间跨天与 access token 刷新,继续留在 controller 里集中托管。
let cancelled = false;
let timer: number | null = null;
const scheduleNextReset = () => {
if (cancelled) {
return;
}
timer = window.setTimeout(() => {
void refreshStoredAccessToken({ clearOnFailure: false })
.catch(() => undefined)
.finally(() => {
if (cancelled) {
return;
}
loadTaskCenter();
scheduleNextReset();
});
}, getDelayUntilNextProfileTaskReset());
};
scheduleNextReset();
return () => {
cancelled = true;
if (timer !== null) {
window.clearTimeout(timer);
}
};
}, [activeTab, isAuthenticated, loadTaskCenter]);
const openTaskCenterPanel = useCallback(() => {
setIsTaskCenterOpen(true);
setTaskClaimSuccess(null);
if (!taskCenter) {
loadTaskCenter();
}
}, [loadTaskCenter, taskCenter]);
const loadReferralCenter = useCallback(() => {
setIsLoadingReferral(true);
setIsReferralCenterInitialized(false);
void getRpgProfileReferralInviteCenter()
.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 redeemRpgProfileReferralInviteCode(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 redeemRpgProfileRewardCode(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]);
const claimTaskReward = useCallback(
(taskId: string) => {
if (claimingTaskId) {
return;
}
setClaimingTaskId(taskId);
setTaskCenterError(null);
setTaskClaimSuccess(null);
void claimRpgProfileTaskReward(taskId)
.then((response) => {
setTaskCenter(response.center);
setTaskClaimSuccess(`已领取 ${response.rewardPoints} 泥点`);
void onRechargeSuccess?.();
})
.catch((error: unknown) => {
setTaskCenterError(
error instanceof Error ? error.message : '领取任务奖励失败',
);
})
.finally(() => setClaimingTaskId(null));
},
[claimingTaskId, onRechargeSuccess],
);
return {
activeRechargeTab,
claimTaskReward,
claimingTaskId,
closeNativeWechatPayment,
closeProfilePopupPanel,
confirmNativeWechatPayment,
inviteCopyState: inviteCopyState as CopyFeedbackState,
isLoadingRechargeCenter,
isLoadingReferral,
isLoadingTaskCenter,
isLoadingWalletLedger,
isRechargeOpen,
isRewardCodeOpen,
isSubmittingReferralRedeem,
isSubmittingRewardCode,
isTaskCenterOpen,
isWalletLedgerOpen,
loadRechargeCenter,
loadReferralCenter,
loadTaskCenter,
loadWalletLedger,
nativeWechatPayment,
openProfilePopupPanel,
openRechargeOrRewardCodeModal,
openRewardCodeModal,
openTaskCenterPanel,
openWalletLedgerPanel,
profilePopupPanel,
rechargeCenter,
rechargeError,
rechargePaymentResult,
referralCenter,
referralError,
referralRedeemCode,
referralSuccess,
rewardCodeError,
rewardCodeInput,
rewardCodeSuccess,
setActiveRechargeTab,
setIsRechargeOpen,
setIsRewardCodeOpen,
setIsTaskCenterOpen,
setIsWalletLedgerOpen,
setRechargePaymentResult,
setReferralRedeemCode,
setRewardCodeInput,
showRechargeEntry,
submittingRechargeProductId,
submitReferralRedeemCode,
submitRewardCode,
taskCenter,
taskCenterError,
taskClaimSuccess,
walletLedger,
walletLedgerError,
wechatRechargeOrderConfirmationState,
buyRechargeProduct,
copyInviteInfo,
};
}