新增 usePlatformProfileCenterController 统一托管充值、任务、邀请码、账单与兑换码状态 RpgEntryHomeView 改为复用个人中心 controller,仅保留展示、扫码和资料编辑交互 更新 PlatformUiKit 收口计划与共享决策记录
1061 lines
34 KiB
TypeScript
1061 lines
34 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||
|
||
import type { AuthUser } from '../../services/authService';
|
||
import { refreshStoredAccessToken } from '../../services/apiClient';
|
||
import {
|
||
resolveProfileRechargeProductPaymentChannel,
|
||
WECHAT_H5_PAYMENT_CHANNEL,
|
||
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL,
|
||
WECHAT_NATIVE_PAYMENT_CHANNEL,
|
||
} from '../../services/payment/paymentPlatform';
|
||
import { redirectToPaymentUrl } from '../../services/payment/paymentRedirect';
|
||
import {
|
||
claimRpgProfileTaskReward,
|
||
confirmWechatRpgProfileRechargeOrder,
|
||
createRpgProfileRechargeOrder,
|
||
getRpgProfileRechargeCenter,
|
||
getRpgProfileReferralInviteCenter,
|
||
getRpgProfileTasks,
|
||
getRpgProfileWalletLedger,
|
||
redeemRpgProfileReferralInviteCode,
|
||
redeemRpgProfileRewardCode,
|
||
watchWechatRpgProfileRechargeOrder,
|
||
} from '../../services/rpg-entry/rpgProfileClient';
|
||
import {
|
||
type ConfirmWechatProfileRechargeOrderResponse,
|
||
type ProfileRechargeCenterResponse,
|
||
type ProfileRechargeProduct,
|
||
type ProfileReferralInviteCenterResponse,
|
||
type ProfileTaskCenterResponse,
|
||
type ProfileWalletLedgerResponse,
|
||
type RedeemProfileRewardCodeResponse,
|
||
type WechatMiniProgramPayParams,
|
||
type WechatMiniProgramVirtualPayParams,
|
||
type WechatNativePayment,
|
||
} from '../../../packages/shared/src/contracts/runtime';
|
||
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_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||
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;
|
||
|
||
export type ProfileReferralPanel = 'invite' | 'redeem' | 'community';
|
||
export type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives';
|
||
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;
|
||
isConfirming: boolean;
|
||
};
|
||
|
||
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 loadWechatJsSdk() {
|
||
if (typeof window === 'undefined') {
|
||
return Promise.reject(new Error('请在微信小程序内完成支付'));
|
||
}
|
||
if (window.wx?.miniProgram?.navigateTo) {
|
||
return Promise.resolve(window.wx);
|
||
}
|
||
|
||
return new Promise<NonNullable<Window['wx']>>((resolve, reject) => {
|
||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||
`script[src="${WECHAT_JS_SDK_URL}"]`,
|
||
);
|
||
const complete = () => {
|
||
if (window.wx?.miniProgram?.navigateTo) {
|
||
resolve(window.wx);
|
||
} else {
|
||
reject(new Error('请在微信小程序内完成支付'));
|
||
}
|
||
};
|
||
|
||
if (existingScript) {
|
||
existingScript.addEventListener('load', complete, { once: true });
|
||
existingScript.addEventListener(
|
||
'error',
|
||
() => reject(new Error('请在微信小程序内完成支付')),
|
||
{ once: true },
|
||
);
|
||
complete();
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = WECHAT_JS_SDK_URL;
|
||
script.async = true;
|
||
script.onload = complete;
|
||
script.onerror = () => reject(new Error('请在微信小程序内完成支付'));
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
async function requestWechatMiniProgramPayment(
|
||
payload:
|
||
| WechatMiniProgramPayParams
|
||
| WechatMiniProgramVirtualPayParams
|
||
| null
|
||
| undefined,
|
||
orderId: string,
|
||
): Promise<void> {
|
||
if (!payload) {
|
||
return Promise.reject(new Error('请在微信小程序内完成支付'));
|
||
}
|
||
const wxBridge = await loadWechatJsSdk();
|
||
const miniProgram = wxBridge.miniProgram;
|
||
if (!miniProgram || typeof miniProgram.navigateTo !== 'function') {
|
||
return Promise.reject(new Error('请在微信小程序内完成支付'));
|
||
}
|
||
const navigateTo = miniProgram.navigateTo;
|
||
|
||
const requestId = `wechat_pay_${orderId}_${Date.now()}`;
|
||
return new Promise<void>((resolve, reject) => {
|
||
navigateTo({
|
||
url: `/pages/wechat-pay/index?requestId=${encodeURIComponent(requestId)}&orderId=${encodeURIComponent(orderId)}&payParams=${encodeURIComponent(JSON.stringify(payload))}`,
|
||
success() {
|
||
resolve();
|
||
},
|
||
fail(error) {
|
||
console.error('[wechat-pay] navigateTo failed', error);
|
||
reject(
|
||
error instanceof Error
|
||
? error
|
||
: new Error('请在微信小程序内完成支付'),
|
||
);
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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(() => {
|
||
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;
|
||
}, [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 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);
|
||
await requestWechatMiniProgramPayment(
|
||
response.wechatMiniProgramPayParams,
|
||
response.order.orderId,
|
||
);
|
||
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: '完成支付后返回页面确认到账状态。',
|
||
});
|
||
redirectToPaymentUrl(h5Url);
|
||
return;
|
||
}
|
||
if (paymentChannel === WECHAT_NATIVE_PAYMENT_CHANNEL) {
|
||
const codeUrl = response.wechatNativePayment?.codeUrl?.trim();
|
||
if (!codeUrl) {
|
||
throw new Error('微信 Native 支付二维码生成失败');
|
||
}
|
||
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
|
||
setRechargeCenter(response.center);
|
||
setNativeWechatPayment({
|
||
orderId: response.order.orderId,
|
||
codeUrl,
|
||
isConfirming: false,
|
||
});
|
||
setSubmittingRechargeProductId(null);
|
||
return;
|
||
}
|
||
|
||
throw new Error('充值支付渠道无效');
|
||
})
|
||
.catch((error: unknown) => {
|
||
pendingWechatRechargeOrderIdRef.current = null;
|
||
setNativeWechatPayment(null);
|
||
setRechargeError(error instanceof Error ? error.message : '充值失败');
|
||
setSubmittingRechargeProductId(null);
|
||
});
|
||
},
|
||
[submittingRechargeProductId],
|
||
);
|
||
|
||
const confirmNativeWechatPayment = useCallback(() => {
|
||
if (!nativeWechatPayment || nativeWechatPayment.isConfirming) {
|
||
return;
|
||
}
|
||
|
||
setNativeWechatPayment((current) =>
|
||
current && current.orderId === nativeWechatPayment.orderId
|
||
? { ...current, isConfirming: true }
|
||
: current,
|
||
);
|
||
setRechargePaymentResult({
|
||
kind: 'pending',
|
||
title: '正在确认支付',
|
||
message: '正在查询微信支付到账状态。',
|
||
});
|
||
void confirmWechatRechargeOrderUntilSettled(nativeWechatPayment.orderId)
|
||
.then((response) => {
|
||
const isPaid = response.order.status === 'paid';
|
||
setRechargeCenter(response.center);
|
||
setRechargePaymentResult(
|
||
isPaid
|
||
? {
|
||
kind: 'success',
|
||
title: '支付成功',
|
||
message: '已到账,账户状态已刷新。',
|
||
}
|
||
: {
|
||
kind: 'pending',
|
||
title: '等待微信确认',
|
||
message: '暂时没能确认到账状态,请稍后再试。',
|
||
},
|
||
);
|
||
if (isPaid) {
|
||
setNativeWechatPayment(null);
|
||
pendingWechatRechargeOrderIdRef.current = null;
|
||
void onRechargeSuccess?.();
|
||
} else {
|
||
setNativeWechatPayment((current) =>
|
||
current && current.orderId === nativeWechatPayment.orderId
|
||
? { ...current, isConfirming: false }
|
||
: current,
|
||
);
|
||
}
|
||
})
|
||
.catch(() => {
|
||
setRechargePaymentResult({
|
||
kind: 'pending',
|
||
title: '等待微信确认',
|
||
message: '暂时没能确认到账状态,请稍后再试。',
|
||
});
|
||
setNativeWechatPayment((current) =>
|
||
current && current.orderId === nativeWechatPayment.orderId
|
||
? { ...current, isConfirming: false }
|
||
: 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,
|
||
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,
|
||
};
|
||
}
|