Merge branch 'hermes/wechat'
# Conflicts: # .hermes/shared-memory/decision-log.md # docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md # docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md # server-rs/crates/module-runtime/src/errors.rs # src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx # src/components/rpg-entry/RpgEntryHomeView.tsx
This commit is contained in:
@@ -37,6 +37,8 @@ import {
|
||||
} from './rpgEntryWorldPresentation';
|
||||
|
||||
const {
|
||||
mockQrCodeToDataUrl,
|
||||
mockRedirectToPaymentUrl,
|
||||
mockBuildReferralCenter,
|
||||
mockBuildTaskCenter,
|
||||
mockClaimRpgProfileTaskReward,
|
||||
@@ -48,6 +50,8 @@ const {
|
||||
mockGetRpgProfileWalletLedger,
|
||||
mockRedeemRpgProfileReferralInviteCode,
|
||||
} = vi.hoisted(() => {
|
||||
const qrCodeToDataUrl = vi.fn(async () => 'data:image/png;base64,QR');
|
||||
const redirectToPaymentUrl = vi.fn();
|
||||
const buildReferralCenter = (
|
||||
overrides: Partial<ProfileReferralInviteCenterResponse> = {},
|
||||
): ProfileReferralInviteCenterResponse => ({
|
||||
@@ -119,6 +123,8 @@ const {
|
||||
});
|
||||
|
||||
return {
|
||||
mockQrCodeToDataUrl: qrCodeToDataUrl,
|
||||
mockRedirectToPaymentUrl: redirectToPaymentUrl,
|
||||
mockBuildReferralCenter: buildReferralCenter,
|
||||
mockBuildTaskCenter: buildTaskCenter,
|
||||
mockGetRpgProfileReferralInviteCenter: vi.fn(async () =>
|
||||
@@ -343,6 +349,16 @@ vi.mock('../../services/authService', () => ({
|
||||
updateAuthProfile: mockUpdateAuthProfile,
|
||||
}));
|
||||
|
||||
vi.mock('qrcode', () => ({
|
||||
default: {
|
||||
toDataURL: mockQrCodeToDataUrl,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/payment/paymentRedirect', () => ({
|
||||
redirectToPaymentUrl: mockRedirectToPaymentUrl,
|
||||
}));
|
||||
|
||||
mockUpdateAuthProfile.mockResolvedValue({
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
@@ -385,6 +401,8 @@ vi.mock('../ResolvedAssetImage', () => ({
|
||||
}));
|
||||
|
||||
const originalMatchMedia = window.matchMedia;
|
||||
const originalUserAgent = navigator.userAgent;
|
||||
const originalMaxTouchPoints = navigator.maxTouchPoints;
|
||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||
const originalCancelAnimationFrame = window.cancelAnimationFrame;
|
||||
|
||||
@@ -584,12 +602,56 @@ function buildBabyObjectMatchEntry(
|
||||
}
|
||||
|
||||
function mockDesktopLayout() {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
configurable: true,
|
||||
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||
});
|
||||
Object.defineProperty(navigator, 'maxTouchPoints', {
|
||||
configurable: true,
|
||||
value: 0,
|
||||
});
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => {
|
||||
const normalizedQuery = query.replace(/\s/g, '');
|
||||
return {
|
||||
matches:
|
||||
normalizedQuery.includes('min-width:1024px') ||
|
||||
normalizedQuery.includes('min-width:1024'),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function mockWechatDesktopLayout() {
|
||||
mockDesktopLayout();
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
configurable: true,
|
||||
value:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 MicroMessenger/8.0',
|
||||
});
|
||||
}
|
||||
|
||||
function mockWechatMobileLayout() {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
configurable: true,
|
||||
value:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit MicroMessenger/8.0 Mobile',
|
||||
});
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(() => ({
|
||||
matches: true,
|
||||
media: '(min-width: 1024px)',
|
||||
media: '(max-width: 767px)',
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
@@ -676,7 +738,10 @@ function renderProfileView(
|
||||
}
|
||||
|
||||
async function openRechargeModal(user: ReturnType<typeof userEvent.setup>) {
|
||||
await user.click(screen.getByRole('button', { name: /充值\s*泥点\/会员/u }));
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||
);
|
||||
}
|
||||
|
||||
function renderLoggedOutHomeView(
|
||||
@@ -981,11 +1046,21 @@ afterEach(() => {
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
mockQrCodeToDataUrl.mockResolvedValue('data:image/png;base64,QR');
|
||||
mockRedirectToPaymentUrl.mockReset();
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: originalMatchMedia,
|
||||
});
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
configurable: true,
|
||||
value: originalUserAgent,
|
||||
});
|
||||
Object.defineProperty(navigator, 'maxTouchPoints', {
|
||||
configurable: true,
|
||||
value: originalMaxTouchPoints,
|
||||
});
|
||||
Object.defineProperty(window, 'requestAnimationFrame', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
@@ -1017,12 +1092,49 @@ test('opens wallet ledger modal from narrative coin card', async () => {
|
||||
expect(screen.getByText('+30')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('profile recharge modal buys points through mock channel outside mini program', async () => {
|
||||
test('profile recharge modal shows native qr code on desktop web by default', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRechargeSuccess = vi.fn();
|
||||
mockWechatDesktopLayout();
|
||||
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-native-1',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_native',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
},
|
||||
wechatNativePayment: {
|
||||
codeUrl: 'weixin://pay.weixin.qq.com/bizpayurl/up?pr=native-test',
|
||||
},
|
||||
});
|
||||
|
||||
renderProfileView(onRechargeSuccess);
|
||||
await openRechargeModal(user);
|
||||
renderProfileView();
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('账户充值')).toBeTruthy();
|
||||
expect(mockGetRpgProfileRechargeCenter).toHaveBeenCalledTimes(1);
|
||||
@@ -1031,16 +1143,84 @@ test('profile recharge modal buys points through mock channel outside mini progr
|
||||
await waitFor(() => {
|
||||
expect(mockCreateRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
||||
'points_60',
|
||||
'mock',
|
||||
'wechat_native',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||
expect(screen.getByText('已到账,账户状态已刷新。')).toBeTruthy();
|
||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(await screen.findByText('微信扫码支付')).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText('微信 Native 支付二维码')).toBeTruthy();
|
||||
});
|
||||
expect(mockQrCodeToDataUrl).toHaveBeenCalledWith(
|
||||
'weixin://pay.weixin.qq.com/bizpayurl/up?pr=native-test',
|
||||
expect.objectContaining({ width: 180 }),
|
||||
);
|
||||
expect(screen.queryByRole('dialog', { name: '支付成功' })).toBeNull();
|
||||
});
|
||||
|
||||
test('profile recharge modal jumps to h5 payment on mobile web by default', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWechatMobileLayout();
|
||||
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-h5-1',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_h5',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
},
|
||||
wechatH5Payment: {
|
||||
h5Url:
|
||||
'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx-h5',
|
||||
},
|
||||
});
|
||||
|
||||
renderProfileView();
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||
);
|
||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
||||
'points_60',
|
||||
'wechat_h5',
|
||||
);
|
||||
});
|
||||
expect(mockRedirectToPaymentUrl).toHaveBeenCalledWith(
|
||||
'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx-h5',
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '正在打开微信支付' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: '支付成功' })).toBeNull();
|
||||
});
|
||||
|
||||
test('profile recharge modal trusts per-product first bonus display after points recharge', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWechatDesktopLayout();
|
||||
mockGetRpgProfileRechargeCenter.mockResolvedValueOnce({
|
||||
walletBalance: 60,
|
||||
membership: {
|
||||
@@ -1158,6 +1338,11 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
|
||||
expect(navigateUrl).toContain('order-wechat-1');
|
||||
expect(decodeURIComponent(navigateUrl)).toContain('prepay_id=wx-prepay');
|
||||
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||
expect(mockCreateRpgProfileRechargeOrder).not.toHaveBeenCalledWith(
|
||||
'points_60',
|
||||
'mock',
|
||||
);
|
||||
expect(mockRedirectToPaymentUrl).not.toHaveBeenCalled();
|
||||
expect(screen.getByText('已到账,账户状态已刷新。')).toBeTruthy();
|
||||
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
||||
'order-wechat-1',
|
||||
@@ -1476,6 +1661,110 @@ test('profile recharge modal releases submitting state after cancelled wechat pa
|
||||
expect(mockConfirmWechatRpgProfileRechargeOrder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('profile native qr confirmation refreshes only after server reports paid', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRechargeSuccess = vi.fn();
|
||||
mockWechatDesktopLayout();
|
||||
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-native-paid',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_native',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
},
|
||||
wechatNativePayment: {
|
||||
codeUrl: 'weixin://pay.weixin.qq.com/bizpayurl/up?pr=native-paid',
|
||||
},
|
||||
});
|
||||
mockConfirmWechatRpgProfileRechargeOrder.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-native-paid',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'paid' as const,
|
||||
paymentChannel: 'wechat_native',
|
||||
paidAt: '2026-04-25T10:01:00Z',
|
||||
providerTransactionId: 'wx-native-1',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 120,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 120,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: true,
|
||||
},
|
||||
});
|
||||
|
||||
renderProfileView(onRechargeSuccess);
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||
);
|
||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||
await user.click(await screen.findByRole('button', { name: '我已支付' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
||||
'order-native-paid',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('non-wechat profile shows reward code instead of recharge entry', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView();
|
||||
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
expect(
|
||||
within(shortcutRegion).queryByRole('button', { name: /充值/u }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: /兑换码/u }),
|
||||
).toBeTruthy();
|
||||
await user.click(within(shortcutRegion).getByRole('button', { name: /兑换码/u }));
|
||||
expect(await screen.findByPlaceholderText('输入兑换码')).toBeTruthy();
|
||||
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('profile daily task shortcut opens task center and claims reward', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRechargeSuccess = vi.fn();
|
||||
@@ -1731,7 +2020,10 @@ test('opens reward code modal from profile action on mobile', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView();
|
||||
await user.click(screen.getByRole('button', { name: /兑换码/u }));
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /兑换码/u }),
|
||||
);
|
||||
|
||||
const modal = await screen.findByPlaceholderText('输入兑换码');
|
||||
expect(modal).toBeTruthy();
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
import communityQqQrImage from '../../../media/social-media-group/qq.png';
|
||||
import communityWechatQrImage from '../../../media/social-media-group/wechat.png';
|
||||
@@ -57,6 +58,7 @@ import type {
|
||||
ProfileRechargeCenterResponse,
|
||||
ProfileRechargeProduct,
|
||||
ProfileReferralInviteCenterResponse,
|
||||
WechatNativePayment,
|
||||
ProfileSaveArchiveSummary,
|
||||
ProfileTaskCenterResponse,
|
||||
ProfileTaskItem,
|
||||
@@ -73,6 +75,14 @@ import {
|
||||
updateAuthProfile,
|
||||
} from '../../services/authService';
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import {
|
||||
resolveProfileRechargePaymentChannel,
|
||||
shouldShowRechargeEntry,
|
||||
WECHAT_H5_PAYMENT_CHANNEL,
|
||||
WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL,
|
||||
WECHAT_NATIVE_PAYMENT_CHANNEL,
|
||||
} from '../../services/payment/paymentPlatform';
|
||||
import { redirectToPaymentUrl } from '../../services/payment/paymentRedirect';
|
||||
import {
|
||||
claimRpgProfileTaskReward,
|
||||
confirmWechatRpgProfileRechargeOrder,
|
||||
@@ -217,9 +227,9 @@ const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
|
||||
const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
|
||||
const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
|
||||
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
||||
const WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL = 'wechat_mp';
|
||||
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_NATIVE_PAY_QR_IMAGE_SIZE = 180;
|
||||
|
||||
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
||||
type RechargeTab = 'points' | 'membership';
|
||||
@@ -235,6 +245,10 @@ type RechargePaymentResult = {
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
type NativeWechatPaymentState = WechatNativePayment & {
|
||||
orderId: string;
|
||||
isConfirming: boolean;
|
||||
};
|
||||
type DiscoverChannel =
|
||||
| 'recommend'
|
||||
| 'today'
|
||||
@@ -2527,18 +2541,6 @@ function formatRechargePrice(priceCents: number) {
|
||||
return `¥${Number.isInteger(yuan) ? yuan.toFixed(0) : yuan.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function isWechatMiniProgramWebView() {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return (
|
||||
params.get('clientRuntime') === 'wechat_mini_program' ||
|
||||
params.get('clientType') === 'mini_program'
|
||||
);
|
||||
}
|
||||
|
||||
function clearWechatPayResultHash() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -2685,6 +2687,36 @@ async function confirmWechatRechargeOrderUntilSettled(
|
||||
return latestResponse;
|
||||
}
|
||||
|
||||
function useWechatNativeQrCode(codeUrl: string | null) {
|
||||
const [qrImageUrl, setQrImageUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setQrImageUrl(null);
|
||||
if (!codeUrl) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
void QRCode.toDataURL(codeUrl, {
|
||||
errorCorrectionLevel: 'M',
|
||||
margin: 1,
|
||||
width: WECHAT_NATIVE_PAY_QR_IMAGE_SIZE,
|
||||
}).then((dataUrl) => {
|
||||
if (!cancelled) {
|
||||
setQrImageUrl(dataUrl);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [codeUrl]);
|
||||
|
||||
return qrImageUrl;
|
||||
}
|
||||
|
||||
function RechargeProductCard({
|
||||
product,
|
||||
submittingProductId,
|
||||
@@ -2737,22 +2769,29 @@ function ProfileRechargeModal({
|
||||
isLoading,
|
||||
error,
|
||||
submittingProductId,
|
||||
nativePayment,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
onClose,
|
||||
onRetry,
|
||||
onBuy,
|
||||
onConfirmNativePayment,
|
||||
}: {
|
||||
center: ProfileRechargeCenterResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
submittingProductId: string | null;
|
||||
nativePayment: NativeWechatPaymentState | null;
|
||||
activeTab: RechargeTab;
|
||||
onTabChange: (tab: RechargeTab) => void;
|
||||
onClose: () => void;
|
||||
onRetry: () => void;
|
||||
onBuy: (product: ProfileRechargeProduct) => void;
|
||||
onConfirmNativePayment: () => void;
|
||||
}) {
|
||||
const nativeQrImageUrl = useWechatNativeQrCode(
|
||||
nativePayment?.codeUrl ?? null,
|
||||
);
|
||||
const products =
|
||||
activeTab === 'points'
|
||||
? (center?.pointProducts ?? [])
|
||||
@@ -2841,6 +2880,33 @@ function ProfileRechargeModal({
|
||||
暂无可购买套餐
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nativePayment ? (
|
||||
<div className="platform-subpanel mt-4 rounded-2xl px-4 py-4 text-center">
|
||||
<div className="text-sm font-black">微信扫码支付</div>
|
||||
<div className="mx-auto mt-3 flex h-[180px] w-[180px] items-center justify-center rounded-xl bg-white p-2">
|
||||
{nativeQrImageUrl ? (
|
||||
<img
|
||||
src={nativeQrImageUrl}
|
||||
alt="微信 Native 支付二维码"
|
||||
className="h-full w-full"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs font-semibold text-slate-500">
|
||||
生成中
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirmNativePayment}
|
||||
disabled={nativePayment.isConfirming}
|
||||
className="platform-primary-button mt-4 rounded-2xl px-4 py-2 text-xs font-black disabled:cursor-wait disabled:opacity-60"
|
||||
>
|
||||
{nativePayment.isConfirming ? '确认中' : '我已支付'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3594,6 +3660,7 @@ export function RpgEntryHomeView({
|
||||
hasUnreadDraftUpdate = false,
|
||||
}: RpgEntryHomeViewProps) {
|
||||
const authUi = useAuthUi();
|
||||
const showRechargeEntry = shouldShowRechargeEntry();
|
||||
const [desktopSearchKeyword, setDesktopSearchKeyword] = useState('');
|
||||
const [mobileSearchKeyword, setMobileSearchKeyword] = useState('');
|
||||
const [activeWorkSearchKeyword, setActiveWorkSearchKeyword] = useState('');
|
||||
@@ -3611,6 +3678,8 @@ export function RpgEntryHomeView({
|
||||
const [rechargeError, setRechargeError] = useState<string | null>(null);
|
||||
const [rechargePaymentResult, setRechargePaymentResult] =
|
||||
useState<RechargePaymentResult | null>(null);
|
||||
const [nativeWechatPayment, setNativeWechatPayment] =
|
||||
useState<NativeWechatPaymentState | null>(null);
|
||||
const [activeRechargeTab, setActiveRechargeTab] =
|
||||
useState<RechargeTab>('points');
|
||||
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
|
||||
@@ -4149,6 +4218,7 @@ export function RpgEntryHomeView({
|
||||
loadRechargeCenter();
|
||||
setSubmittingRechargeProductId(null);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
setNativeWechatPayment(null);
|
||||
}, [loadRechargeCenter]);
|
||||
const handleWechatPayResult = useCallback(() => {
|
||||
const payResult = readWechatPayResultFromHash();
|
||||
@@ -4232,16 +4302,24 @@ export function RpgEntryHomeView({
|
||||
setIsRechargeOpen(true);
|
||||
loadRechargeCenter();
|
||||
};
|
||||
const openRechargeOrRewardCodeModal = () => {
|
||||
if (showRechargeEntry) {
|
||||
openRechargeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
openRewardCodeModal();
|
||||
};
|
||||
const buyRechargeProduct = (product: ProfileRechargeProduct) => {
|
||||
if (submittingRechargeProductId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentChannel = isWechatMiniProgramWebView()
|
||||
? WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL
|
||||
: 'mock';
|
||||
const paymentChannel = resolveProfileRechargePaymentChannel();
|
||||
setSubmittingRechargeProductId(product.productId);
|
||||
setRechargeError(null);
|
||||
setRechargePaymentResult(null);
|
||||
setNativeWechatPayment(null);
|
||||
void createRpgProfileRechargeOrder(product.productId, paymentChannel)
|
||||
.then(async (response) => {
|
||||
if (paymentChannel === WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL) {
|
||||
@@ -4252,24 +4330,105 @@ export function RpgEntryHomeView({
|
||||
);
|
||||
setRechargeCenter(response.center);
|
||||
return;
|
||||
} else {
|
||||
}
|
||||
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: 'success',
|
||||
title: '支付成功',
|
||||
message: '已到账,账户状态已刷新。',
|
||||
kind: 'pending',
|
||||
title: '正在打开微信支付',
|
||||
message: '完成支付后返回页面确认到账状态。',
|
||||
});
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
setSubmittingRechargeProductId(null);
|
||||
redirectToPaymentUrl(h5Url);
|
||||
return;
|
||||
}
|
||||
void onRechargeSuccess?.();
|
||||
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);
|
||||
});
|
||||
};
|
||||
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]);
|
||||
useEffect(() => {
|
||||
const handleResume = () => {
|
||||
handleWechatPayResult();
|
||||
@@ -5569,13 +5728,21 @@ export function RpgEntryHomeView({
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={openRechargeModal}
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
className="platform-profile-action flex shrink-0 items-center gap-2 rounded-[1.1rem] px-3 py-2 text-left"
|
||||
>
|
||||
<Coins className="h-4 w-4" />
|
||||
{showRechargeEntry ? (
|
||||
<Coins className="h-4 w-4" />
|
||||
) : (
|
||||
<Ticket className="h-4 w-4" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xs font-bold">充值</div>
|
||||
<div className="text-[10px] opacity-80">泥点/会员</div>
|
||||
<div className="text-xs font-bold">
|
||||
{showRechargeEntry ? '充值' : '兑换码'}
|
||||
</div>
|
||||
<div className="text-[10px] opacity-80">
|
||||
{showRechargeEntry ? '泥点/会员' : '福利奖励'}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 opacity-80" />
|
||||
</button>
|
||||
@@ -5659,11 +5826,19 @@ export function RpgEntryHomeView({
|
||||
onClick={openTaskCenterPanel}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="兑换码"
|
||||
subLabel="福利奖励"
|
||||
icon={Ticket}
|
||||
onClick={openRewardCodeModal}
|
||||
label={showRechargeEntry ? '充值' : '兑换码'}
|
||||
subLabel={showRechargeEntry ? '泥点/会员' : '福利奖励'}
|
||||
icon={showRechargeEntry ? Coins : Ticket}
|
||||
onClick={openRechargeOrRewardCodeModal}
|
||||
/>
|
||||
{showRechargeEntry ? (
|
||||
<ProfileShortcutButton
|
||||
label="兑换码"
|
||||
subLabel="福利奖励"
|
||||
icon={Ticket}
|
||||
onClick={openRewardCodeModal}
|
||||
/>
|
||||
) : null}
|
||||
<ProfileShortcutButton
|
||||
label="邀请好友"
|
||||
subLabel={
|
||||
@@ -6149,11 +6324,13 @@ export function RpgEntryHomeView({
|
||||
isLoading={isLoadingRechargeCenter}
|
||||
error={rechargeError}
|
||||
submittingProductId={submittingRechargeProductId}
|
||||
nativePayment={nativeWechatPayment}
|
||||
activeTab={activeRechargeTab}
|
||||
onTabChange={setActiveRechargeTab}
|
||||
onClose={() => setIsRechargeOpen(false)}
|
||||
onRetry={loadRechargeCenter}
|
||||
onBuy={buyRechargeProduct}
|
||||
onConfirmNativePayment={confirmNativeWechatPayment}
|
||||
/>
|
||||
) : null;
|
||||
const rechargePaymentResultModal: ReactNode = rechargePaymentResult ? (
|
||||
|
||||
118
src/services/payment/paymentPlatform.test.ts
Normal file
118
src/services/payment/paymentPlatform.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveProfileRechargePaymentChannel,
|
||||
shouldShowRechargeEntry,
|
||||
WECHAT_H5_PAYMENT_CHANNEL,
|
||||
WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL,
|
||||
WECHAT_NATIVE_PAYMENT_CHANNEL,
|
||||
} from './paymentPlatform';
|
||||
|
||||
describe('resolveProfileRechargePaymentChannel', () => {
|
||||
test('小程序运行态选择 wechat_mp', () => {
|
||||
expect(
|
||||
resolveProfileRechargePaymentChannel({
|
||||
location: { search: '?clientRuntime=wechat_mini_program' },
|
||||
navigator: { userAgent: 'Mozilla/5.0 (iPhone)' },
|
||||
}),
|
||||
).toBe(WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL);
|
||||
});
|
||||
|
||||
test('移动网页选择 wechat_h5', () => {
|
||||
expect(
|
||||
resolveProfileRechargePaymentChannel({
|
||||
location: { search: '' },
|
||||
navigator: {
|
||||
userAgent:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile',
|
||||
},
|
||||
}),
|
||||
).toBe(WECHAT_H5_PAYMENT_CHANNEL);
|
||||
});
|
||||
|
||||
test('微信内 H5 首版仍选择 wechat_h5', () => {
|
||||
expect(
|
||||
resolveProfileRechargePaymentChannel({
|
||||
location: { search: '' },
|
||||
navigator: {
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Linux; Android 14) AppleWebKit MicroMessenger/8.0 Mobile',
|
||||
},
|
||||
}),
|
||||
).toBe(WECHAT_H5_PAYMENT_CHANNEL);
|
||||
});
|
||||
|
||||
test('桌面网页选择 wechat_native', () => {
|
||||
expect(
|
||||
resolveProfileRechargePaymentChannel({
|
||||
location: { search: '' },
|
||||
navigator: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' },
|
||||
matchMedia: () => ({ matches: false }) as unknown as MediaQueryList,
|
||||
}),
|
||||
).toBe(WECHAT_NATIVE_PAYMENT_CHANNEL);
|
||||
});
|
||||
|
||||
test('桌面微信内网页选择 wechat_native', () => {
|
||||
expect(
|
||||
resolveProfileRechargePaymentChannel({
|
||||
location: { search: '' },
|
||||
navigator: {
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit MicroMessenger/8.0',
|
||||
},
|
||||
matchMedia: () => ({ matches: false }) as unknown as MediaQueryList,
|
||||
}),
|
||||
).toBe(WECHAT_NATIVE_PAYMENT_CHANNEL);
|
||||
});
|
||||
|
||||
test('默认路径永远不会解析成 mock', () => {
|
||||
expect(
|
||||
resolveProfileRechargePaymentChannel({
|
||||
location: { search: '' },
|
||||
navigator: { userAgent: '' },
|
||||
matchMedia: () => ({ matches: false }) as unknown as MediaQueryList,
|
||||
}),
|
||||
).not.toBe('mock');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowRechargeEntry', () => {
|
||||
test('小程序运行态显示充值入口', () => {
|
||||
expect(
|
||||
shouldShowRechargeEntry({
|
||||
location: { search: '?clientRuntime=wechat_mini_program' },
|
||||
navigator: { userAgent: 'Mozilla/5.0 (iPhone)' },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('微信内网页显示充值入口', () => {
|
||||
expect(
|
||||
shouldShowRechargeEntry({
|
||||
location: { search: '' },
|
||||
navigator: {
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Linux; Android 14) AppleWebKit MicroMessenger/8.0 Mobile',
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('普通浏览器不显示充值入口', () => {
|
||||
expect(
|
||||
shouldShowRechargeEntry({
|
||||
location: { search: '' },
|
||||
navigator: {
|
||||
userAgent:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile',
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldShowRechargeEntry({
|
||||
location: { search: '' },
|
||||
navigator: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
95
src/services/payment/paymentPlatform.ts
Normal file
95
src/services/payment/paymentPlatform.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export const WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL = 'wechat_mp';
|
||||
export const WECHAT_H5_PAYMENT_CHANNEL = 'wechat_h5';
|
||||
export const WECHAT_NATIVE_PAYMENT_CHANNEL = 'wechat_native';
|
||||
export const MOCK_PAYMENT_CHANNEL = 'mock';
|
||||
|
||||
export type ProfileRechargeWechatPaymentChannel =
|
||||
| typeof WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL
|
||||
| typeof WECHAT_H5_PAYMENT_CHANNEL
|
||||
| typeof WECHAT_NATIVE_PAYMENT_CHANNEL;
|
||||
|
||||
type PaymentPlatformNavigator = Pick<Navigator, 'userAgent' | 'maxTouchPoints'>;
|
||||
|
||||
export type PaymentPlatformContext = {
|
||||
location?: Pick<Location, 'search'> | null;
|
||||
navigator?: Partial<PaymentPlatformNavigator> | null;
|
||||
matchMedia?: Window['matchMedia'] | null;
|
||||
};
|
||||
|
||||
export function shouldShowRechargeEntry(
|
||||
context: PaymentPlatformContext = {},
|
||||
) {
|
||||
const location =
|
||||
context.location ?? (typeof window !== 'undefined' ? window.location : null);
|
||||
const navigatorLike =
|
||||
context.navigator ?? (typeof navigator !== 'undefined' ? navigator : null);
|
||||
|
||||
return (
|
||||
isWechatMiniProgramRuntime(location) ||
|
||||
isWechatBrowserRuntime(navigatorLike)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveProfileRechargePaymentChannel(
|
||||
context: PaymentPlatformContext = {},
|
||||
): ProfileRechargeWechatPaymentChannel {
|
||||
const location =
|
||||
context.location ??
|
||||
(typeof window !== 'undefined' ? window.location : null);
|
||||
const navigatorLike =
|
||||
context.navigator ?? (typeof navigator !== 'undefined' ? navigator : null);
|
||||
const matchMedia =
|
||||
context.matchMedia ??
|
||||
(typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
||||
? window.matchMedia.bind(window)
|
||||
: null);
|
||||
|
||||
if (isWechatMiniProgramRuntime(location)) {
|
||||
return WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL;
|
||||
}
|
||||
|
||||
if (isMobileWebRuntime(navigatorLike, matchMedia)) {
|
||||
return WECHAT_H5_PAYMENT_CHANNEL;
|
||||
}
|
||||
|
||||
return WECHAT_NATIVE_PAYMENT_CHANNEL;
|
||||
}
|
||||
|
||||
export function isManualMockPaymentChannel(paymentChannel: string) {
|
||||
return paymentChannel.trim() === MOCK_PAYMENT_CHANNEL;
|
||||
}
|
||||
|
||||
function isWechatMiniProgramRuntime(
|
||||
location: Pick<Location, 'search'> | null | undefined,
|
||||
) {
|
||||
const params = new URLSearchParams(location?.search ?? '');
|
||||
return (
|
||||
params.get('clientRuntime') === 'wechat_mini_program' ||
|
||||
params.get('clientType') === 'mini_program'
|
||||
);
|
||||
}
|
||||
|
||||
function isWechatBrowserRuntime(
|
||||
navigatorLike: Partial<PaymentPlatformNavigator> | null | undefined,
|
||||
) {
|
||||
return (
|
||||
navigatorLike?.userAgent?.toLowerCase().includes('micromessenger') ??
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function isMobileWebRuntime(
|
||||
navigatorLike: Partial<PaymentPlatformNavigator> | null | undefined,
|
||||
matchMedia: Window['matchMedia'] | null | undefined,
|
||||
) {
|
||||
const userAgent = navigatorLike?.userAgent?.toLowerCase() ?? '';
|
||||
if (/android|iphone|ipad|ipod|mobile|windows phone/u.test(userAgent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((navigatorLike?.maxTouchPoints ?? 0) > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(matchMedia?.('(max-width: 767px)').matches);
|
||||
}
|
||||
3
src/services/payment/paymentRedirect.ts
Normal file
3
src/services/payment/paymentRedirect.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function redirectToPaymentUrl(url: string) {
|
||||
window.location.assign(url);
|
||||
}
|
||||
@@ -67,9 +67,7 @@ export function getRpgProfileDashboard(options: RuntimeRequestOptions = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getRpgProfileWalletLedger(
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
export function getRpgProfileWalletLedger(options: RuntimeRequestOptions = {}) {
|
||||
return requestRpgRuntimeJson<ProfileWalletLedgerResponse>(
|
||||
'/profile/wallet-ledger',
|
||||
{ method: 'GET' },
|
||||
@@ -91,7 +89,7 @@ export function getRpgProfileRechargeCenter(
|
||||
|
||||
export function createRpgProfileRechargeOrder(
|
||||
productId: string,
|
||||
paymentChannel = 'mock',
|
||||
paymentChannel: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRpgRuntimeJson<CreateProfileRechargeOrderResponse>(
|
||||
@@ -227,12 +225,13 @@ export async function resumeRpgProfileSaveArchive(
|
||||
worldKey: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
const response = await requestRpgRuntimeJson<ProfileSaveArchiveResumeResponse>(
|
||||
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
|
||||
{ method: 'POST' },
|
||||
'恢复存档失败',
|
||||
options,
|
||||
);
|
||||
const response =
|
||||
await requestRpgRuntimeJson<ProfileSaveArchiveResumeResponse>(
|
||||
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
|
||||
{ method: 'POST' },
|
||||
'恢复存档失败',
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
entry: response.entry,
|
||||
|
||||
Reference in New Issue
Block a user