4b529a8952
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m59s
Project CI / Backend tests (pull_request) Failing after 4m8s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m9s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m20s
Project CI / Frontend tests (pull_request) Successful in 3m17s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m48s
Project CI / Repository checks (pull_request) Failing after 4m15s
Project CI / Native shell tests (pull_request) Successful in 6m52s
新增支付应用、API Key 与外部产品订单接口 接入微信 Native 下单、二维码收银台与支付结果回调 新增后台支付订单与回调投递页面 补齐 SpacetimeDB 支付表、生成绑定、OpenAPI 与使用说明
348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import {
|
|
ArrowLeft,
|
|
Check,
|
|
Copy,
|
|
CreditCard,
|
|
ExternalLink,
|
|
KeyRound,
|
|
} from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import type { AuthUser } from '@/packages/shared/src';
|
|
|
|
import type {
|
|
PaymentApiKey,
|
|
PaymentApp,
|
|
PaymentAppCreateResponse,
|
|
} from '../../../packages/shared/src/contracts/payment';
|
|
import {
|
|
createPlatformProfilePaymentApp,
|
|
listPlatformProfilePaymentApps,
|
|
revokePlatformProfilePaymentApiKey,
|
|
updatePlatformProfilePaymentApp,
|
|
} from '../../services/platform-entry/platformProfileClient';
|
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
|
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
|
|
|
type PlatformPaymentServiceViewProps = {
|
|
user: AuthUser | null | undefined;
|
|
onBack: () => void;
|
|
onLogin: () => void;
|
|
};
|
|
|
|
export function PlatformPaymentServiceView({
|
|
user,
|
|
onBack,
|
|
onLogin,
|
|
}: PlatformPaymentServiceViewProps) {
|
|
const [apps, setApps] = useState<PaymentApp[]>([]);
|
|
const [keys, setKeys] = useState<PaymentApiKey[]>([]);
|
|
const [name, setName] = useState('我的游戏支付');
|
|
const [callbackUrl, setCallbackUrl] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [created, setCreated] = useState<PaymentAppCreateResponse | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!user) {
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
setIsLoading(true);
|
|
listPlatformProfilePaymentApps()
|
|
.then((response) => {
|
|
if (!cancelled) {
|
|
setApps(response.apps ?? []);
|
|
setKeys(response.keys ?? []);
|
|
}
|
|
})
|
|
.catch((nextError: unknown) => {
|
|
if (!cancelled) {
|
|
setError(
|
|
nextError instanceof Error ? nextError.message : '读取支付服务失败',
|
|
);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) {
|
|
setIsLoading(false);
|
|
}
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [user]);
|
|
|
|
const submit = async () => {
|
|
const normalizedName = name.trim();
|
|
if (!normalizedName) {
|
|
setError('请填写应用名称');
|
|
return;
|
|
}
|
|
setIsCreating(true);
|
|
setError('');
|
|
try {
|
|
const response = await createPlatformProfilePaymentApp(
|
|
normalizedName,
|
|
callbackUrl.trim() || null,
|
|
);
|
|
setCreated(response);
|
|
setApps((current) => [response.app, ...current]);
|
|
} catch (nextError: unknown) {
|
|
setError(
|
|
nextError instanceof Error ? nextError.message : '创建支付应用失败',
|
|
);
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
const toggleApp = async (app: PaymentApp) => {
|
|
try {
|
|
const updated = await updatePlatformProfilePaymentApp(
|
|
app.appId,
|
|
!app.enabled,
|
|
);
|
|
setApps((current) =>
|
|
current.map((item) => (item.appId === updated.appId ? updated : item)),
|
|
);
|
|
} catch (nextError: unknown) {
|
|
setError(
|
|
nextError instanceof Error ? nextError.message : '更新支付应用失败',
|
|
);
|
|
}
|
|
};
|
|
|
|
const revokeKey = async (key: PaymentApiKey) => {
|
|
try {
|
|
const updated = await revokePlatformProfilePaymentApiKey(key.keyId);
|
|
setKeys((current) =>
|
|
current.map((item) => (item.keyId === updated.keyId ? updated : item)),
|
|
);
|
|
} catch (nextError: unknown) {
|
|
setError(
|
|
nextError instanceof Error
|
|
? nextError.message
|
|
: '撤销支付 API Key 失败',
|
|
);
|
|
}
|
|
};
|
|
|
|
const copyKey = async () => {
|
|
if (!created?.apiKey) {
|
|
return;
|
|
}
|
|
await navigator.clipboard?.writeText(created.apiKey);
|
|
};
|
|
|
|
if (!user) {
|
|
return (
|
|
<main className="platform-profile-page platform-remap-surface w-full p-4 sm:p-8">
|
|
<PlatformSubpanel
|
|
className="mx-auto w-full max-w-xl p-6 text-center"
|
|
radius="xl"
|
|
>
|
|
<CreditCard className="mx-auto h-10 w-10 text-[var(--platform-accent-strong)]" />
|
|
<h1 className="mt-4 text-xl font-black">支付服务</h1>
|
|
<p className="mt-2 text-sm text-[var(--platform-text-base)]">
|
|
登录后可以创建游戏或外部产品的支付应用。
|
|
</p>
|
|
<PlatformActionButton className="mt-5" onClick={onLogin}>
|
|
登录
|
|
</PlatformActionButton>
|
|
</PlatformSubpanel>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<main className="platform-profile-page platform-remap-surface w-full space-y-4 p-4 pb-8 sm:p-8">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
className="platform-profile-icon-button rounded-full p-2"
|
|
onClick={onBack}
|
|
aria-label="返回我的"
|
|
>
|
|
<ArrowLeft className="h-5 w-5" aria-hidden="true" />
|
|
</button>
|
|
<div>
|
|
<h1 className="text-xl font-black text-[var(--platform-text-strong)]">
|
|
支付服务
|
|
</h1>
|
|
<p className="mt-1 text-sm text-[var(--platform-text-base)]">
|
|
统一收款、动态二维码和支付结果通知。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{error ? (
|
|
<PlatformStatusMessage tone="error" surface="profile">
|
|
{error}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
|
|
<PlatformSubpanel as="section" className="p-5 sm:p-6" radius="xl">
|
|
<div className="flex items-start gap-3">
|
|
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-[rgba(199,101,50,0.12)] text-[var(--platform-accent-strong)]">
|
|
<CreditCard className="h-5 w-5" aria-hidden="true" />
|
|
</span>
|
|
<div>
|
|
<h2 className="font-black">创建接入应用</h2>
|
|
<p className="mt-1 text-sm leading-6 text-[var(--platform-text-base)]">
|
|
为一款游戏或外部产品创建独立的应用身份。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 grid gap-4 sm:grid-cols-2">
|
|
<label className="text-sm font-semibold">
|
|
应用名称
|
|
<input
|
|
className="platform-profile-input mt-2 w-full rounded-xl px-3 py-2.5"
|
|
value={name}
|
|
onChange={(event) => setName(event.target.value)}
|
|
maxLength={80}
|
|
/>
|
|
</label>
|
|
<label className="text-sm font-semibold">
|
|
支付成功回调地址(可选)
|
|
<input
|
|
className="platform-profile-input mt-2 w-full rounded-xl px-3 py-2.5"
|
|
value={callbackUrl}
|
|
onChange={(event) => setCallbackUrl(event.target.value)}
|
|
placeholder="https://example.com/payment/callback"
|
|
/>
|
|
</label>
|
|
</div>
|
|
<PlatformActionButton
|
|
className="mt-5"
|
|
onClick={() => void submit()}
|
|
disabled={isCreating}
|
|
>
|
|
<CreditCard className="h-4 w-4" aria-hidden="true" />
|
|
{isCreating ? '创建中...' : '创建支付应用'}
|
|
</PlatformActionButton>
|
|
</PlatformSubpanel>
|
|
|
|
{created ? (
|
|
<PlatformSubpanel
|
|
as="section"
|
|
className="border-[var(--platform-accent-strong)] p-5 sm:p-6"
|
|
radius="xl"
|
|
>
|
|
<div className="flex items-center gap-2 font-black text-[var(--platform-accent-strong)]">
|
|
<Check className="h-5 w-5" />
|
|
应用已创建
|
|
</div>
|
|
<p className="mt-2 text-sm text-[var(--platform-text-base)]">
|
|
完整 API Key 只显示这一次,请立即复制并保存到你的服务端。
|
|
</p>
|
|
<div className="mt-4 flex items-center gap-2 rounded-xl bg-[var(--platform-subpanel-fill)] p-3">
|
|
<code className="min-w-0 flex-1 break-all text-xs">
|
|
{created.apiKey}
|
|
</code>
|
|
<button
|
|
type="button"
|
|
className="platform-profile-icon-button rounded-lg p-2"
|
|
onClick={() => void copyKey()}
|
|
aria-label="复制 API Key"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
<div className="mt-3 flex items-center gap-2 text-xs text-[var(--platform-text-soft)]">
|
|
<KeyRound className="h-4 w-4" />
|
|
{created.key.scopes.join('、')}
|
|
</div>
|
|
</PlatformSubpanel>
|
|
) : null}
|
|
|
|
<PlatformSubpanel as="section" className="p-5 sm:p-6" radius="xl">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<h2 className="font-black">我的支付应用</h2>
|
|
<span className="text-xs text-[var(--platform-text-soft)]">
|
|
{isLoading ? '读取中...' : `${apps.length} 个应用`}
|
|
</span>
|
|
</div>
|
|
<div className="mt-4 space-y-3">
|
|
{apps.length === 0 && !isLoading ? (
|
|
<p className="text-sm text-[var(--platform-text-soft)]">
|
|
还没有支付应用。
|
|
</p>
|
|
) : null}
|
|
{apps.map((app) => (
|
|
<div
|
|
key={app.appId}
|
|
className="flex flex-col gap-3 rounded-2xl border border-[var(--platform-subpanel-border)] p-4 sm:flex-row sm:items-center sm:justify-between"
|
|
>
|
|
<div>
|
|
<strong className="block text-sm">{app.name}</strong>
|
|
<code className="mt-1 block text-xs text-[var(--platform-text-soft)]">
|
|
{app.appId}
|
|
</code>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs text-[var(--platform-text-base)]">
|
|
<span
|
|
className={`h-2 w-2 rounded-full ${app.enabled ? 'bg-emerald-500' : 'bg-slate-400'}`}
|
|
/>
|
|
{app.enabled ? '可用' : '已停用'}
|
|
<button
|
|
type="button"
|
|
className="ml-2 font-bold text-[var(--platform-accent-strong)]"
|
|
onClick={() => void toggleApp(app)}
|
|
>
|
|
{app.enabled ? '停用' : '启用'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</PlatformSubpanel>
|
|
|
|
<PlatformSubpanel as="section" className="p-5 sm:p-6" radius="xl">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<h2 className="font-black">支付 API Key</h2>
|
|
<span className="text-xs text-[var(--platform-text-soft)]">
|
|
{keys.length} 个
|
|
</span>
|
|
</div>
|
|
<div className="mt-4 space-y-3">
|
|
{keys.length === 0 ? (
|
|
<p className="text-sm text-[var(--platform-text-soft)]">
|
|
创建应用后会生成默认 Key。
|
|
</p>
|
|
) : null}
|
|
{keys.map((key) => (
|
|
<div
|
|
key={key.keyId}
|
|
className="flex flex-col gap-2 rounded-2xl border border-[var(--platform-subpanel-border)] p-4 sm:flex-row sm:items-center sm:justify-between"
|
|
>
|
|
<div>
|
|
<strong className="block text-sm">{key.name}</strong>
|
|
<code className="mt-1 block text-xs text-[var(--platform-text-soft)]">
|
|
{key.keyPrefix}
|
|
</code>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="self-start text-xs font-bold text-rose-600 sm:self-auto"
|
|
disabled={Boolean(key.revokedAt)}
|
|
onClick={() => void revokeKey(key)}
|
|
>
|
|
{key.revokedAt ? '已撤销' : '撤销 Key'}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</PlatformSubpanel>
|
|
|
|
<p className="flex items-center gap-2 text-xs leading-5 text-[var(--platform-text-soft)]">
|
|
<ExternalLink className="h-4 w-4 shrink-0" />
|
|
支付金额由服务端订单快照决定,到账以微信支付通知或查单为准。
|
|
</p>
|
|
</main>
|
|
);
|
|
}
|