reuse recharge modal
This commit is contained in:
@@ -40,6 +40,10 @@ import {
|
||||
import {
|
||||
PlatformMudPointWalletEntry,
|
||||
} from '../../../packages/shared/src/components/PlatformMudPointWalletEntry';
|
||||
import {
|
||||
PlatformProfileRechargeModal,
|
||||
type PlatformProfileRechargeNativePaymentState,
|
||||
} from '../../../packages/shared/src/components/PlatformProfileRechargeModal';
|
||||
|
||||
import type {
|
||||
AuthEntryResponse,
|
||||
@@ -78,6 +82,8 @@ import {
|
||||
import type {
|
||||
ProfileDashboardSummary,
|
||||
ProfileMudPointBalance,
|
||||
ProfileRechargeCenterResponse,
|
||||
ProfileRechargeProduct,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
API_RESPONSE_ENVELOPE_HEADER,
|
||||
@@ -85,6 +91,8 @@ import {
|
||||
unwrapApiResponse,
|
||||
} from '../../../packages/shared/src/http';
|
||||
import {
|
||||
confirmClientWechatProfileRechargeOrder,
|
||||
createClientProfileRechargeOrder,
|
||||
getClientProfileDashboard,
|
||||
getClientProfileRechargeCenter,
|
||||
} from './services/clientApi';
|
||||
@@ -5361,6 +5369,15 @@ export function WorkspaceLauncher({
|
||||
'idle' | 'loading' | 'ready' | 'error'
|
||||
>('idle');
|
||||
const [mudPointBalanceError, setMudPointBalanceError] = useState('');
|
||||
const [rechargeOpen, setRechargeOpen] = useState(false);
|
||||
const [rechargeCenter, setRechargeCenter] =
|
||||
useState<ProfileRechargeCenterResponse | null>(null);
|
||||
const [rechargeLoading, setRechargeLoading] = useState(false);
|
||||
const [rechargeError, setRechargeError] = useState<string | null>(null);
|
||||
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
|
||||
useState<string | null>(null);
|
||||
const [nativeRechargePayment, setNativeRechargePayment] =
|
||||
useState<PlatformProfileRechargeNativePaymentState | null>(null);
|
||||
const [launcherNotice, setLauncherNotice] = useState<{
|
||||
title: string;
|
||||
message: string;
|
||||
@@ -6185,6 +6202,109 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
}
|
||||
|
||||
function applyRechargeCenter(center: ProfileRechargeCenterResponse) {
|
||||
setRechargeCenter(center);
|
||||
setMudPointBalance(center.mudPointBalance ?? null);
|
||||
setMudPointBalanceStatus('ready');
|
||||
setMudPointBalanceError('');
|
||||
setProfileDashboard((current) =>
|
||||
current ? { ...current, walletBalance: center.walletBalance } : current,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadRechargeCenter() {
|
||||
setRechargeLoading(true);
|
||||
setRechargeError(null);
|
||||
try {
|
||||
applyRechargeCenter(await getClientProfileRechargeCenter());
|
||||
} catch (error) {
|
||||
setRechargeError(
|
||||
error instanceof Error ? error.message : '读取泥点购买信息失败',
|
||||
);
|
||||
} finally {
|
||||
setRechargeLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openRecharge() {
|
||||
setRechargeOpen(true);
|
||||
setRechargeError(null);
|
||||
void loadRechargeCenter();
|
||||
}
|
||||
|
||||
async function buyRechargeProduct(product: ProfileRechargeProduct) {
|
||||
if (submittingRechargeProductId) {
|
||||
return;
|
||||
}
|
||||
setSubmittingRechargeProductId(product.productId);
|
||||
setRechargeError(null);
|
||||
try {
|
||||
const response = await createClientProfileRechargeOrder(
|
||||
product.productId,
|
||||
);
|
||||
applyRechargeCenter(response.center);
|
||||
const nativePayment = response.wechatNativePayment;
|
||||
const codeUrl = nativePayment?.codeUrl?.trim();
|
||||
const expiresAt = nativePayment?.expiresAt?.trim();
|
||||
if (!nativePayment || !codeUrl || !expiresAt) {
|
||||
throw new Error('微信 Native 支付链接生成失败');
|
||||
}
|
||||
setNativeRechargePayment({
|
||||
orderId: response.order.orderId,
|
||||
productTitle: response.order.productTitle,
|
||||
amountCents: response.order.amountCents,
|
||||
codeUrl,
|
||||
expiresAt,
|
||||
isConfirming: false,
|
||||
});
|
||||
} catch (error) {
|
||||
setRechargeError(error instanceof Error ? error.message : '充值失败');
|
||||
} finally {
|
||||
setSubmittingRechargeProductId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmNativeRechargePayment() {
|
||||
if (!nativeRechargePayment || nativeRechargePayment.isConfirming) {
|
||||
return;
|
||||
}
|
||||
const orderId = nativeRechargePayment.orderId;
|
||||
setNativeRechargePayment((current) =>
|
||||
current?.orderId === orderId
|
||||
? { ...current, isConfirming: true, confirmMessage: undefined }
|
||||
: current,
|
||||
);
|
||||
try {
|
||||
const response = await confirmClientWechatProfileRechargeOrder(orderId);
|
||||
applyRechargeCenter(response.center);
|
||||
if (response.order.status === 'paid') {
|
||||
setNativeRechargePayment(null);
|
||||
return;
|
||||
}
|
||||
const confirmMessage =
|
||||
response.order.status === 'pending' ||
|
||||
(response.order.status === 'expired' &&
|
||||
!response.order.expirationCheckedAt)
|
||||
? '暂未确认到账,请确认付款完成后再点一次。'
|
||||
: '订单未支付成功,请关闭二维码后重新下单。';
|
||||
setNativeRechargePayment((current) =>
|
||||
current?.orderId === orderId
|
||||
? { ...current, isConfirming: false, confirmMessage }
|
||||
: current,
|
||||
);
|
||||
} catch {
|
||||
setNativeRechargePayment((current) =>
|
||||
current?.orderId === orderId
|
||||
? {
|
||||
...current,
|
||||
isConfirming: false,
|
||||
confirmMessage: '暂时没能确认到账状态,请稍后再试。',
|
||||
}
|
||||
: current,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function showLauncherNotice(title: string) {
|
||||
setLauncherNotice({
|
||||
title,
|
||||
@@ -8484,6 +8604,7 @@ export function WorkspaceLauncher({
|
||||
onLogout();
|
||||
}}
|
||||
onNoticeRequest={showLauncherNotice}
|
||||
onRechargeRequest={openRecharge}
|
||||
onRuntimeConfigOpen={() => setRuntimeConfigOpen(true)}
|
||||
onViewChange={setLauncherView}
|
||||
/>
|
||||
@@ -8513,7 +8634,7 @@ export function WorkspaceLauncher({
|
||||
isLoading={mudPointBalanceStatus === 'loading'}
|
||||
error={mudPointBalanceError || null}
|
||||
onRequestDetails={() => void loadMudPointBalance()}
|
||||
onRecharge={() => showLauncherNotice('充值')}
|
||||
onRecharge={openRecharge}
|
||||
onOpenLedger={() => showLauncherNotice('使用详情')}
|
||||
/>
|
||||
</div>
|
||||
@@ -9271,6 +9392,23 @@ export function WorkspaceLauncher({
|
||||
onClose={() => setRuntimeConfigOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{rechargeOpen ? (
|
||||
<PlatformProfileRechargeModal
|
||||
center={rechargeCenter}
|
||||
isLoading={rechargeLoading}
|
||||
error={rechargeError}
|
||||
submittingProductId={submittingRechargeProductId}
|
||||
nativePayment={nativeRechargePayment}
|
||||
onClose={() => {
|
||||
setRechargeOpen(false);
|
||||
setNativeRechargePayment(null);
|
||||
}}
|
||||
onRetry={() => void loadRechargeCenter()}
|
||||
onBuy={(product) => void buyRechargeProduct(product)}
|
||||
onConfirmNativePayment={() => void confirmNativeRechargePayment()}
|
||||
onCloseNativePayment={() => setNativeRechargePayment(null)}
|
||||
/>
|
||||
) : null}
|
||||
{agentChatGoalDialog ? (
|
||||
<div
|
||||
className="launcher-dialog-backdrop"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
API_RESPONSE_ENVELOPE_HEADER,
|
||||
API_RESPONSE_ENVELOPE_VERSION,
|
||||
ConfirmWechatProfileRechargeOrderResponse,
|
||||
CreateProfileRechargeOrderResponse,
|
||||
ProfileDashboardSummary,
|
||||
ProfileRechargeCenterResponse,
|
||||
unwrapApiResponse,
|
||||
@@ -142,6 +144,29 @@ export function getClientProfileRechargeCenter() {
|
||||
);
|
||||
}
|
||||
|
||||
export function createClientProfileRechargeOrder(productId: string) {
|
||||
return requestClientApi<CreateProfileRechargeOrderResponse>(
|
||||
'/api/profile/recharge/orders',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
productId,
|
||||
paymentChannel: 'wechat_native',
|
||||
}),
|
||||
},
|
||||
'充值失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function confirmClientWechatProfileRechargeOrder(orderId: string) {
|
||||
return requestClientApi<ConfirmWechatProfileRechargeOrderResponse>(
|
||||
`/api/profile/recharge/orders/${encodeURIComponent(orderId)}/wechat/confirm`,
|
||||
{ method: 'POST' },
|
||||
'确认微信支付订单失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function listClientShowcaseResources() {
|
||||
return requestClientApi<EditorShowcaseResourceListResponse>(
|
||||
'/api/editor/showcase/resources',
|
||||
|
||||
@@ -42,6 +42,7 @@ type LauncherSidebarProps = {
|
||||
currentUser: SidebarUserInfo;
|
||||
onViewChange: (view: LauncherView) => void;
|
||||
onNoticeRequest: (title: string) => void;
|
||||
onRechargeRequest: () => void;
|
||||
onRuntimeConfigOpen: () => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
@@ -51,6 +52,7 @@ type SidebarAccountMenuProps = {
|
||||
onClose: () => void;
|
||||
onLogout: () => void;
|
||||
onNoticeRequest: (title: string) => void;
|
||||
onRechargeRequest: () => void;
|
||||
};
|
||||
|
||||
function formatMudPoints(value: number | null | undefined) {
|
||||
@@ -65,6 +67,7 @@ function SidebarAccountMenu({
|
||||
onClose,
|
||||
onLogout,
|
||||
onNoticeRequest,
|
||||
onRechargeRequest,
|
||||
}: SidebarAccountMenuProps) {
|
||||
const [walletBalanceLabel, setWalletBalanceLabel] = useState('--');
|
||||
const [walletBalanceStatus, setWalletBalanceStatus] =
|
||||
@@ -140,9 +143,9 @@ function SidebarAccountMenu({
|
||||
type="button"
|
||||
className="grid size-7 shrink-0 place-items-center rounded-full border border-white/70 bg-white/15 text-white"
|
||||
aria-label="充值泥点"
|
||||
onClick={()=>{
|
||||
onClick={() => {
|
||||
onClose();
|
||||
// TODO recharge component and logic is outdated on this branch
|
||||
onRechargeRequest();
|
||||
}}
|
||||
>
|
||||
<Plus size={14} aria-hidden="true" />
|
||||
@@ -200,6 +203,7 @@ export function Sidebar({
|
||||
currentUser,
|
||||
onViewChange,
|
||||
onNoticeRequest,
|
||||
onRechargeRequest,
|
||||
onRuntimeConfigOpen,
|
||||
onLogout,
|
||||
}: LauncherSidebarProps) {
|
||||
@@ -310,6 +314,7 @@ export function Sidebar({
|
||||
onClose={closeMenus}
|
||||
onLogout={onLogout}
|
||||
onNoticeRequest={onNoticeRequest}
|
||||
onRechargeRequest={onRechargeRequest}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1518,6 +1518,166 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
).toContain('/tmp/authorized-game');
|
||||
});
|
||||
|
||||
it('opens the shared recharge modal from the wallet and sidebar entries', async () => {
|
||||
const rechargeCenter = {
|
||||
walletBalance: 120,
|
||||
mudPointBalance: {
|
||||
totalPoints: 120,
|
||||
permanentPoints: 100,
|
||||
limitedPoints: 20,
|
||||
limitedExpiresAt: '2026-08-01T00:00:00Z',
|
||||
dailyFreePoints: 0,
|
||||
dailyFreeResetPoints: 20,
|
||||
dailyFreeResetsAt: '2026-07-18T00:00:00Z',
|
||||
},
|
||||
membership: {
|
||||
status: 'inactive',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
cycleStartedAt: null,
|
||||
cycleResetsAt: null,
|
||||
cycleGrantedPoints: 0,
|
||||
cycleRemainingPoints: 0,
|
||||
cyclePeriodDays: 0,
|
||||
},
|
||||
pointProducts: [
|
||||
{
|
||||
productId: 'points_60',
|
||||
title: '60泥点',
|
||||
priceCents: 600,
|
||||
kind: 'points',
|
||||
pointsAmount: 60,
|
||||
bonusPoints: 0,
|
||||
durationDays: 0,
|
||||
badgeLabel: '',
|
||||
description: '',
|
||||
tier: 'normal',
|
||||
membershipPeriodPoints: 0,
|
||||
membershipPeriodDays: 0,
|
||||
membershipQueueLimit: 0,
|
||||
membershipDiscountBps: 0,
|
||||
},
|
||||
],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: true,
|
||||
};
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/profile/dashboard') {
|
||||
return new Response(JSON.stringify({ walletBalance: 120 }), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
if (url === '/api/profile/recharge-center') {
|
||||
return new Response(JSON.stringify(rechargeCenter), { status: 200 });
|
||||
}
|
||||
if (url === '/api/profile/recharge/orders') {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
order: {
|
||||
orderId: 'order-native-1',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending',
|
||||
paymentChannel: 'wechat_native',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-07-17T00:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: rechargeCenter,
|
||||
wechatNativePayment: {
|
||||
codeUrl: 'weixin://pay.weixin.qq.com/native-test',
|
||||
expiresAt: '2099-01-01T00:05:00Z',
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (
|
||||
url === '/api/profile/recharge/orders/order-native-1/wechat/confirm'
|
||||
) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
order: {
|
||||
orderId: 'order-native-1',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'paid',
|
||||
paymentChannel: 'wechat_native',
|
||||
paidAt: '2026-07-17T00:01:00Z',
|
||||
providerTransactionId: 'wechat-transaction-1',
|
||||
createdAt: '2026-07-17T00:00:00Z',
|
||||
pointsDelta: 60,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
...rechargeCenter,
|
||||
walletBalance: 180,
|
||||
mudPointBalance: {
|
||||
...rechargeCenter.mudPointBalance,
|
||||
totalPoints: 180,
|
||||
permanentPoints: 160,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
});
|
||||
|
||||
renderLauncherAt('/?launcher');
|
||||
fireEvent.click(screen.getByRole('button', { name: '充值' }));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '购买更多泥点' }),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText('当前余额 120 泥点')).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭购买更多泥点' }));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '我的' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '充值泥点' }));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '购买更多泥点' }),
|
||||
).not.toBeNull();
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
'/api/profile/recharge-center',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /60泥点.*购买/ }));
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '微信扫码支付' }),
|
||||
).not.toBeNull();
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
'/api/profile/recharge/orders',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
productId: 'points_60',
|
||||
paymentChannel: 'wechat_native',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '我已支付' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '微信扫码支付' })).toBeNull();
|
||||
});
|
||||
expect(screen.getByText('当前余额 180 泥点')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('opens the help notice and account menu from the sidebar', () => {
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
|
||||
@@ -526,7 +526,7 @@ game-project/
|
||||
- 聊天输入 `/publish` 只使用主窗口当前已加载的 manifest、最近 run trace、预览状态、资产来源和最近命令摘要,在聊天里生成发布准备清单,列出原型通过状态、预览、任务、资产、音频、包装说明和试玩包导出状态,并提供 `/run`、`/trace`、`/agent-resume ` 或 `/export` 草稿;该命令不调用 Tauri 读写、不启动或打开预览、不读取文件、不新增普通用户面板,真正导出仍由用户发送 `/export` 并走确认流。
|
||||
- 开发窗口可从 Agent 状态列表进入单个专业 Agent 对话并管理其 Session;正式用户项目开发页只读展示专业 Agent 协作状态,不提供单 Agent 对话入口、Session 控件或工具台。
|
||||
- v1 普通用户登录后直接进入单窗口客户端首页;同一窗口中切换首页、项目组、指南 / 反馈和项目开发页。项目组页管理最近项目、打开项目、新建项目和显示目录;打开项目只切换到项目开发页,不调用 `open_game_creator_workspace_window` 打开第二窗口。旧 Tauri 窗口 command 只保留兼容,不进入用户主流程。
|
||||
- 2026-07-16 补充:普通用户窗口顶部账户资产统一复用 `PlatformMudPointWalletEntry`;首屏余额来自 `/api/profile/dashboard`,展开时按需读取 `/api/profile/recharge-center` 中的泥点拆分。AI 游戏创作壳的 Tailwind 入口必须显式扫描 `packages/shared/src/components`,避免共享组件的 utility 样式在构建时被遗漏。
|
||||
- 2026-07-16 补充,2026-07-17 扩展:普通用户窗口顶部账户资产统一复用 `PlatformMudPointWalletEntry`;首屏余额来自 `/api/profile/dashboard`,展开时按需读取 `/api/profile/recharge-center` 中的泥点拆分。顶部资产“充值”与侧栏账户菜单“充值泥点”共用 `PlatformProfileRechargeModal`,桌面壳固定使用 `wechat_native` 下单并在弹窗内完成扫码与到账确认,成功后同步刷新顶部余额和泥点拆分。AI 游戏创作壳的 Tailwind 入口必须显式扫描 `packages/shared/src/components`,避免共享组件的 utility 样式在构建时被遗漏。
|
||||
- 主窗口可通过系统文件管理器显示当前项目目录,也可在聊天输入 `/open-project` 走同一只读打开动作;该操作只打开本地目录,不初始化项目、不写项目文件、不切换工作区。主窗口头部显示最近 `.agent/run.latest.json` 的 run 状态摘要和当前项目预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。
|
||||
- 首页、项目组页和项目开发页共用单窗口壳的全局运行时配置弹窗,读写 Tauri 应用配置目录中的 `game-creator.config.json`;正式 Supervisor 项目页缺配置时只显示错误,不自动打开该弹窗。API Key 仍不进入本地项目、trace、manifest 或聊天记录。
|
||||
- 首页发送和项目组新建都通过 Tauri 原生目录选择器选择项目路径;用户取消目录选择时不覆盖已有输入或草稿。
|
||||
|
||||
Reference in New Issue
Block a user