Files
Genarrative/src/components/platform-entry/usePlatformProfileCenterController.ts
T
kdletters 5f6f4d0ba6 接入会员周期泥点制度
新增 Starter、Basic、Pro、Ultimate 会员商品配置与权益字段。

拆分会员有效期、周期重置时间、周期限时泥点和永久泥点余额。

实现升级补价补点但不延长有效期或移动重置时间,同级购买只延长有效期。

补齐钱包扣费与退款中的会员周期泥点消耗、重置和流水记录。

同步后台商品配置、前端充值展示、共享契约、SpacetimeDB 迁移与生成绑定。
2026-07-09 14:48:26 +08:00

1096 lines
36 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
type ConfirmWechatProfileRechargeOrderResponse,
type ProfileRechargeCenterResponse,
type ProfileRechargeProduct,
type ProfileReferralInviteCenterResponse,
type ProfileTaskCenterResponse,
type ProfileWalletLedgerResponse,
type RedeemProfileRewardCodeResponse,
type WechatNativePayment,
} from '../../../packages/shared/src/contracts/runtime';
import { refreshStoredAccessToken } from '../../services/apiClient';
import { startWechatBind, type AuthUser } from '../../services/authService';
import { 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_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';
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)
);
}
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);
});
}
async function confirmWechatRechargeOrderUntilSettled(
orderId: string,
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
if (latestResponse.order.status !== 'pending') {
return latestResponse;
}
for (const delayMs of WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
if (latestResponse.order.status !== 'pending') {
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 (latestResponse.order.status !== 'pending') {
return latestResponse;
}
for (const delayMs of WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS) {
await waitWechatPayConfirmDelay(delayMs);
latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
if (latestResponse.order.status !== 'pending') {
return latestResponse;
}
}
return latestResponse;
}
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 isPaid = response.order.status === 'paid';
setRechargeCenter(response.center);
pendingWechatRechargeOrderIdRef.current = null;
confirmingWechatRechargeOrderIdRef.current = null;
setWechatRechargeOrderConfirmationState(null);
setRechargePaymentResult(
isPaid
? {
kind: 'success',
title: '支付成功',
message: '已到账,账户状态已刷新。',
}
: {
kind: 'pending',
title: '支付处理中',
message: '正在等待到账状态确认,请稍后查看余额或会员状态。',
},
);
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 isPaid = response.order.status === 'paid';
setRechargeCenter(response.center);
pendingWechatRechargeOrderIdRef.current = null;
confirmingWechatRechargeOrderIdRef.current = null;
setWechatRechargeOrderConfirmationState(null);
setSubmittingRechargeProductId(null);
setRechargePaymentResult(
isPaid
? {
kind: 'success',
title: '支付成功',
message: '已到账,账户状态已刷新。',
}
: {
kind: 'pending',
title: '支付处理中',
message: '正在等待到账状态确认,请稍后查看余额或会员状态。',
},
);
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 isPaid = confirmResponse.order.status === 'paid';
setRechargeCenter(confirmResponse.center);
setRechargePaymentResult(
isPaid
? {
kind: 'success',
title: '支付成功',
message: '已到账,账户状态已刷新。',
}
: {
kind: 'pending',
title: '等待微信确认',
message: '暂时没能确认到账状态,请稍后再试。',
},
);
if (isPaid) {
pendingWechatRechargeOrderIdRef.current = null;
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 codeUrl = response.wechatNativePayment?.codeUrl?.trim();
const expiresAt = response.wechatNativePayment?.expiresAt?.trim();
if (!codeUrl || !expiresAt) {
throw new Error('微信 Native 支付链接生成失败');
}
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
setRechargeCenter(response.center);
setNativeWechatPayment({
...response.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_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 isPaid = response.order.status === 'paid';
setRechargeCenter(response.center);
if (isPaid) {
setNativeWechatPayment(null);
pendingWechatRechargeOrderIdRef.current = null;
setRechargePaymentResult({
kind: 'success',
title: '支付成功',
message: '已到账,账户状态已刷新。',
});
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,
};
}