平台入口与图片编辑器接入会员购买与升级

- platformProfileClient 新增会员升级报价接口调用
- usePlatformProfileCenterController 增加报价状态与会员下单入口
- PlatformEntryActiveFlowShell 与 ImageCanvasEditorView 向共享弹层透传报价与会员下单回调
- 同步修正平台入口与图片编辑器顶栏受入口文案变化影响的测试断言

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-10-03 12:12:12 +08:00
parent 9ba3aec026
commit f33d11b050
7 changed files with 130 additions and 19 deletions
@@ -662,7 +662,7 @@ describe('ImageCanvasEditorView', () => {
</AuthUiContext.Provider>,
);
expect(await screen.findByLabelText('泥点 1,234')).toBeTruthy();
expect(await screen.findByLabelText(/可用总额 1,234/)).toBeTruthy();
expect(getPlatformProfileDashboardMock).not.toHaveBeenCalled();
});
@@ -729,12 +729,12 @@ describe('ImageCanvasEditorView', () => {
);
const walletButton = await screen.findByRole('button', {
name: '泥点 --',
name: /可用总额 --/,
});
await user.hover(walletButton);
const details = await screen.findByRole('dialog', {
name: '泥点账户详情',
name: '会员与泥点账户详情',
});
expect(within(details).getByText('泥点明细读取失败')).toBeTruthy();
expect(within(details).queryByText('充值中心响应缺少泥点余额')).toBeNull();
@@ -764,7 +764,7 @@ describe('ImageCanvasEditorView', () => {
);
expect(
(await screen.findByRole('button', { name: '泥点 --' })).getAttribute(
(await screen.findByRole('button', { name: /可用总额 --/ })).getAttribute(
'aria-busy',
),
).toBe('true');
@@ -807,13 +807,13 @@ describe('ImageCanvasEditorView', () => {
);
const walletButton = await screen.findByRole('button', {
name: '泥点 1,234',
name: /可用总额 1,234/,
});
await user.hover(walletButton);
const details = await screen.findByRole('dialog', {
name: '泥点账户详情',
name: '会员与泥点账户详情',
});
expect(within(details).getByText('永久泥点')).toBeTruthy();
expect(within(details).getByText('月度泥点')).toBeTruthy();
@@ -911,11 +911,11 @@ describe('ImageCanvasEditorView', () => {
const viewport = screen.getByLabelText('画布工作区');
fireEvent.click(
await screen.findByRole('button', {
name: '充值',
name: '会员与泥点',
}),
);
expect(
await screen.findByRole('dialog', { name: '购买更多泥点' }),
await screen.findByRole('dialog', { name: '会员与泥点' }),
).toBeTruthy();
fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' });
@@ -941,7 +941,7 @@ describe('ImageCanvasEditorView', () => {
).toBe('true');
expect(viewport.getAttribute('aria-disabled')).toBe('true');
fireEvent.click(screen.getByRole('button', { name: '关闭购买更多泥点' }));
fireEvent.click(screen.getByRole('button', { name: '关闭会员与泥点' }));
const resumedToolbar = await screen.findByRole('toolbar', {
name: '快速编辑框选工具',
@@ -561,6 +561,7 @@ export function ImageCanvasEditorView({
buyRechargeProduct,
closeNativeWechatPayment,
confirmNativeWechatPayment,
isLoadingMembershipQuote,
isLoadingRechargeCenter,
isLoadingWalletLedger,
isRechargeOpen,
@@ -569,12 +570,15 @@ export function ImageCanvasEditorView({
isWalletLedgerOpen,
loadRechargeCenter,
loadWalletLedger,
membershipQuote,
membershipQuoteError,
nativeWechatPayment,
openRechargeOrRewardCodeModal,
openWalletLedgerPanel,
rechargeCenter,
rechargeError,
rechargePaymentResult,
requestMembershipQuote,
rewardCodeError,
rewardCodeInput,
rewardCodeSuccess,
@@ -584,6 +588,7 @@ export function ImageCanvasEditorView({
setRechargePaymentResult,
setRewardCodeInput,
submittingRechargeProductId,
submitMembershipCheckout,
submitRewardCode,
walletLedger,
walletLedgerError,
@@ -2746,9 +2751,14 @@ export function ImageCanvasEditorView({
error={rechargeError}
submittingProductId={submittingRechargeProductId}
nativePayment={nativeWechatPayment}
membershipQuote={membershipQuote}
isLoadingMembershipQuote={isLoadingMembershipQuote}
membershipQuoteError={membershipQuoteError}
onClose={() => setIsRechargeOpen(false)}
onRetry={loadRechargeCenter}
onBuy={buyRechargeProduct}
onRequestMembershipQuote={requestMembershipQuote}
onSubmitMembershipCheckout={submitMembershipCheckout}
onConfirmNativePayment={confirmNativeWechatPayment}
onCloseNativePayment={closeNativeWechatPayment}
/>
@@ -107,13 +107,15 @@ describe('ImageCanvasTopbarView', () => {
walletBalance: 12_345,
});
const walletChip = screen.getByRole('button', {
name: '泥点 12,345',
name: /可用总额 12,345/,
});
expect(walletChip.textContent).toBe('泥点 1.2万');
expect(walletChip.textContent).toContain('可用总额 1.2万');
await user.hover(walletChip);
const details = screen.getByRole('dialog', { name: '泥点账户详情' });
const details = screen.getByRole('dialog', {
name: '会员与泥点账户详情',
});
expect(within(details).getByText('永久泥点')).toBeTruthy();
expect(within(details).getByText('月度泥点')).toBeTruthy();
expect(within(details).getByText('每日免费泥点')).toBeTruthy();
@@ -121,7 +123,7 @@ describe('ImageCanvasTopbarView', () => {
await user.click(within(details).getByRole('button', { name: '使用详情' }));
expect(props.onOpenWalletLedger).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('button', { name: '充值' }));
await user.click(screen.getByRole('button', { name: '会员与泥点' }));
expect(props.onRecharge).toHaveBeenCalledTimes(1);
expect(props.onRequestWalletDetails).toHaveBeenCalledTimes(1);
});
@@ -268,7 +268,7 @@ describe('PlatformEntryActiveFlowShell', () => {
/>,
);
expect(await screen.findByLabelText('泥点 207')).toBeTruthy();
expect(await screen.findByLabelText(/可用总额 207/)).toBeTruthy();
expect(screen.getByRole('button', { name: '下载客户端' })).toBeTruthy();
rerender(
@@ -363,7 +363,7 @@ describe('PlatformEntryActiveFlowShell', () => {
setSelectionStage={vi.fn()}
/>,
);
await screen.findByLabelText('泥点 66');
await screen.findByLabelText(/可用总额 66/);
let resolveNextOwner!: (value: unknown) => void;
profileClientMock.getPlatformProfileRechargeCenter.mockImplementation(
@@ -380,7 +380,7 @@ describe('PlatformEntryActiveFlowShell', () => {
/>,
);
expect(screen.queryByLabelText('泥点 66')).toBeNull();
expect(screen.queryByLabelText(/可用总额 66/)).toBeNull();
await waitFor(() => {
expect(usePlatformWalletStore.getState()).toMatchObject({
ownerUserId: 'user-2',
@@ -397,7 +397,7 @@ describe('PlatformEntryActiveFlowShell', () => {
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
},
});
await screen.findByLabelText('泥点 67');
await screen.findByLabelText(/可用总额 67/);
});
it('coalesces visibility and focus refreshes when the page returns to the foreground', async () => {
@@ -429,7 +429,7 @@ describe('PlatformEntryActiveFlowShell', () => {
setSelectionStage={vi.fn()}
/>,
);
await screen.findByLabelText('泥点 10');
await screen.findByLabelText(/可用总额 10/);
await act(async () => {
document.dispatchEvent(new Event('visibilitychange'));
@@ -872,9 +872,14 @@ export function PlatformEntryFlowShellImpl({
error={profileCenter.rechargeError}
submittingProductId={profileCenter.submittingRechargeProductId}
nativePayment={profileCenter.nativeWechatPayment}
membershipQuote={profileCenter.membershipQuote}
isLoadingMembershipQuote={profileCenter.isLoadingMembershipQuote}
membershipQuoteError={profileCenter.membershipQuoteError}
onClose={() => profileCenter.setIsRechargeOpen(false)}
onRetry={profileCenter.loadRechargeCenter}
onBuy={profileCenter.buyRechargeProduct}
onRequestMembershipQuote={profileCenter.requestMembershipQuote}
onSubmitMembershipCheckout={profileCenter.submitMembershipCheckout}
onConfirmNativePayment={profileCenter.confirmNativeWechatPayment}
onCloseNativePayment={profileCenter.closeNativeWechatPayment}
/>
@@ -1,8 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
import { type PlatformProfileRechargeNativePaymentState } from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
import { resolveMembershipProductId } from '../../../packages/shared/src/components/PlatformProfileRechargeModal/membershipProduct';
import {
type ConfirmWechatProfileRechargeOrderResponse,
type ProfileMembershipCycleKind,
type ProfileMembershipPlan,
type ProfileMembershipUpgradeQuote,
type ProfileRechargeCenterResponse,
type ProfileRechargeOrder,
type ProfileRechargeProduct,
@@ -29,6 +33,7 @@ import { requestWechatJsapiPayment } from '../../services/payment/wechatJsapiPay
import {
confirmWechatPlatformProfileRechargeOrder,
createPlatformProfileRechargeOrder,
getPlatformProfileMembershipUpgradeQuote,
getPlatformProfileRechargeCenter,
getPlatformProfileReferralInviteCenter,
getPlatformProfileWalletLedger,
@@ -392,6 +397,13 @@ export function usePlatformProfileCenterController({
useState<NativeWechatPaymentState | null>(null);
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
useState<string | null>(null);
const [membershipQuote, setMembershipQuote] =
useState<ProfileMembershipUpgradeQuote | null>(null);
const [isLoadingMembershipQuote, setIsLoadingMembershipQuote] =
useState(false);
const [membershipQuoteError, setMembershipQuoteError] = useState<
string | null
>(null);
const [isWalletLedgerOpen, setIsWalletLedgerOpen] = useState(false);
const [walletLedger, setWalletLedger] =
useState<ProfileWalletLedgerResponse | null>(null);
@@ -1333,6 +1345,65 @@ export function usePlatformProfileCenterController({
],
);
const requestMembershipQuote = useCallback(
(plan: ProfileMembershipPlan) => {
const account = captureAccountLifecycle();
const requestSignal = accountWriteAbortControllerRef.current.signal;
if (
!account.ownerUserId ||
!isAccountLifecycleCurrent(account) ||
requestSignal.aborted
) {
return;
}
setMembershipQuote(null);
setMembershipQuoteError(null);
setIsLoadingMembershipQuote(true);
void getPlatformProfileMembershipUpgradeQuote(plan, {
signal: requestSignal,
})
.then((quote) => {
if (!isAccountLifecycleCurrent(account)) {
return;
}
setMembershipQuote(quote);
})
.catch((error: unknown) => {
if (!isAccountLifecycleCurrent(account)) {
return;
}
setMembershipQuoteError(
error instanceof Error ? error.message : '读取升级报价失败',
);
})
.finally(() => {
if (isAccountLifecycleCurrent(account)) {
setIsLoadingMembershipQuote(false);
}
});
},
[captureAccountLifecycle, isAccountLifecycleCurrent],
);
const submitMembershipCheckout = useCallback(
(plan: ProfileMembershipPlan, cycleKind: ProfileMembershipCycleKind) => {
// 会员商品没有商品配置行:标题与价格由后端按档位与周期现算,
// 这里只需要把 productId 交给同一条下单链路。
buyRechargeProduct({
productId: resolveMembershipProductId(plan, cycleKind),
title: plan,
priceCents: 0,
kind: 'membership',
pointsAmount: 0,
bonusPoints: 0,
durationDays: 0,
badgeLabel: '',
description: '',
});
},
[buyRechargeProduct],
);
const confirmNativeWechatPayment = useCallback(() => {
if (!nativeWechatPayment || nativeWechatPayment.isConfirming) {
return;
@@ -1872,6 +1943,11 @@ export function usePlatformProfileCenterController({
closeNativeWechatPayment,
closeProfilePopupPanel,
confirmNativeWechatPayment,
isLoadingMembershipQuote,
membershipQuote,
membershipQuoteError,
requestMembershipQuote,
submitMembershipCheckout,
copyInviteCode,
inviteCodeCopyState: inviteCodeCopyState as CopyFeedbackState,
inviteLinkCopyState: inviteLinkCopyState as CopyFeedbackState,
@@ -5,6 +5,8 @@ import type {
ExternalApiKeyListResponse,
ExternalApiKeyMutationResponse,
ProfileDashboardSummary,
ProfileMembershipPlan,
ProfileMembershipUpgradeQuote,
ProfileRechargeCenterResponse,
ProfileRechargeOrder,
ProfileReferralInviteCenterResponse,
@@ -168,6 +170,22 @@ export function createPlatformProfileRechargeOrder(
);
}
export function getPlatformProfileMembershipUpgradeQuote(
targetPlan: ProfileMembershipPlan,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileMembershipUpgradeQuote>(
'/membership/upgrade-quote',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetPlan }),
},
'读取升级报价失败',
options,
);
}
export function confirmWechatPlatformProfileRechargeOrder(
orderId: string,
options: PlatformProfileRequestOptions = {},