Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions docs/white-book/10-生态篇/02-BioSDK开发指南.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ pnpm add @biochain/bio-sdk
```typescript
import '@biochain/bio-sdk'

// KeyApp Miniapp 环境会注入:
// - window.bio (KeyApp/BioChain 专用 API)
// - window.ethereum (EIP-1193 兼容,面向 EVM: Ethereum/BSC)
// - window.tronWeb / window.tronLink (TronLink 兼容,面向 TRON)

// window.bio 现在可用
async function connect() {
const accounts = await window.bio.request({
Expand All @@ -22,6 +27,16 @@ async function connect() {
}
```

## 链标识(非常重要)

KeyApp 内部链 ID 与常见外部标识存在差异:

- `ETH` / `eth` / `0x1` → `ethereum`
- `BSC` / `bsc` / `0x38` → `binance`
- `TRON` / `tron` → `tron`

建议在调用 `window.bio` 的 `chain` 参数时使用 KeyApp 内部 ID(`ethereum` / `binance` / `tron`),避免出现“暂无支持 bsc 的钱包”这类匹配失败。

## API 参考

### request(args)
Expand Down Expand Up @@ -161,7 +176,7 @@ const result = await window.bio.request<{ txHash: string }>({

```typescript
type BioUnsignedTransaction = {
chain: string
chainId: string
data: unknown
}

Expand All @@ -182,9 +197,9 @@ const unsignedTx = await window.bio.request<BioUnsignedTransaction>({

```typescript
type BioSignedTransaction = {
chain: string
raw: string // 链特定的 raw tx(例如 EVM 的 RLP hex)
signature?: string
chainId: string
data: unknown // 链特定的签名产物(例如 EVM 的 raw tx hex)
signature: string
}

const signedTx = await window.bio.request<BioSignedTransaction>({
Expand All @@ -197,6 +212,33 @@ const signedTx = await window.bio.request<BioSignedTransaction>({
})
```

## EVM Provider(window.ethereum)

面向传统 dApp(EIP-1193 规范)的注入对象:`window.ethereum`。

常用示例:

```typescript
const chainId = await window.ethereum.request({ method: 'eth_chainId' })
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
```

当前支持(按需逐步完善):

- `eth_chainId` / `eth_accounts` / `eth_requestAccounts`
- `wallet_switchEthereumChain`
- `personal_sign` / `eth_sign` / `eth_signTypedData_v4`

## TRON Provider(window.tronLink / window.tronWeb)

面向 TronLink 生态的注入对象:`window.tronLink` 与 `window.tronWeb`(兼容常见调用路径)。

常用示例:

```typescript
const result = await window.tronLink.request({ method: 'tron_requestAccounts' })
```

## 事件

### accountsChanged
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 35 additions & 29 deletions e2e/miniapp-ui.mock.spec.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,62 @@
import { test, expect } from '@playwright/test'
import { test, expect, type Page } from '@playwright/test'

/**
* 小程序 UI 截图测试
*
* 直接测试小程序界面,验证共享组件和主题正常工作
*/

async function getMiniappBaseUrl(page: Page, appId: string): Promise<string> {
const res = await page.request.get('/miniapps/ecosystem.json')
expect(res.ok()).toBeTruthy()

const data = (await res.json()) as { apps?: Array<{ id: string; url: string }> }
const app = data.apps?.find((a) => a.id === appId)
expect(app?.url).toBeTruthy()

return app!.url
}

async function gotoMiniapp(page: Page, appId: string, query?: string): Promise<void> {
const baseUrl = await getMiniappBaseUrl(page, appId)
const url = query ? `${baseUrl}${query}` : baseUrl

await page.goto(url)
await expect(page.getByTestId('connect-button')).toBeVisible()
await expect(page.getByTestId('connect-button')).toBeEnabled()
}

test.describe('Teleport 小程序 UI', () => {
test.beforeEach(async ({ page }) => {
// 设置视口为移动端尺寸
await page.setViewportSize({ width: 375, height: 667 })
})

test('连接页面 - 初始状态', async ({ page }) => {
await page.goto('/miniapps/teleport/index.html')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(500)

await gotoMiniapp(page, 'xin.dweb.teleport')
await expect(page).toHaveScreenshot('teleport-01-connect.png')
})

test('连接页面 - 暗色主题', async ({ page }) => {
await page.goto('/miniapps/teleport/index.html?colorMode=dark')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(500)
await gotoMiniapp(page, 'xin.dweb.teleport', '?colorMode=dark')

// 手动添加 dark class(因为小程序需要接收 context)
await page.evaluate(() => {
document.documentElement.classList.add('dark')
})
await page.waitForTimeout(300)
await expect(page.getByTestId('connect-button')).toBeVisible()

await expect(page).toHaveScreenshot('teleport-02-connect-dark.png')
})

test('连接页面 - 不同主题色', async ({ page }) => {
await page.goto('/miniapps/teleport/index.html?primaryHue=200')
await page.waitForLoadState('networkidle')
await gotoMiniapp(page, 'xin.dweb.teleport', '?primaryHue=200')

// 设置蓝色主题
await page.evaluate(() => {
document.documentElement.style.setProperty('--primary-hue', '200')
})
await page.waitForTimeout(300)
await expect(page.getByTestId('connect-button')).toBeVisible()

await expect(page).toHaveScreenshot('teleport-03-connect-blue-theme.png')
})
Expand All @@ -54,34 +68,28 @@ test.describe('Forge 小程序 UI', () => {
})

test('连接页面 - 初始状态', async ({ page }) => {
await page.goto('/miniapps/forge/index.html')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(500)

await gotoMiniapp(page, 'xin.dweb.forge')
await expect(page).toHaveScreenshot('forge-01-connect.png')
})

test('连接页面 - 暗色主题', async ({ page }) => {
await page.goto('/miniapps/forge/index.html?colorMode=dark')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(500)
await gotoMiniapp(page, 'xin.dweb.forge', '?colorMode=dark')

await page.evaluate(() => {
document.documentElement.classList.add('dark')
})
await page.waitForTimeout(300)
await expect(page.getByTestId('connect-button')).toBeVisible()

await expect(page).toHaveScreenshot('forge-02-connect-dark.png')
})

test('连接页面 - 自定义主题色', async ({ page }) => {
await page.goto('/miniapps/forge/index.html?primaryHue=145')
await page.waitForLoadState('networkidle')
await gotoMiniapp(page, 'xin.dweb.forge', '?primaryHue=145')

await page.evaluate(() => {
document.documentElement.style.setProperty('--primary-hue', '145')
})
await page.waitForTimeout(300)
await expect(page.getByTestId('connect-button')).toBeVisible()

await expect(page).toHaveScreenshot('forge-03-green-theme.png')
})
Expand All @@ -92,29 +100,27 @@ test.describe('小程序主题同步', () => {
await page.setViewportSize({ width: 375, height: 667 })

// 使用绿色主题 (hue=145)
await page.goto('/miniapps/teleport/index.html?primaryHue=145&primarySaturation=0.2')
await page.waitForLoadState('networkidle')
await gotoMiniapp(page, 'xin.dweb.teleport', '?primaryHue=145&primarySaturation=0.2')

await page.evaluate(() => {
document.documentElement.style.setProperty('--primary-hue', '145')
document.documentElement.style.setProperty('--primary-saturation', '0.2')
})
await page.waitForTimeout(300)
await expect(page.getByTestId('connect-button')).toBeVisible()

await expect(page).toHaveScreenshot('teleport-04-green-theme.png')
})

test('Forge - 暗色 + 自定义主题色', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 })

await page.goto('/miniapps/forge/index.html?colorMode=dark&primaryHue=280')
await page.waitForLoadState('networkidle')
await gotoMiniapp(page, 'xin.dweb.forge', '?colorMode=dark&primaryHue=280')

await page.evaluate(() => {
document.documentElement.classList.add('dark')
document.documentElement.style.setProperty('--primary-hue', '280')
})
await page.waitForTimeout(300)
await expect(page.getByTestId('connect-button')).toBeVisible()

await expect(page).toHaveScreenshot('forge-04-dark-purple-theme.png')
})
Expand Down
44 changes: 35 additions & 9 deletions miniapps/forge/src/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ const setupMockApi = () => {
})
}

type EthereumRequest = (args: { method: string; params?: unknown[] }) => Promise<unknown>

function setupMockEthereumProvider(opts?: {
accounts?: string[]
}): void {
const accounts = opts?.accounts ?? ['0x1111111111111111111111111111111111111111']

const request: EthereumRequest = fn().mockImplementation(({ method }) => {
if (method === 'wallet_switchEthereumChain') return Promise.resolve(null)
if (method === 'eth_requestAccounts') return Promise.resolve(accounts)
if (method === 'eth_chainId') return Promise.resolve('0x1')
return Promise.resolve(null)
})

window.ethereum = { request } as unknown as typeof window.ethereum
}

const meta = {
title: 'App/ForgeApp',
component: App,
Expand All @@ -59,6 +76,7 @@ const meta = {
decorators: [
(Story) => {
setupMockApi()
setupMockEthereumProvider()
return (
<div style={{ width: '375px', height: '667px', margin: '0 auto' }}>
<Story />
Expand Down Expand Up @@ -103,14 +121,17 @@ export const SwapStep: Story = {
decorators: [
(Story) => {
setupMockApi()
setupMockEthereumProvider()
// Mock bio SDK with connected wallet
// @ts-expect-error - mock global
window.bio = {
request: fn().mockImplementation(({ method }: { method: string }) => {
request: fn().mockImplementation(({ method, params }: { method: string; params?: unknown[] }) => {
if (method === 'bio_selectAccount') {
const chain = (params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta'
return Promise.resolve({
address: '0x1234567890abcdef1234567890abcdef12345678',
chain: 'eth',
address: chain === 'bfmeta' ? 'bfmeta123' : '0x1234567890abcdef1234567890abcdef12345678',
chain,
publicKey: '0x',
})
}
if (method === 'bio_closeSplashScreen') {
Expand Down Expand Up @@ -222,13 +243,16 @@ export const TokenPicker: Story = {
decorators: [
(Story) => {
setupMockApi()
setupMockEthereumProvider()
// @ts-expect-error - mock global
window.bio = {
request: fn().mockImplementation(({ method }: { method: string }) => {
request: fn().mockImplementation(({ method, params }: { method: string; params?: unknown[] }) => {
if (method === 'bio_selectAccount') {
const chain = (params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta'
return Promise.resolve({
address: '0x1234567890abcdef1234567890abcdef12345678',
chain: 'eth',
address: chain === 'bfmeta' ? 'bfmeta123' : '0x1234567890abcdef1234567890abcdef12345678',
chain,
publicKey: '0x',
})
}
if (method === 'bio_closeSplashScreen') {
Expand Down Expand Up @@ -286,17 +310,19 @@ export const LoadingState: Story = {
decorators: [
(Story) => {
setupMockApi()
setupMockEthereumProvider()
// Mock slow bio SDK
// @ts-expect-error - mock global
window.bio = {
request: fn().mockImplementation(({ method }: { method: string }) => {
request: fn().mockImplementation(({ method, params }: { method: string; params?: unknown[] }) => {
if (method === 'bio_selectAccount') {
// Simulate slow connection
return new Promise((resolve) => {
setTimeout(() => {
resolve({
address: '0x123',
chain: 'eth',
address: ((params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta') === 'bfmeta' ? 'bfmeta123' : '0x123',
chain: (params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta',
publicKey: '0x',
})
}, 10000)
})
Expand Down
Loading