diff --git a/.gitignore b/.gitignore index 412a4e087..56bf26e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ dists/ dist-web/ dist-dweb/ +public/miniapps/ tmp/ .tmp/ .chat/ diff --git a/deno.lock b/deno.lock index 01b73c89e..9b712a535 100644 --- a/deno.lock +++ b/deno.lock @@ -10153,6 +10153,7 @@ "npm:@noble/secp256k1@3", "npm:@number-flow/react@~0.5.10", "npm:@plaoc/cli@^1.2.2", + "npm:@plaoc/is-dweb@~0.1.2", "npm:@plaoc/plugins@^1.1.9", "npm:@playwright/test@^1.57.0", "npm:@radix-ui/react-avatar@^1.1.11", diff --git a/miniapps/forge/src/App.tsx b/miniapps/forge/src/App.tsx index 5001a6f10..8c30046db 100644 --- a/miniapps/forge/src/App.tsx +++ b/miniapps/forge/src/App.tsx @@ -219,8 +219,35 @@ export default function App() { setRechargeStep('confirm'); }; + const externalDecimals = + typeof selectedOption?.externalInfo.decimals === 'number' && Number.isFinite(selectedOption.externalInfo.decimals) + ? selectedOption.externalInfo.decimals + : undefined; + const externalTokenAddress = selectedOption?.externalInfo.contract?.trim(); + const externalDecimalsFallback = externalTokenAddress + ? (() => { + switch (selectedOption?.externalChain) { + case 'ETH': + case 'BSC': + return 18; + case 'TRON': + return 6; + default: + return undefined; + } + })() + : undefined; + const resolvedExternalDecimals = externalDecimals ?? externalDecimalsFallback; + const usingFallbackDecimals = + Boolean(externalTokenAddress) && externalDecimals === undefined && resolvedExternalDecimals !== undefined; + const handleConfirm = useCallback(async () => { if (!externalAccount || !internalAccount || !selectedOption) return; + const tokenAddress = selectedOption.externalInfo.contract?.trim(); + if (tokenAddress && resolvedExternalDecimals === undefined) { + setError(t('error.missingDecimals')); + return; + } setError(null); setRechargeStep('processing'); @@ -228,16 +255,16 @@ export default function App() { await forgeHook.forge({ externalChain: selectedOption.externalChain, externalAsset: selectedOption.externalAsset, - externalDecimals: selectedOption.externalInfo.decimals, + externalDecimals: resolvedExternalDecimals, depositAddress: selectedOption.externalInfo.depositAddress, - externalContract: selectedOption.externalInfo.contract, + externalContract: tokenAddress, amount, externalAccount, internalChain: selectedOption.internalChain, internalAsset: selectedOption.internalAsset, internalAccount, }); - }, [externalAccount, internalAccount, selectedOption, amount, forgeHook]); + }, [externalAccount, internalAccount, selectedOption, amount, forgeHook, resolvedExternalDecimals, t]); const handleReset = useCallback(() => { setRechargeStep('swap'); @@ -257,7 +284,6 @@ export default function App() { return groups; }, [forgeOptions]); - const externalDecimals = selectedOption?.externalInfo.decimals; const contractAddress = selectedOption?.externalInfo.contract?.trim(); const contractDisplay = contractAddress ? `${contractAddress.slice(0, 6)}...${contractAddress.slice(-4)}` @@ -455,8 +481,11 @@ export default function App() {
{t('forge.amountHint', { symbol: selectedOption.externalAsset })}
- {typeof externalDecimals === 'number' && ( -
{t('forge.decimalsHint', { decimals: externalDecimals })}
+ {typeof resolvedExternalDecimals === 'number' && ( +
+ {t('forge.decimalsHint', { decimals: resolvedExternalDecimals })} + {usingFallbackDecimals ? ` (${t('forge.decimalsFallback')})` : null} +
)} {contractDisplay && (
diff --git a/miniapps/forge/src/api/schemas.ts b/miniapps/forge/src/api/schemas.ts index d7c580aba..66f9a48a3 100644 --- a/miniapps/forge/src/api/schemas.ts +++ b/miniapps/forge/src/api/schemas.ts @@ -12,7 +12,7 @@ const externalAssetInfoItemSchema = z.object({ depositAddress: z.string(), assetType: z.string(), logo: z.string().optional(), - decimals: z.number().optional(), + decimals: z.coerce.number().optional(), }).passthrough() const redemptionConfigSchema = z.object({ diff --git a/miniapps/forge/src/i18n/locales/en.json b/miniapps/forge/src/i18n/locales/en.json index 6e55b75bc..e4ab0299d 100644 --- a/miniapps/forge/src/i18n/locales/en.json +++ b/miniapps/forge/src/i18n/locales/en.json @@ -25,6 +25,7 @@ "continue": "Continue", "amountHint": "Amount is in {{symbol}}", "decimalsHint": "Token decimals: {{decimals}}", + "decimalsFallback": "default", "contractHint": "Contract: {{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK not initialized", "connectionFailed": "Connection failed", "invalidAmount": "Please enter a valid amount", + "missingDecimals": "Missing token decimals configuration", "forgeFailed": "Operation failed" }, "picker": { diff --git a/miniapps/forge/src/i18n/locales/zh-CN.json b/miniapps/forge/src/i18n/locales/zh-CN.json index aaa2789e3..3661545c8 100644 --- a/miniapps/forge/src/i18n/locales/zh-CN.json +++ b/miniapps/forge/src/i18n/locales/zh-CN.json @@ -25,6 +25,7 @@ "continue": "继续", "amountHint": "数量单位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsFallback": "默认", "contractHint": "合约:{{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "连接失败", "invalidAmount": "请输入有效金额", + "missingDecimals": "缺少资产精度配置", "forgeFailed": "操作失败" }, "picker": { diff --git a/miniapps/forge/src/i18n/locales/zh-TW.json b/miniapps/forge/src/i18n/locales/zh-TW.json index 744f5d7bc..b90475f4b 100644 --- a/miniapps/forge/src/i18n/locales/zh-TW.json +++ b/miniapps/forge/src/i18n/locales/zh-TW.json @@ -25,6 +25,7 @@ "continue": "繼續", "amountHint": "數量單位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsFallback": "預設", "contractHint": "合約:{{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "連接失敗", "invalidAmount": "請輸入有效金額", + "missingDecimals": "缺少資產精度配置", "forgeFailed": "操作失敗" }, "picker": { diff --git a/miniapps/forge/src/i18n/locales/zh.json b/miniapps/forge/src/i18n/locales/zh.json index aaa2789e3..3661545c8 100644 --- a/miniapps/forge/src/i18n/locales/zh.json +++ b/miniapps/forge/src/i18n/locales/zh.json @@ -25,6 +25,7 @@ "continue": "继续", "amountHint": "数量单位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsFallback": "默认", "contractHint": "合约:{{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "连接失败", "invalidAmount": "请输入有效金额", + "missingDecimals": "缺少资产精度配置", "forgeFailed": "操作失败" }, "picker": { diff --git a/miniapps/teleport/src/components/AuroraBackground.tsx b/miniapps/teleport/src/components/AuroraBackground.tsx index f12be2902..b7caa3715 100644 --- a/miniapps/teleport/src/components/AuroraBackground.tsx +++ b/miniapps/teleport/src/components/AuroraBackground.tsx @@ -14,7 +14,7 @@ export const AuroraBackground = ({ ...props }: AuroraBackgroundProps) => { return ( -
+
{children}
-
+
); }; diff --git a/package.json b/package.json index 8d9ac0b3c..e56b702f1 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@noble/hashes": "^2.0.1", "@noble/secp256k1": "^3.0.0", "@number-flow/react": "^0.5.10", + "@plaoc/is-dweb": "^0.1.2", "@plaoc/plugins": "^1.1.9", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", @@ -143,8 +144,8 @@ "@axe-core/playwright": "^4.11.0", "@biochain/e2e-tools": "workspace:*", "@inquirer/prompts": "^8.0.2", - "@playwright/test": "^1.57.0", "@plaoc/cli": "^1.2.2", + "@playwright/test": "^1.57.0", "@storybook/addon-docs": "^10.1.4", "@storybook/addon-vitest": "^10.1.4", "@storybook/react": "^10.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d786e330b..34675ab56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: '@number-flow/react': specifier: ^0.5.10 version: 0.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@plaoc/is-dweb': + specifier: ^0.1.2 + version: 0.1.2 '@plaoc/plugins': specifier: ^1.1.9 version: 1.1.9(typescript@5.9.3) @@ -1626,6 +1629,9 @@ packages: '@dweb-browser/js-process@0.4.7': resolution: {integrity: sha512-XUBSPxcgpKjjEibIo1f7f5RWH4VqYW9M4RunUSVIsI64ENk4FjG7AjeD27FBikwbuku+rmTWzgH3wQyNDDnEyg==} + '@dweb-browser/polyfill@0.0.8': + resolution: {integrity: sha512-OhjyCJiaOKFTmUoRBugn+bcmjZqKSek6WesB/2vASnPMTODpyqKLQ6uUguGuYb9mAC64lwi0P7eoCJmRyzQ6Jw==} + '@dweb-browser/polyfill@0.0.9': resolution: {integrity: sha512-1siZWAvzXMVZ5z6TmFPCSYXHTNTu5qmZv5UR0QSlujXuQr93I+BX6bhyu2FG4pCqqkoE3OAVlQ15D9Nj+beS4g==} @@ -2877,6 +2883,9 @@ packages: resolution: {integrity: sha512-4txWq8sytf6HCNt+ryWJzjhXVtNMrckdO/sBb2Xd3MbHPBMo1rvRIvVy0FCkPHe8cODF60SF9W/tovAFyYSuVw==} hasBin: true + '@plaoc/is-dweb@0.1.2': + resolution: {integrity: sha512-H2QdJVjOVhnE75mTq5vQQYVLZzqDFHoPUVtMnJRGpFCgjkTA36mswo3St9kxnAZlAMJziDckMbu6+/1daXn87Q==} + '@plaoc/plugins@1.1.9': resolution: {integrity: sha512-TnFNddIYsX0myjhhNVRR+HZS415CPnw0Xe4ipyP0ICUm94dleGZodJTDAPkgsKZi085gI7dlSXp8hYsNI3TIhw==} @@ -9537,6 +9546,8 @@ snapshots: transitivePeerDependencies: - typescript + '@dweb-browser/polyfill@0.0.8': {} + '@dweb-browser/polyfill@0.0.9': {} '@dweb-browser/svg-wasm@0.9.0': {} @@ -10788,6 +10799,10 @@ snapshots: - typescript - utf-8-validate + '@plaoc/is-dweb@0.1.2': + dependencies: + '@dweb-browser/polyfill': 0.0.8 + '@plaoc/plugins@1.1.9(typescript@5.9.3)': dependencies: '@dweb-browser/core': 0.1.4(typescript@5.9.3) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 004ac75c0..28d81e83c 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -7,10 +7,13 @@ */ import { createServer, build as viteBuild, type Plugin, type ViteDevServer } from 'vite'; -import { resolve, join } from 'node:path'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { resolve, join, basename } from 'node:path'; import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'node:fs'; import detectPort from 'detect-port'; import https from 'node:https'; +import type { RollupWatcher } from 'rollup'; +import sirv from 'sirv'; import { getRemoteMiniappsForEcosystem, type RemoteMiniappConfig } from './vite-plugin-remote-miniapps'; import type { WujieRuntimeConfig } from '../src/services/ecosystem/types'; @@ -23,6 +26,8 @@ interface MiniappRuntimeConfig { build?: MiniappRuntime | { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig }; } +type MiniappDevMode = 'proxy' | 'build'; + function parseRuntimeConfig(config?: MiniappRuntime | { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig }): { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig; @@ -69,23 +74,27 @@ interface MiniappServer { dirName: string; port: number; server: ViteDevServer; - baseUrl: string; } interface MiniappsPluginOptions { miniappsDir?: string; apps?: Record; remoteMiniapps?: RemoteMiniappConfig[]; + devMode?: MiniappDevMode; } // ==================== Plugin ==================== export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { - const { miniappsDir = 'miniapps', apps = {}, remoteMiniapps = [] } = options; + const { miniappsDir = 'miniapps', apps = {}, remoteMiniapps = [], devMode } = options; + const resolvedDevMode: MiniappDevMode = + devMode ?? (process.env.MINIAPPS_DEV_MODE === 'build' ? 'build' : 'proxy'); let root: string; let isBuild = false; const miniappServers: MiniappServer[] = []; + const miniappBuildWatchers: RollupWatcher[] = []; + let miniappsDevDistDir: string | null = null; return { name: 'vite-plugin-miniapps', @@ -112,6 +121,85 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const miniappsPath = resolve(root, miniappsDir); const manifests = scanMiniapps(miniappsPath); + if (resolvedDevMode === 'build') { + miniappsDevDistDir = resolve(root, 'public', 'miniapps'); + mkdirSync(miniappsDevDistDir, { recursive: true }); + + await Promise.all( + manifests.map(async (manifest) => { + const miniappPath = join(miniappsPath, manifest.dirName); + const outputDir = resolve(miniappsDevDistDir!, manifest.dirName); + const result = await viteBuild({ + root: miniappPath, + configFile: join(miniappPath, 'vite.config.ts'), + base: './', + build: { + outDir: outputDir, + emptyOutDir: true, + watch: {}, + minify: false, + sourcemap: true, + }, + logLevel: 'warn', + }); + + if (result && typeof (result as RollupWatcher).on === 'function') { + miniappBuildWatchers.push(result as RollupWatcher); + } + + console.log(`[miniapps] ${manifest.name} built (watch mode)`); + }), + ); + + server.middlewares.use( + '/miniapps', + sirv(miniappsDevDistDir, { + dev: true, + single: false, + }), + ); + + const localApps = manifests.map((manifest) => { + const appConfig = apps[manifest.id]; + const { runtime, wujieConfig } = parseRuntimeConfig(appConfig?.server); + return { + ...manifest, + dirName: manifest.dirName, + icon: resolveMiniappDevAsset(manifest.icon, manifest.dirName), + url: `/miniapps/${manifest.dirName}/`, + screenshots: manifest.screenshots.map((sc) => resolveMiniappDevAsset(sc, manifest.dirName)), + runtime, + wujieConfig, + }; + }); + + const remoteApps = getRemoteMiniappsForEcosystem(); + const ecosystemData: EcosystemJson = { + name: 'Bio 官方生态', + version: '1.0.0', + updated: new Date().toISOString().split('T')[0], + icon: '/logos/logo-256.webp', + apps: [...localApps, ...remoteApps], + }; + const ecosystemCache = JSON.stringify(ecosystemData, null, 2); + + server.middlewares.use((req, res, next) => { + if (req.url === '/miniapps/ecosystem.json') { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end(ecosystemCache); + return; + } + next(); + }); + + const cleanup = async () => { + await Promise.all(miniappBuildWatchers.map((watcher) => watcher.close())); + }; + server.httpServer?.on('close', cleanup); + return; + } + // 预分配端口 const portAssignments: Array<{ manifest: MiniappManifest; port: number }> = []; for (const manifest of manifests) { @@ -130,13 +218,38 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { dirName: manifest.dirName, port, server: miniappServer, - baseUrl: `https://localhost:${port}`, }); console.log(`[miniapps] ${manifest.name} (${manifest.id}) started at https://localhost:${port}`); }), ); + // 同源代理:/miniapps/{dirName}/ -> miniapp dev server + const proxyRoutes: Array<{ prefix: string; miniapp: MiniappServer }> = []; + for (const miniapp of miniappServers) { + proxyRoutes.push({ prefix: `/miniapps/${miniapp.dirName}`, miniapp }); + } + const proxyMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { + const url = req.url ?? ''; + for (const route of proxyRoutes) { + if (url === route.prefix) { + res.statusCode = 302; + res.setHeader('Location', `${route.prefix}/`); + res.end(); + return; + } + if (!url.startsWith(`${route.prefix}/`)) { + continue; + } + const nextUrl = url.slice(route.prefix.length) || '/'; + void proxyMiniappRequest(req, res, route, nextUrl).catch(next); + return; + } + next(); + }; + + prependMiddleware(server, proxyMiddleware); + // 等待所有 miniapp 启动后,fetch 各自的 /manifest.json 生成 ecosystem const generateEcosystem = async (): Promise => { const localApps = await Promise.all( @@ -148,9 +261,9 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { return { ...manifest, dirName: s.dirName, - icon: new URL(manifest.icon, s.baseUrl).href, - url: new URL('/', s.baseUrl).href, - screenshots: manifest.screenshots.map((sc) => new URL(sc, s.baseUrl).href), + icon: resolveMiniappDevAsset(manifest.icon, s.dirName), + url: `/miniapps/${s.dirName}/`, + screenshots: manifest.screenshots.map((sc) => resolveMiniappDevAsset(sc, s.dirName)), runtime, wujieConfig, }; @@ -197,6 +310,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { async closeBundle() { await Promise.all(miniappServers.map((s) => s.server.close())); + await Promise.all(miniappBuildWatchers.map((watcher) => watcher.close())); }, }; } @@ -280,6 +394,184 @@ function scanScreenshots(root: string, shortId: string): string[] { .map((f) => `screenshots/${f}`); } +function resolveMiniappDevAsset(path: string, dirName: string): string { + if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) { + return path; + } + const normalized = path.replace(/^\.\//, '').replace(/^\//, ''); + return `/miniapps/${dirName}/${normalized}`; +} + +function getRewriteMode(url: string, req: IncomingMessage): 'html' | 'js' | null { + const accept = req.headers.accept ?? ''; + const path = url.split('?', 1)[0] ?? ''; + + if (path.endsWith('/') || path.endsWith('.html') || accept.includes('text/html')) { + return 'html'; + } + + if ( + path.includes('/@') || + path.includes('/@id/') || + path.endsWith('.js') || + path.endsWith('.ts') || + path.endsWith('.tsx') || + path.endsWith('.jsx') + ) { + return 'js'; + } + + return null; +} + +function rewriteMiniappBody(body: string, prefix: string): string { + const replacements: Array<[RegExp, string]> = [ + [/(['"])\/@/g, `$1${prefix}/@`], + [/(['"])\/@id\//g, `$1${prefix}/@id/`], + [/(['"])\/src\//g, `$1${prefix}/src/`], + [/(['"])\/node_modules\//g, `$1${prefix}/node_modules/`], + [/(['"])\/@fs\//g, `$1${prefix}/@fs/`], + ]; + + let output = body; + for (const [pattern, replacement] of replacements) { + output = output.replace(pattern, replacement); + } + return output; +} + +async function proxyMiniappRequest( + req: IncomingMessage, + res: ServerResponse, + route: { prefix: string; miniapp: MiniappServer }, + nextUrl: string, +): Promise { + const rewriteMode = getRewriteMode(`${route.prefix}${nextUrl}`, req); + const body = await readRequestBody(req); + const headers = buildProxyHeaders(req, body, rewriteMode !== null); + + await new Promise((resolve, reject) => { + const proxyReq = https.request( + { + hostname: 'localhost', + port: route.miniapp.port, + path: nextUrl, + method: req.method, + headers, + rejectUnauthorized: false, + }, + (proxyRes) => { + res.statusCode = proxyRes.statusCode ?? 502; + + const outgoingHeaders = normalizeProxyHeaders(proxyRes.headers, rewriteMode !== null); + for (const [key, value] of Object.entries(outgoingHeaders)) { + if (typeof value !== 'undefined') { + res.setHeader(key, value); + } + } + + if (!rewriteMode) { + proxyRes.pipe(res); + proxyRes.on('end', resolve); + proxyRes.on('error', reject); + return; + } + + const chunks: Buffer[] = []; + proxyRes.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + proxyRes.on('end', () => { + const rawBody = Buffer.concat(chunks).toString('utf-8'); + const rewritten = rewriteMiniappBody(rawBody, route.prefix); + res.setHeader('Content-Length', Buffer.byteLength(rewritten)); + res.end(rewritten); + resolve(); + }); + proxyRes.on('error', reject); + }, + ); + + proxyReq.on('error', reject); + + if (body) { + proxyReq.write(body); + } + proxyReq.end(); + }); +} + +async function readRequestBody(req: IncomingMessage): Promise { + const method = req.method?.toUpperCase(); + if (!method || method === 'GET' || method === 'HEAD' || method === 'OPTIONS') { + return null; + } + + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + if (chunks.length === 0) return null; + return Buffer.concat(chunks); +} + +function buildProxyHeaders( + req: IncomingMessage, + body: Buffer | null, + disableCompression: boolean, +): Record { + const headers: Record = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (key.startsWith(':')) continue; + if (key === 'host') continue; + if (key === 'connection') continue; + if (key === 'content-length') continue; + if (key === 'transfer-encoding') continue; + if (key === 'accept-encoding' && disableCompression) continue; + if (typeof value === 'undefined') continue; + headers[key] = Array.isArray(value) ? value.join(', ') : value; + } + + if (disableCompression) { + headers['accept-encoding'] = 'identity'; + } + if (body) { + headers['content-length'] = String(body.length); + } + return headers; +} + +function normalizeProxyHeaders( + headers: IncomingMessage['headers'], + stripEncoding: boolean, +): Record { + const output: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (typeof value === 'undefined') continue; + if (stripEncoding && (key === 'content-encoding' || key === 'content-length' || key === 'transfer-encoding')) { + continue; + } + output[key] = value; + } + return output; +} + +function prependMiddleware( + server: ViteDevServer, + middleware: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void, +): void { + const stack = ( + server.middlewares as { + stack?: Array<{ route: string; handle: typeof middleware }>; + } + ).stack; + if (stack) { + stack.unshift({ route: '', handle: middleware }); + return; + } + server.middlewares.use(middleware); +} + function generateEcosystemDataForBuild( root: string, miniappsDir: string, diff --git a/src/lib/__tests__/dweb-update.test.ts b/src/lib/__tests__/dweb-update.test.ts index 4606a9222..011d0b206 100644 --- a/src/lib/__tests__/dweb-update.test.ts +++ b/src/lib/__tests__/dweb-update.test.ts @@ -11,7 +11,10 @@ function stubGlobals() { vi.stubGlobal('__KEYAPP_SITE_ORIGIN__', 'https://example.com/KeyApp/') vi.stubGlobal('__DEV_MODE__', false) vi.stubGlobal('__APP_VERSION__', '1.2.3') - Object.defineProperty(window, 'dwebTarget', { value: true, configurable: true }) + Object.defineProperty(window, '__native_close_watcher_kit__', { + value: {}, + configurable: true, + }) } describe('dweb update', () => { diff --git a/src/lib/crypto/secure-storage.ts b/src/lib/crypto/secure-storage.ts index a1428eae2..fa1dc149a 100644 --- a/src/lib/crypto/secure-storage.ts +++ b/src/lib/crypto/secure-storage.ts @@ -5,6 +5,7 @@ * - Web 环境:使用 AES-GCM 密码加密 + localStorage */ +import { isDweb } from '@plaoc/is-dweb'; import { encrypt, decrypt, type EncryptedData } from './encryption'; import i18n from '@/i18n'; @@ -47,7 +48,11 @@ export interface StoredData { /** 检测是否在 DWEB 环境 */ export function isDwebEnvironment(): boolean { - return typeof window !== 'undefined' && 'dwebTarget' in window; + try { + return isDweb(); + } catch { + return false; + } } /** DWEB biometric 插件类型 */ diff --git a/vite.config.ts b/vite.config.ts index 240167eb2..0f52c2f19 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -82,10 +82,10 @@ function getPreferredLanIPv4(): string | undefined { function getPackageVersion(): string { try { - const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')) as { version?: string } - return pkg.version ?? '0.0.0' + const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')) as { version?: string }; + return pkg.version ?? '0.0.0'; } catch { - return '0.0.0' + return '0.0.0'; } } @@ -112,16 +112,13 @@ export default defineConfig(({ mode }) => { const tronGridApiKey = env.TRONGRID_API_KEY ?? process.env.TRONGRID_API_KEY ?? ''; const etherscanApiKey = env.ETHERSCAN_API_KEY ?? process.env.ETHERSCAN_API_KEY ?? ''; const moralisApiKey = env.MORALIS_API_KEY ?? process.env.MORALIS_API_KEY ?? ''; - const isDevBuild = (env.VITE_DEV_MODE ?? process.env.VITE_DEV_MODE) === 'true' - const siteOrigin = - env.SITE_ORIGIN ?? - process.env.SITE_ORIGIN ?? - 'https://bioforestchain.github.io/KeyApp/' + const isDevBuild = (env.VITE_DEV_MODE ?? process.env.VITE_DEV_MODE) === 'true'; + const siteOrigin = env.SITE_ORIGIN ?? process.env.SITE_ORIGIN ?? 'https://bioforestchain.github.io/KeyApp/'; - const buildTime = new Date() - const pad = (value: number) => value.toString().padStart(2, '0') - const buildSuffix = `-${pad(buildTime.getUTCMonth() + 1)}${pad(buildTime.getUTCDate())}${pad(buildTime.getUTCHours())}` - const appVersion = `${getPackageVersion()}${isDevBuild ? buildSuffix : ''}` + const buildTime = new Date(); + const pad = (value: number) => value.toString().padStart(2, '0'); + const buildSuffix = `-${pad(buildTime.getUTCMonth() + 1)}${pad(buildTime.getUTCDate())}${pad(buildTime.getUTCHours())}`; + const appVersion = `${getPackageVersion()}${isDevBuild ? buildSuffix : ''}`; return { base: BASE_URL, @@ -162,14 +159,15 @@ export default defineConfig(({ mode }) => { retries: 3, }), miniappsPlugin({ + devMode: 'build', apps: { 'xin.dweb.teleport': { - server: 'wujie', - build: 'wujie', + server: 'iframe', + build: 'iframe', }, 'xin.dweb.biobridge': { - server: 'wujie', - build: 'wujie', + server: 'iframe', + build: 'iframe', }, }, remoteMiniapps: remoteMiniappsConfig,