1756 lines
55 KiB
TypeScript
1756 lines
55 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 { usePlatformWalletStore } from '../../stores/usePlatformWalletStore';
|
|
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;
|
|
|
|
type AccountLifecycle = Readonly<{
|
|
ownerUserId: string;
|
|
revision: number;
|
|
}>;
|
|
|
|
type WechatRechargeOrderLifecycle = Readonly<{
|
|
orderId: string;
|
|
account: AccountLifecycle;
|
|
abortController: AbortController;
|
|
}>;
|
|
|
|
type WechatRechargeConfirmationOptions = Readonly<{
|
|
signal: AbortSignal;
|
|
isCurrent: () => boolean;
|
|
}>;
|
|
|
|
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 createWechatRechargeAbortError(signal: AbortSignal) {
|
|
return signal.reason instanceof Error
|
|
? signal.reason
|
|
: new DOMException('Wechat recharge confirmation aborted', 'AbortError');
|
|
}
|
|
|
|
function assertWechatRechargeConfirmationActive(
|
|
options: WechatRechargeConfirmationOptions,
|
|
) {
|
|
if (options.signal.aborted || !options.isCurrent()) {
|
|
throw createWechatRechargeAbortError(options.signal);
|
|
}
|
|
}
|
|
|
|
function waitWechatPayConfirmDelay(delayMs: number, signal: AbortSignal) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
if (signal.aborted) {
|
|
reject(createWechatRechargeAbortError(signal));
|
|
return;
|
|
}
|
|
const timerId = window.setTimeout(() => {
|
|
signal.removeEventListener('abort', handleAbort);
|
|
resolve();
|
|
}, delayMs);
|
|
const handleAbort = () => {
|
|
window.clearTimeout(timerId);
|
|
signal.removeEventListener('abort', handleAbort);
|
|
reject(createWechatRechargeAbortError(signal));
|
|
};
|
|
signal.addEventListener('abort', handleAbort, { once: true });
|
|
});
|
|
}
|
|
|
|
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,
|
|
options: WechatRechargeConfirmationOptions,
|
|
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
|
|
assertWechatRechargeConfirmationActive(options);
|
|
let latestResponse = await confirmWechatPlatformProfileRechargeOrder(
|
|
orderId,
|
|
{ signal: options.signal },
|
|
);
|
|
assertWechatRechargeConfirmationActive(options);
|
|
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
|
|
return latestResponse;
|
|
}
|
|
|
|
for (const delayMs of WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS) {
|
|
assertWechatRechargeConfirmationActive(options);
|
|
await waitWechatPayConfirmDelay(delayMs, options.signal);
|
|
assertWechatRechargeConfirmationActive(options);
|
|
|
|
latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId, {
|
|
signal: options.signal,
|
|
});
|
|
assertWechatRechargeConfirmationActive(options);
|
|
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
|
|
return latestResponse;
|
|
}
|
|
}
|
|
|
|
try {
|
|
assertWechatRechargeConfirmationActive(options);
|
|
const streamedResponse = await watchWechatPlatformProfileRechargeOrder(
|
|
orderId,
|
|
{
|
|
signal: options.signal,
|
|
},
|
|
);
|
|
assertWechatRechargeConfirmationActive(options);
|
|
return streamedResponse;
|
|
} catch {
|
|
assertWechatRechargeConfirmationActive(options);
|
|
return latestResponse;
|
|
}
|
|
}
|
|
|
|
async function confirmWechatRechargeOrderQuickly(
|
|
orderId: string,
|
|
options: WechatRechargeConfirmationOptions,
|
|
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
|
|
assertWechatRechargeConfirmationActive(options);
|
|
let latestResponse = await confirmWechatPlatformProfileRechargeOrder(
|
|
orderId,
|
|
{ signal: options.signal },
|
|
);
|
|
assertWechatRechargeConfirmationActive(options);
|
|
if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) {
|
|
return latestResponse;
|
|
}
|
|
|
|
for (const delayMs of WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS) {
|
|
assertWechatRechargeConfirmationActive(options);
|
|
await waitWechatPayConfirmDelay(delayMs, options.signal);
|
|
assertWechatRechargeConfirmationActive(options);
|
|
|
|
latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId, {
|
|
signal: options.signal,
|
|
});
|
|
assertWechatRechargeConfirmationActive(options);
|
|
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 pendingWechatRechargeOrderRef =
|
|
useRef<WechatRechargeOrderLifecycle | null>(null);
|
|
const confirmingWechatRechargeOrderRef =
|
|
useRef<WechatRechargeOrderLifecycle | null>(null);
|
|
const rechargeCenterReadRevisionRef = useRef(0);
|
|
const accountLifecycleRevisionRef = useRef(0);
|
|
const walletLedgerReadRevisionRef = useRef(0);
|
|
const currentUserId = currentUser?.id ?? '';
|
|
const currentUserIdRef = useRef(currentUserId);
|
|
currentUserIdRef.current = currentUserId;
|
|
const captureAccountLifecycle = useCallback(
|
|
(): AccountLifecycle => ({
|
|
ownerUserId: currentUserId,
|
|
revision: accountLifecycleRevisionRef.current,
|
|
}),
|
|
[currentUserId],
|
|
);
|
|
const isAccountLifecycleCurrent = useCallback(
|
|
(account: AccountLifecycle) =>
|
|
account.revision === accountLifecycleRevisionRef.current &&
|
|
account.ownerUserId === currentUserIdRef.current,
|
|
[],
|
|
);
|
|
const isRechargeOrderCurrent = useCallback(
|
|
(
|
|
order: WechatRechargeOrderLifecycle | null | undefined,
|
|
): order is WechatRechargeOrderLifecycle =>
|
|
Boolean(order) && isAccountLifecycleCurrent(order!.account),
|
|
[isAccountLifecycleCurrent],
|
|
);
|
|
const isSameRechargeOrder = useCallback(
|
|
(
|
|
left: WechatRechargeOrderLifecycle | null | undefined,
|
|
right: WechatRechargeOrderLifecycle | null | undefined,
|
|
) => {
|
|
if (!left || !right) {
|
|
return false;
|
|
}
|
|
return (
|
|
left.orderId === right.orderId &&
|
|
left.account.ownerUserId === right.account.ownerUserId &&
|
|
left.account.revision === right.account.revision
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
const abortRechargeOrders = useCallback(
|
|
(...orders: Array<WechatRechargeOrderLifecycle | null | undefined>) => {
|
|
const controllers = new Set(
|
|
orders.flatMap((order) => (order ? [order.abortController] : [])),
|
|
);
|
|
controllers.forEach((controller) => controller.abort());
|
|
},
|
|
[],
|
|
);
|
|
const createRechargeOrderLifecycle = useCallback(
|
|
(
|
|
orderId: string,
|
|
account: AccountLifecycle,
|
|
): WechatRechargeOrderLifecycle => ({
|
|
orderId,
|
|
account,
|
|
abortController: new AbortController(),
|
|
}),
|
|
[],
|
|
);
|
|
const createRechargeConfirmationOptions = useCallback(
|
|
(
|
|
order: WechatRechargeOrderLifecycle,
|
|
): WechatRechargeConfirmationOptions => ({
|
|
signal: order.abortController.signal,
|
|
isCurrent: () =>
|
|
isRechargeOrderCurrent(order) &&
|
|
(isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order) ||
|
|
isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order)),
|
|
}),
|
|
[isRechargeOrderCurrent, isSameRechargeOrder],
|
|
);
|
|
const walletOwnerUserId = usePlatformWalletStore(
|
|
(state) => state.ownerUserId,
|
|
);
|
|
const storedMudPointBalance = usePlatformWalletStore(
|
|
(state) => state.mudPointBalance,
|
|
);
|
|
const applyWalletBalanceSnapshot = usePlatformWalletStore(
|
|
(state) => state.applyWalletBalanceSnapshot,
|
|
);
|
|
const captureWalletBalanceSnapshot = usePlatformWalletStore(
|
|
(state) => state.captureWalletBalanceSnapshot,
|
|
);
|
|
const onWalletBalanceMayHaveChanged = usePlatformWalletStore(
|
|
(state) => state.onWalletBalanceMayHaveChanged,
|
|
);
|
|
const walletOwnerMatchesCurrentUser =
|
|
Boolean(currentUserId) && walletOwnerUserId === currentUserId;
|
|
const mudPointBalance = walletOwnerMatchesCurrentUser
|
|
? storedMudPointBalance
|
|
: null;
|
|
|
|
useEffect(() => {
|
|
rechargeCenterReadRevisionRef.current += 1;
|
|
accountLifecycleRevisionRef.current += 1;
|
|
walletLedgerReadRevisionRef.current += 1;
|
|
const pendingOrder = pendingWechatRechargeOrderRef.current;
|
|
const confirmingOrder = confirmingWechatRechargeOrderRef.current;
|
|
abortRechargeOrders(pendingOrder, confirmingOrder);
|
|
if (
|
|
(pendingOrder && pendingOrder.account.ownerUserId !== currentUserId) ||
|
|
(confirmingOrder && confirmingOrder.account.ownerUserId !== currentUserId)
|
|
) {
|
|
clearWechatPayResultHash();
|
|
}
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
setRechargeCenter(null);
|
|
setIsLoadingRechargeCenter(false);
|
|
setRechargeError(null);
|
|
setRechargePaymentResult(null);
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
setNativeWechatPayment(null);
|
|
setSubmittingRechargeProductId(null);
|
|
setIsWalletLedgerOpen(false);
|
|
setWalletLedger(null);
|
|
setWalletLedgerError(null);
|
|
setIsLoadingWalletLedger(false);
|
|
setIsRewardCodeOpen(false);
|
|
setRewardCodeInput('');
|
|
setIsSubmittingRewardCode(false);
|
|
setRewardCodeError(null);
|
|
setRewardCodeSuccess(null);
|
|
setIsSubmittingReferralRedeem(false);
|
|
}, [abortRechargeOrders, currentUserId]);
|
|
const rechargeCenterReadAbortControllerRef = useRef<AbortController | null>(
|
|
null,
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
rechargeCenterReadRevisionRef.current += 1;
|
|
accountLifecycleRevisionRef.current += 1;
|
|
abortRechargeOrders(
|
|
pendingWechatRechargeOrderRef.current,
|
|
confirmingWechatRechargeOrderRef.current,
|
|
);
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
rechargeCenterReadAbortControllerRef.current?.abort();
|
|
rechargeCenterReadAbortControllerRef.current = null;
|
|
};
|
|
}, [abortRechargeOrders]);
|
|
|
|
// 中文注释:支持带邀请码 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(() => {
|
|
const snapshotOwnerUserId = currentUserId;
|
|
const accountRevision = accountLifecycleRevisionRef.current;
|
|
const revision = ++walletLedgerReadRevisionRef.current;
|
|
setWalletLedgerError(null);
|
|
setIsLoadingWalletLedger(true);
|
|
void getPlatformProfileWalletLedger()
|
|
.then((ledger) => {
|
|
if (
|
|
revision === walletLedgerReadRevisionRef.current &&
|
|
accountRevision === accountLifecycleRevisionRef.current &&
|
|
currentUserIdRef.current === snapshotOwnerUserId
|
|
) {
|
|
setWalletLedger(ledger);
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (
|
|
revision !== walletLedgerReadRevisionRef.current ||
|
|
accountRevision !== accountLifecycleRevisionRef.current ||
|
|
currentUserIdRef.current !== snapshotOwnerUserId
|
|
) {
|
|
return;
|
|
}
|
|
setWalletLedger(null);
|
|
setWalletLedgerError(
|
|
error instanceof Error ? error.message : '读取泥点账单失败',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (
|
|
revision === walletLedgerReadRevisionRef.current &&
|
|
accountRevision === accountLifecycleRevisionRef.current &&
|
|
currentUserIdRef.current === snapshotOwnerUserId
|
|
) {
|
|
setIsLoadingWalletLedger(false);
|
|
}
|
|
});
|
|
}, [currentUserId]);
|
|
|
|
const openWalletLedgerPanel = useCallback(() => {
|
|
setIsWalletLedgerOpen(true);
|
|
loadWalletLedger();
|
|
}, [loadWalletLedger]);
|
|
|
|
const applyRechargeCenter = useCallback(
|
|
(
|
|
center: ProfileRechargeCenterResponse,
|
|
account: AccountLifecycle,
|
|
refreshWallet = false,
|
|
walletSnapshot = captureWalletBalanceSnapshot(account.ownerUserId),
|
|
) => {
|
|
if (
|
|
!isAccountLifecycleCurrent(account) ||
|
|
usePlatformWalletStore.getState().ownerUserId !== account.ownerUserId
|
|
) {
|
|
return false;
|
|
}
|
|
rechargeCenterReadRevisionRef.current += 1;
|
|
rechargeCenterReadAbortControllerRef.current?.abort();
|
|
rechargeCenterReadAbortControllerRef.current = null;
|
|
setIsLoadingRechargeCenter(false);
|
|
setRechargeError(null);
|
|
setRechargeCenter(center);
|
|
if (center.mudPointBalance && walletSnapshot) {
|
|
applyWalletBalanceSnapshot(walletSnapshot, center.mudPointBalance);
|
|
}
|
|
if (refreshWallet || !center.mudPointBalance) {
|
|
void onWalletBalanceMayHaveChanged();
|
|
}
|
|
return true;
|
|
},
|
|
[
|
|
applyWalletBalanceSnapshot,
|
|
captureWalletBalanceSnapshot,
|
|
isAccountLifecycleCurrent,
|
|
onWalletBalanceMayHaveChanged,
|
|
],
|
|
);
|
|
|
|
const loadRechargeCenter = useCallback(() => {
|
|
const account = captureAccountLifecycle();
|
|
const walletSnapshot = captureWalletBalanceSnapshot(account.ownerUserId);
|
|
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 &&
|
|
isAccountLifecycleCurrent(account)
|
|
) {
|
|
applyRechargeCenter(center, account, false, walletSnapshot);
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (
|
|
abortController.signal.aborted ||
|
|
revision !== rechargeCenterReadRevisionRef.current ||
|
|
!isAccountLifecycleCurrent(account)
|
|
) {
|
|
return;
|
|
}
|
|
setRechargeCenter(null);
|
|
setRechargeError(
|
|
error instanceof Error ? error.message : '读取泥点购买信息失败',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (rechargeCenterReadAbortControllerRef.current === abortController) {
|
|
rechargeCenterReadAbortControllerRef.current = null;
|
|
}
|
|
if (
|
|
revision === rechargeCenterReadRevisionRef.current &&
|
|
isAccountLifecycleCurrent(account)
|
|
) {
|
|
setIsLoadingRechargeCenter(false);
|
|
}
|
|
});
|
|
}, [
|
|
applyRechargeCenter,
|
|
captureAccountLifecycle,
|
|
captureWalletBalanceSnapshot,
|
|
isAccountLifecycleCurrent,
|
|
]);
|
|
|
|
const refreshRechargeState = useCallback(
|
|
(account: AccountLifecycle) => {
|
|
if (!isAccountLifecycleCurrent(account)) {
|
|
return;
|
|
}
|
|
loadRechargeCenter();
|
|
setSubmittingRechargeProductId(null);
|
|
abortRechargeOrders(
|
|
pendingWechatRechargeOrderRef.current,
|
|
confirmingWechatRechargeOrderRef.current,
|
|
);
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
setNativeWechatPayment(null);
|
|
},
|
|
[abortRechargeOrders, isAccountLifecycleCurrent, loadRechargeCenter],
|
|
);
|
|
|
|
const handleWechatPayResult = useCallback(() => {
|
|
const payResult = readWechatPayResultFromHash();
|
|
if (!payResult) {
|
|
return false;
|
|
}
|
|
|
|
const pendingOrder = pendingWechatRechargeOrderRef.current;
|
|
if (pendingOrder && !isRechargeOrderCurrent(pendingOrder)) {
|
|
return false;
|
|
}
|
|
if (
|
|
pendingOrder &&
|
|
payResult.orderId &&
|
|
payResult.orderId !== pendingOrder.orderId
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const account = pendingOrder?.account ?? captureAccountLifecycle();
|
|
if (!account.ownerUserId || !isAccountLifecycleCurrent(account)) {
|
|
return false;
|
|
}
|
|
|
|
if (payResult.status === 'success') {
|
|
const orderId = payResult.orderId || pendingOrder?.orderId;
|
|
if (!orderId) {
|
|
clearWechatPayResultHash();
|
|
return true;
|
|
}
|
|
const order =
|
|
pendingOrder ?? createRechargeOrderLifecycle(orderId, account);
|
|
if (
|
|
isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
clearWechatPayResultHash();
|
|
return true;
|
|
}
|
|
confirmingWechatRechargeOrderRef.current = order;
|
|
setWechatRechargeOrderConfirmationState({ orderId });
|
|
setSubmittingRechargeProductId(null);
|
|
setRechargePaymentResult(null);
|
|
void confirmWechatRechargeOrderUntilSettled(
|
|
orderId,
|
|
createRechargeConfirmationOptions(order),
|
|
)
|
|
.then((response) => {
|
|
if (
|
|
!isRechargeOrderCurrent(order) ||
|
|
!isSameRechargeOrder(
|
|
confirmingWechatRechargeOrderRef.current,
|
|
order,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
const result = buildRechargePaymentResultForOrder(response.order);
|
|
const isPaid = result.kind === 'success';
|
|
if (!applyRechargeCenter(response.center, account, true)) {
|
|
return;
|
|
}
|
|
if (
|
|
isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
}
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
order.abortController.abort();
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
setRechargePaymentResult(result);
|
|
if (isPaid) {
|
|
void onRechargeSuccess?.();
|
|
}
|
|
clearWechatPayResultHash();
|
|
})
|
|
.catch(() => {
|
|
if (
|
|
!isRechargeOrderCurrent(order) ||
|
|
!isSameRechargeOrder(
|
|
confirmingWechatRechargeOrderRef.current,
|
|
order,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
setRechargePaymentResult({
|
|
kind: 'pending',
|
|
title: '支付处理中',
|
|
message: '暂时没能确认到账状态,请稍后查看泥点余额。',
|
|
});
|
|
clearWechatPayResultHash();
|
|
});
|
|
} else if (payResult.status === 'cancel') {
|
|
setRechargePaymentResult({
|
|
kind: 'cancel',
|
|
title: '支付已取消',
|
|
message: '本次没有扣款,泥点余额未发生变化。',
|
|
});
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
refreshRechargeState(account);
|
|
} else {
|
|
const detail = payResult.errorMessage
|
|
? `微信返回:${payResult.errorMessage}`
|
|
: '微信支付没有完成,本次不会入账。';
|
|
setRechargePaymentResult({
|
|
kind: 'failed',
|
|
title: '支付未完成',
|
|
message: detail,
|
|
});
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
refreshRechargeState(account);
|
|
}
|
|
|
|
clearWechatPayResultHash();
|
|
return true;
|
|
}, [
|
|
applyRechargeCenter,
|
|
captureAccountLifecycle,
|
|
createRechargeConfirmationOptions,
|
|
createRechargeOrderLifecycle,
|
|
isAccountLifecycleCurrent,
|
|
isRechargeOrderCurrent,
|
|
isSameRechargeOrder,
|
|
onRechargeSuccess,
|
|
refreshRechargeState,
|
|
]);
|
|
|
|
const pollWechatPayResultFromHash = useCallback(
|
|
() => handleWechatPayResult(),
|
|
[handleWechatPayResult],
|
|
);
|
|
|
|
const confirmPendingWechatRechargeOrder = useCallback(() => {
|
|
if (nativeWechatPayment) {
|
|
return false;
|
|
}
|
|
|
|
const order = pendingWechatRechargeOrderRef.current;
|
|
if (
|
|
!isRechargeOrderCurrent(order) ||
|
|
isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
confirmingWechatRechargeOrderRef.current = order;
|
|
setWechatRechargeOrderConfirmationState({ orderId: order.orderId });
|
|
setRechargePaymentResult(null);
|
|
void confirmWechatRechargeOrderUntilSettled(
|
|
order.orderId,
|
|
createRechargeConfirmationOptions(order),
|
|
)
|
|
.then((response) => {
|
|
if (
|
|
!isRechargeOrderCurrent(order) ||
|
|
!isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
return;
|
|
}
|
|
const result = buildRechargePaymentResultForOrder(response.order);
|
|
const isPaid = result.kind === 'success';
|
|
if (!applyRechargeCenter(response.center, order.account, true)) {
|
|
return;
|
|
}
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
order.abortController.abort();
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
setSubmittingRechargeProductId(null);
|
|
setRechargePaymentResult(result);
|
|
if (isPaid) {
|
|
void onRechargeSuccess?.();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (
|
|
!isRechargeOrderCurrent(order) ||
|
|
!isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
return;
|
|
}
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
setWechatRechargeOrderConfirmationState(null);
|
|
setRechargePaymentResult({
|
|
kind: 'pending',
|
|
title: '支付处理中',
|
|
message: '暂时没能确认到账状态,请稍后查看泥点余额。',
|
|
});
|
|
});
|
|
return true;
|
|
}, [
|
|
applyRechargeCenter,
|
|
createRechargeConfirmationOptions,
|
|
isRechargeOrderCurrent,
|
|
isSameRechargeOrder,
|
|
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;
|
|
}
|
|
const pendingOrder = pendingWechatRechargeOrderRef.current;
|
|
if (
|
|
current &&
|
|
pendingOrder?.orderId === current.orderId &&
|
|
isRechargeOrderCurrent(pendingOrder)
|
|
) {
|
|
pendingOrder.abortController.abort();
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
}
|
|
return null;
|
|
});
|
|
}, [isRechargeOrderCurrent]);
|
|
|
|
const buyRechargeProduct = useCallback(
|
|
(product: ProfileRechargeProduct) => {
|
|
const account = captureAccountLifecycle();
|
|
if (
|
|
submittingRechargeProductId ||
|
|
!account.ownerUserId ||
|
|
!isAccountLifecycleCurrent(account)
|
|
) {
|
|
return;
|
|
}
|
|
const walletSnapshot = captureWalletBalanceSnapshot(account.ownerUserId);
|
|
let createdOrder: WechatRechargeOrderLifecycle | null = null;
|
|
|
|
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 (!isAccountLifecycleCurrent(account)) {
|
|
return;
|
|
}
|
|
const order = createRechargeOrderLifecycle(
|
|
response.order.orderId,
|
|
account,
|
|
);
|
|
abortRechargeOrders(
|
|
pendingWechatRechargeOrderRef.current,
|
|
confirmingWechatRechargeOrderRef.current,
|
|
);
|
|
createdOrder = order;
|
|
pendingWechatRechargeOrderRef.current = order;
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
|
|
if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) {
|
|
if (
|
|
!applyRechargeCenter(
|
|
response.center,
|
|
account,
|
|
true,
|
|
walletSnapshot,
|
|
) ||
|
|
!isRechargeOrderCurrent(order)
|
|
) {
|
|
return;
|
|
}
|
|
const paymentHandled = await requestHostPayment({
|
|
payload: response.wechatMiniProgramPayParams,
|
|
orderId: response.order.orderId,
|
|
});
|
|
if (!isRechargeOrderCurrent(order)) {
|
|
return;
|
|
}
|
|
if (!paymentHandled) {
|
|
throw new Error('请在微信小程序内完成支付');
|
|
}
|
|
return;
|
|
}
|
|
if (paymentChannel === WECHAT_JSAPI_PAYMENT_CHANNEL) {
|
|
if (
|
|
!applyRechargeCenter(
|
|
response.center,
|
|
account,
|
|
true,
|
|
walletSnapshot,
|
|
) ||
|
|
!isRechargeOrderCurrent(order)
|
|
) {
|
|
return;
|
|
}
|
|
setRechargePaymentResult({
|
|
kind: 'pending',
|
|
title: '正在打开微信支付',
|
|
message: '请在微信支付面板中完成支付。',
|
|
});
|
|
await requestWechatJsapiPayment(
|
|
response.wechatMiniProgramPayParams,
|
|
);
|
|
if (!isRechargeOrderCurrent(order)) {
|
|
return;
|
|
}
|
|
setRechargePaymentResult({
|
|
kind: 'pending',
|
|
title: '支付处理中',
|
|
message: '正在查询微信支付到账状态。',
|
|
});
|
|
void confirmWechatRechargeOrderUntilSettled(
|
|
response.order.orderId,
|
|
createRechargeConfirmationOptions(order),
|
|
)
|
|
.then((confirmResponse) => {
|
|
if (!isRechargeOrderCurrent(order)) {
|
|
return;
|
|
}
|
|
const result = buildRechargePaymentResultForOrder(
|
|
confirmResponse.order,
|
|
);
|
|
const isPaid = result.kind === 'success';
|
|
if (
|
|
!applyRechargeCenter(confirmResponse.center, account, true)
|
|
) {
|
|
return;
|
|
}
|
|
setRechargePaymentResult(result);
|
|
if (result.kind !== 'pending') {
|
|
if (
|
|
isSameRechargeOrder(
|
|
pendingWechatRechargeOrderRef.current,
|
|
order,
|
|
)
|
|
) {
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
}
|
|
order.abortController.abort();
|
|
}
|
|
if (isPaid) {
|
|
void onRechargeSuccess?.();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (!isRechargeOrderCurrent(order)) {
|
|
return;
|
|
}
|
|
setRechargePaymentResult({
|
|
kind: 'pending',
|
|
title: '等待微信确认',
|
|
message: '暂时没能确认到账状态,请稍后再试。',
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
if (paymentChannel === WECHAT_H5_PAYMENT_CHANNEL) {
|
|
const h5Url = response.wechatH5Payment?.h5Url?.trim();
|
|
if (!h5Url) {
|
|
throw new Error('微信 H5 支付链接生成失败');
|
|
}
|
|
if (
|
|
!applyRechargeCenter(
|
|
response.center,
|
|
account,
|
|
true,
|
|
walletSnapshot,
|
|
) ||
|
|
!isRechargeOrderCurrent(order)
|
|
) {
|
|
return;
|
|
}
|
|
setRechargePaymentResult({
|
|
kind: 'pending',
|
|
title: '正在打开微信支付',
|
|
message: '完成支付后返回页面确认到账状态。',
|
|
});
|
|
if (!isRechargeOrderCurrent(order)) {
|
|
return;
|
|
}
|
|
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 支付链接生成失败');
|
|
}
|
|
if (
|
|
!applyRechargeCenter(
|
|
response.center,
|
|
account,
|
|
true,
|
|
walletSnapshot,
|
|
) ||
|
|
!isRechargeOrderCurrent(order)
|
|
) {
|
|
return;
|
|
}
|
|
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) => {
|
|
if (!isAccountLifecycleCurrent(account)) {
|
|
return;
|
|
}
|
|
if (
|
|
createdOrder &&
|
|
isSameRechargeOrder(
|
|
pendingWechatRechargeOrderRef.current,
|
|
createdOrder,
|
|
)
|
|
) {
|
|
createdOrder.abortController.abort();
|
|
pendingWechatRechargeOrderRef.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) => {
|
|
if (!isAccountLifecycleCurrent(account)) {
|
|
return;
|
|
}
|
|
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) => {
|
|
if (!isAccountLifecycleCurrent(account)) {
|
|
return;
|
|
}
|
|
setRechargePaymentResult(null);
|
|
setRechargeError(
|
|
wechatLoginError instanceof Error
|
|
? wechatLoginError.message
|
|
: '微信授权暂不可用,请稍后再试',
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
setRechargeError(error instanceof Error ? error.message : '充值失败');
|
|
setSubmittingRechargeProductId(null);
|
|
});
|
|
},
|
|
[
|
|
applyRechargeCenter,
|
|
abortRechargeOrders,
|
|
captureAccountLifecycle,
|
|
captureWalletBalanceSnapshot,
|
|
createRechargeConfirmationOptions,
|
|
createRechargeOrderLifecycle,
|
|
isAccountLifecycleCurrent,
|
|
isRechargeOrderCurrent,
|
|
isSameRechargeOrder,
|
|
onRechargeSuccess,
|
|
submittingRechargeProductId,
|
|
],
|
|
);
|
|
|
|
const confirmNativeWechatPayment = useCallback(() => {
|
|
if (!nativeWechatPayment || nativeWechatPayment.isConfirming) {
|
|
return;
|
|
}
|
|
const order = pendingWechatRechargeOrderRef.current;
|
|
if (
|
|
!isRechargeOrderCurrent(order) ||
|
|
order.orderId !== nativeWechatPayment.orderId
|
|
) {
|
|
return;
|
|
}
|
|
|
|
setNativeWechatPayment((current) =>
|
|
current && current.orderId === nativeWechatPayment.orderId
|
|
? { ...current, isConfirming: true, confirmMessage: undefined }
|
|
: current,
|
|
);
|
|
void confirmWechatRechargeOrderQuickly(
|
|
nativeWechatPayment.orderId,
|
|
createRechargeConfirmationOptions(order),
|
|
)
|
|
.then((response) => {
|
|
if (!isRechargeOrderCurrent(order)) {
|
|
return;
|
|
}
|
|
const result = buildRechargePaymentResultForOrder(response.order);
|
|
const isPaid = result.kind === 'success';
|
|
if (!applyRechargeCenter(response.center, order.account, true)) {
|
|
return;
|
|
}
|
|
if (result.kind !== 'pending') {
|
|
setNativeWechatPayment(null);
|
|
if (
|
|
isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
}
|
|
order.abortController.abort();
|
|
setRechargePaymentResult(result);
|
|
if (isPaid) {
|
|
void onRechargeSuccess?.();
|
|
}
|
|
} else {
|
|
setNativeWechatPayment((current) =>
|
|
current && current.orderId === nativeWechatPayment.orderId
|
|
? {
|
|
...current,
|
|
isConfirming: false,
|
|
confirmMessage: '暂未确认到账,请确认付款完成后再点一次。',
|
|
}
|
|
: current,
|
|
);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (!isRechargeOrderCurrent(order)) {
|
|
return;
|
|
}
|
|
setNativeWechatPayment((current) =>
|
|
current && current.orderId === nativeWechatPayment.orderId
|
|
? {
|
|
...current,
|
|
isConfirming: false,
|
|
confirmMessage: '暂时没能确认到账状态,请稍后再试。',
|
|
}
|
|
: current,
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (isRechargeOrderCurrent(order)) {
|
|
setSubmittingRechargeProductId(null);
|
|
}
|
|
});
|
|
}, [
|
|
applyRechargeCenter,
|
|
createRechargeConfirmationOptions,
|
|
isRechargeOrderCurrent,
|
|
isSameRechargeOrder,
|
|
nativeWechatPayment,
|
|
onRechargeSuccess,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const orderId = nativeWechatPayment?.orderId;
|
|
const expiresAtMs = Date.parse(nativeWechatPayment?.expiresAt ?? '');
|
|
const order = pendingWechatRechargeOrderRef.current;
|
|
if (
|
|
!orderId ||
|
|
!Number.isFinite(expiresAtMs) ||
|
|
!isRechargeOrderCurrent(order) ||
|
|
order.orderId !== orderId
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
let cancelled = false;
|
|
const confirmationOptions = createRechargeConfirmationOptions(order);
|
|
const watchUntilSettled = async () => {
|
|
while (
|
|
!cancelled &&
|
|
Date.now() < expiresAtMs &&
|
|
confirmationOptions.isCurrent()
|
|
) {
|
|
try {
|
|
assertWechatRechargeConfirmationActive(confirmationOptions);
|
|
const response = await watchWechatPlatformProfileRechargeOrder(
|
|
orderId,
|
|
{
|
|
signal: confirmationOptions.signal,
|
|
},
|
|
);
|
|
assertWechatRechargeConfirmationActive(confirmationOptions);
|
|
if (
|
|
cancelled ||
|
|
!response ||
|
|
!isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const result = buildRechargePaymentResultForOrder(response.order);
|
|
if (!applyRechargeCenter(response.center, order.account, true)) {
|
|
return;
|
|
}
|
|
if (result.kind === 'pending') {
|
|
await waitWechatPayConfirmDelay(
|
|
WECHAT_NATIVE_WATCH_RETRY_DELAY_MS,
|
|
confirmationOptions.signal,
|
|
);
|
|
assertWechatRechargeConfirmationActive(confirmationOptions);
|
|
continue;
|
|
}
|
|
|
|
pendingWechatRechargeOrderRef.current = null;
|
|
if (
|
|
isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order)
|
|
) {
|
|
confirmingWechatRechargeOrderRef.current = null;
|
|
}
|
|
order.abortController.abort();
|
|
setNativeWechatPayment((current) =>
|
|
current?.orderId === orderId ? null : current,
|
|
);
|
|
setSubmittingRechargeProductId(null);
|
|
setRechargePaymentResult(result);
|
|
if (result.kind === 'success') {
|
|
void onRechargeSuccess?.();
|
|
}
|
|
return;
|
|
} catch {
|
|
if (
|
|
cancelled ||
|
|
confirmationOptions.signal.aborted ||
|
|
!confirmationOptions.isCurrent()
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
await waitWechatPayConfirmDelay(
|
|
WECHAT_NATIVE_WATCH_RETRY_DELAY_MS,
|
|
confirmationOptions.signal,
|
|
);
|
|
assertWechatRechargeConfirmationActive(confirmationOptions);
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
void watchUntilSettled();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [
|
|
nativeWechatPayment?.expiresAt,
|
|
nativeWechatPayment?.orderId,
|
|
applyRechargeCenter,
|
|
createRechargeConfirmationOptions,
|
|
isRechargeOrderCurrent,
|
|
isSameRechargeOrder,
|
|
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);
|
|
const snapshotOwnerUserId = currentUserId;
|
|
const accountRevision = accountLifecycleRevisionRef.current;
|
|
void redeemPlatformProfileReferralInviteCode(inviteCode)
|
|
.then((response) => {
|
|
if (
|
|
accountRevision !== accountLifecycleRevisionRef.current ||
|
|
currentUserIdRef.current !== snapshotOwnerUserId
|
|
) {
|
|
return;
|
|
}
|
|
setReferralCenter(response.center);
|
|
setReferralRedeemCode('');
|
|
setReferralSuccess('已填写');
|
|
void onWalletBalanceMayHaveChanged();
|
|
void onRechargeSuccess?.();
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (
|
|
accountRevision !== accountLifecycleRevisionRef.current ||
|
|
currentUserIdRef.current !== snapshotOwnerUserId
|
|
) {
|
|
return;
|
|
}
|
|
setReferralError(
|
|
error instanceof Error ? error.message : '填写邀请码失败',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (
|
|
accountRevision === accountLifecycleRevisionRef.current &&
|
|
currentUserIdRef.current === snapshotOwnerUserId
|
|
) {
|
|
setIsSubmittingReferralRedeem(false);
|
|
}
|
|
});
|
|
}, [
|
|
currentUserId,
|
|
isSubmittingReferralRedeem,
|
|
onRechargeSuccess,
|
|
onWalletBalanceMayHaveChanged,
|
|
referralRedeemCode,
|
|
]);
|
|
|
|
const submitRewardCode = useCallback(() => {
|
|
if (isSubmittingRewardCode || !rewardCodeInput.trim()) {
|
|
return;
|
|
}
|
|
|
|
setIsSubmittingRewardCode(true);
|
|
setRewardCodeError(null);
|
|
setRewardCodeSuccess(null);
|
|
const snapshotOwnerUserId = currentUserId;
|
|
const accountRevision = accountLifecycleRevisionRef.current;
|
|
void redeemPlatformProfileRewardCode(rewardCodeInput)
|
|
.then((response: RedeemProfileRewardCodeResponse) => {
|
|
if (
|
|
accountRevision !== accountLifecycleRevisionRef.current ||
|
|
currentUserIdRef.current !== snapshotOwnerUserId
|
|
) {
|
|
return;
|
|
}
|
|
setRewardCodeInput('');
|
|
setRewardCodeSuccess(`已到账 ${response.amountGranted} 泥点`);
|
|
void onWalletBalanceMayHaveChanged();
|
|
void onRechargeSuccess?.();
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (
|
|
accountRevision !== accountLifecycleRevisionRef.current ||
|
|
currentUserIdRef.current !== snapshotOwnerUserId
|
|
) {
|
|
return;
|
|
}
|
|
setRewardCodeError(error instanceof Error ? error.message : '兑换失败');
|
|
})
|
|
.finally(() => {
|
|
if (
|
|
accountRevision === accountLifecycleRevisionRef.current &&
|
|
currentUserIdRef.current === snapshotOwnerUserId
|
|
) {
|
|
setIsSubmittingRewardCode(false);
|
|
}
|
|
});
|
|
}, [
|
|
currentUserId,
|
|
isSubmittingRewardCode,
|
|
onRechargeSuccess,
|
|
onWalletBalanceMayHaveChanged,
|
|
rewardCodeInput,
|
|
]);
|
|
|
|
const rechargeModalCenter = useMemo(() => {
|
|
if (!walletOwnerMatchesCurrentUser || !rechargeCenter) {
|
|
return null;
|
|
}
|
|
return {
|
|
...rechargeCenter,
|
|
walletBalance:
|
|
mudPointBalance?.totalPoints ?? rechargeCenter.walletBalance,
|
|
mudPointBalance: mudPointBalance ?? rechargeCenter.mudPointBalance,
|
|
};
|
|
}, [mudPointBalance, rechargeCenter, walletOwnerMatchesCurrentUser]);
|
|
|
|
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: rechargeModalCenter,
|
|
rechargeError,
|
|
rechargePaymentResult,
|
|
referralCenter,
|
|
referralError,
|
|
referralRedeemCode,
|
|
referralSuccess,
|
|
rewardCodeError,
|
|
rewardCodeInput,
|
|
rewardCodeSuccess,
|
|
setIsRechargeOpen,
|
|
setIsRewardCodeOpen,
|
|
setIsWalletLedgerOpen,
|
|
setRechargePaymentResult,
|
|
setReferralRedeemCode,
|
|
setRewardCodeInput,
|
|
showRechargeEntry,
|
|
submittingRechargeProductId,
|
|
submitReferralRedeemCode,
|
|
submitRewardCode,
|
|
walletLedger,
|
|
walletLedgerError,
|
|
wechatRechargeOrderConfirmationState,
|
|
buyRechargeProduct,
|
|
copyInviteInfo,
|
|
};
|
|
}
|