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