接入微信 Native 充值与订单过期处理

调整网页端充值渠道,微信内浏览器在开放平台接入前走 Native 支付。

拆分 Native 支付二维码弹窗,展示金额和充值内容并取消无效 code_url 跳转。

修复微信 Native 下单 time_expire 秒级 RFC3339 格式,避免小数秒参数错误。

新增充值订单 5 分钟过期调度,过期前查询微信真实订单状态。

补充微信授权绑定、路由加载恢复和后台表时间显示等配套逻辑。

更新 SpacetimeDB schema、绑定和相关文档测试。
This commit is contained in:
kdletters
2026-07-08 21:32:51 +08:00
parent 2721fec51c
commit 542fb09365
385 changed files with 6811 additions and 3169 deletions
+51
View File
@@ -15,6 +15,7 @@ import { RouteLoadingScreen } from './RouteLoadingScreen';
afterEach(() => {
vi.useRealTimers();
window.localStorage.clear();
window.sessionStorage.clear();
});
describe('RouteImageReadyGate image url helpers', () => {
@@ -87,6 +88,56 @@ describe('RouteImageReadyGate image url helpers', () => {
expect(visibilityGate?.style.visibility).toBe('visible');
});
it('does not show the full screen route loader again after the same route was revealed', () => {
vi.useFakeTimers();
const firstRender = render(
createElement(
RouteImageReadyGate,
{
eyebrow: 'Loading',
text: 'Please wait',
},
createElement(
'section',
{
'data-testid': 'route-content',
},
createElement('img', {
src: '/generated-characters/slow-cover.png',
alt: 'slow cover',
}),
),
),
);
expect(screen.getByText('Please wait')).toBeTruthy();
act(() => {
vi.advanceTimersByTime(1600);
});
expect(screen.queryByText('Please wait')).toBeNull();
firstRender.unmount();
render(
createElement(
RouteImageReadyGate,
{
eyebrow: 'Loading',
text: 'Please wait',
},
createElement('section', { 'data-testid': 'route-content' }, 'Ready'),
),
);
const content = screen.getByTestId('route-content');
const visibilityGate = content.parentElement;
expect(visibilityGate?.getAttribute('aria-hidden')).toBe('false');
expect(visibilityGate?.style.visibility).toBe('visible');
expect(screen.queryByText('Please wait')).toBeNull();
});
it('uses the saved platform theme tokens for the route loading screen', () => {
window.localStorage.setItem(
'tavernrealms.settings.v1',
+37 -1
View File
@@ -17,10 +17,40 @@ const IMAGE_GATE_QUIET_MS = 140;
const IMAGE_GATE_MIN_VISIBLE_WAIT_MS = 260;
const IMAGE_GATE_MAX_BLOCK_MS = 1400;
const IMAGE_PRELOAD_TIMEOUT_MS = 12000;
const ROUTE_IMAGE_READY_SESSION_PREFIX = 'genarrative:route-image-ready:';
const settledImageUrls = new Set<string>();
const imagePreloadTasks = new Map<string, Promise<ImagePreloadResult>>();
function getRouteImageReadySessionKey() {
if (typeof window === 'undefined') {
return ROUTE_IMAGE_READY_SESSION_PREFIX;
}
return `${ROUTE_IMAGE_READY_SESSION_PREFIX}${window.location.pathname}`;
}
function hasSeenRouteImageReady() {
if (typeof window === 'undefined') {
return false;
}
try {
return window.sessionStorage.getItem(getRouteImageReadySessionKey()) === '1';
} catch {
return false;
}
}
function markRouteImageReadySeen() {
if (typeof window === 'undefined') {
return;
}
try {
window.sessionStorage.setItem(getRouteImageReadySessionKey(), '1');
} catch {
// sessionStorage may be blocked in embedded browsers; the visual gate still works without it.
}
}
function preloadImageUrl(url: string) {
const existingTask = imagePreloadTasks.get(url);
if (existingTask) {
@@ -80,13 +110,18 @@ export function RouteImageReadyGate({
const revealTimerRef = useRef<number | null>(null);
const scanVersionRef = useRef(0);
const revealedRef = useRef(false);
const [ready, setReady] = useState(false);
const [ready, setReady] = useState(() => hasSeenRouteImageReady());
useEffect(() => {
const root = rootRef.current;
if (!root) {
return;
}
if (hasSeenRouteImageReady()) {
revealedRef.current = true;
setReady(true);
return;
}
let disposed = false;
startTimeRef.current = window.performance.now();
@@ -133,6 +168,7 @@ export function RouteImageReadyGate({
}
revealedRef.current = true;
markRouteImageReadySeen();
setReady(true);
}, delay);
};