web-to-miniapp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMigrate Next.js Web App to World App Mini App
将Next.js Web应用迁移至World App小程序
You are converting an existing Next.js web app that uses viem to work as a World App mini app. Follow these steps in order.
你正在将一个使用viem的现有Next.js Web应用转换为World App小程序,请按以下步骤依次操作。
Step 1 — Install MiniKit
步骤1 — 安装MiniKit
bash
npm install @worldcoin/minikit-js @worldcoin/minikit-reactbash
npm install @worldcoin/minikit-js @worldcoin/minikit-reactStep 2 — Disable SSR for pages that use MiniKit
步骤2 — 禁用使用MiniKit页面的SSR
MiniKit depends on which doesn't exist on the server. SSR causes hydration mismatches that silently break all React event handlers — buttons render but do nothing.
window.WorldAppWrap the page component in a dynamic import with :
ssr: falsetsx
// src/app/page.tsx
'use client';
import dynamic from 'next/dynamic';
const App = dynamic(() => import('../components/App'), { ssr: false });
export default function Page() {
return <App />;
}Move the actual page logic into .
src/components/App.tsxMiniKit依赖,而该对象在服务器端不存在。SSR会导致hydration不匹配,进而静默破坏所有React事件处理器——按钮会渲染但点击无响应。
window.WorldApp使用的动态导入包裹页面组件:
ssr: falsetsx
// src/app/page.tsx
'use client';
import dynamic from 'next/dynamic';
const App = dynamic(() => import('../components/App'), { ssr: false });
export default function Page() {
return <App />;
}将实际页面逻辑移至中。
src/components/App.tsxStep 3 — Add MiniKitProvider
步骤3 — 添加MiniKitProvider
Create :
src/app/providers.tsxtsx
'use client';
import { MiniKitProvider } from '@worldcoin/minikit-js/minikit-provider';
export default function Providers({ children }: { children: React.ReactNode }) {
return <MiniKitProvider>{children}</MiniKitProvider>;
}Wrap children in :
src/app/layout.tsxtsx
import Providers from './providers';
// ...
<body>
<Providers>{children}</Providers>
</body>;创建:
src/app/providers.tsxtsx
'use client';
import { MiniKitProvider } from '@worldcoin/minikit-js/minikit-provider';
export default function Providers({ children }: { children: React.ReactNode }) {
return <MiniKitProvider>{children}</MiniKitProvider>;
}在中包裹子组件:
src/app/layout.tsxtsx
import Providers from './providers';
// ...
<body>
<Providers>{children}</Providers>
</body>;Step 4 — Dual-provider wallet connection
步骤4 — 双Provider钱包连接
Detect World App with and use as the EIP-1193 provider. Fall back to for browser wallets.
MiniKit.isInWorldApp()getWorldAppProvider()window.ethereumtsx
import { MiniKit } from '@worldcoin/minikit-js';
import { getWorldAppProvider } from '@worldcoin/minikit-js';
import { createWalletClient, custom } from 'viem';
import { worldchain } from 'viem/chains';
const provider = MiniKit.isInWorldApp()
? getWorldAppProvider()
: window.ethereum;
const walletClient = createWalletClient({
chain: worldchain,
transport: custom(provider),
});getWorldAppProvider()- →
eth_requestAccounts(SIWE sign-in)MiniKit.walletAuth() - →
eth_sendTransaction(returnsMiniKit.sendTransaction())userOpHash - →
eth_chainId(World Chain 480)0x1e0
All existing / calls work unchanged through this provider.
writeContractreadContract使用检测World App环境,并将作为EIP-1193 Provider。对于浏览器钱包,则回退使用。
MiniKit.isInWorldApp()getWorldAppProvider()window.ethereumtsx
import { MiniKit } from '@worldcoin/minikit-js';
import { getWorldAppProvider } from '@worldcoin/minikit-js';
import { createWalletClient, custom } from 'viem';
import { worldchain } from 'viem/chains';
const provider = MiniKit.isInWorldApp()
? getWorldAppProvider()
: window.ethereum;
const walletClient = createWalletClient({
chain: worldchain,
transport: custom(provider),
});getWorldAppProvider()- →
eth_requestAccounts(SIWE登录)MiniKit.walletAuth() - →
eth_sendTransaction(返回MiniKit.sendTransaction())userOpHash - →
eth_chainId(World Chain 480)0x1e0
所有现有的/调用通过该Provider均可正常工作,无需修改。
writeContractreadContractStep 5 — Bundle Approve with Contract Calls
步骤5 — 将授权操作与合约调用打包
World App resets token approvals to 0 after each transaction. A separate followed by a in the next tx will fail, the approval is already gone. Thus you should bundle the approval and your contract call in a single :
approve()transferFrom()sendTransactiontsx
import { MiniKit } from '@worldcoin/minikit-js';
import { encodeFunctionData } from 'viem';
await MiniKit.sendTransaction({
chainId: 480,
transactions: [
{
to: TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [CONTRACT, amount],
}),
},
{
to: CONTRACT,
data: encodeFunctionData({
abi: contractAbi,
functionName: 'swap',
args: [amount],
}),
},
],
});In World App, these execute atomically. On web, they execute sequentially — each requires a separate wallet confirmation and is not atomic.
World App会在每笔交易后将代币授权重置为0。如果先单独执行,再在下一笔交易中执行会失败,因为授权已失效。因此你需要将授权操作和合约调用打包到单个中:
approve()transferFrom()sendTransactiontsx
import { MiniKit } from '@worldcoin/minikit-js';
import { encodeFunctionData } from 'viem';
await MiniKit.sendTransaction({
chainId: 480,
transactions: [
{
to: TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [CONTRACT, amount],
}),
},
{
to: CONTRACT,
data: encodeFunctionData({
abi: contractAbi,
functionName: 'swap',
args: [amount],
}),
},
],
});在World App中,这些操作会原子性执行;在Web端,它们会按顺序执行——每一步都需要单独的钱包确认,且不具备原子性。
Step 6 — Handle userOpHash receipts
步骤6 — 处理userOpHash收据
MiniKit returns a , not a standard tx hash. Use from to poll for the receipt:
userOpHashuseUserOperationReceipt@worldcoin/minikit-reacttsx
import { useUserOperationReceipt } from '@worldcoin/minikit-react';
import { createPublicClient, http } from 'viem';
import { worldchain } from 'viem/chains';
const client = createPublicClient({
chain: worldchain,
transport: http(),
});
const { poll, isLoading } = useUserOperationReceipt({ client });
// After sendTransaction:
const result = await MiniKit.sendTransaction({ ... });
await poll(result.data.userOpHash);MiniKit返回的是,而非标准交易哈希。使用中的来轮询获取收据:
userOpHash@worldcoin/minikit-reactuseUserOperationReceipttsx
import { useUserOperationReceipt } from '@worldcoin/minikit-react';
import { createPublicClient, http } from 'viem';
import { worldchain } from 'viem/chains';
const client = createPublicClient({
chain: worldchain,
transport: http(),
});
const { poll, isLoading } = useUserOperationReceipt({ client });
// 执行sendTransaction后:
const result = await MiniKit.sendTransaction({ ... });
await poll(result.data.userOpHash);Step 7 — Whitelist contracts and tokens
步骤7 — 白名单合约与代币
In the Developer Portal > Mini App > Permissions, add:
- Permit2 Tokens — every ERC-20 your app transfers
- Contract Entrypoints — every contract your app calls directly
Transactions touching non-whitelisted contracts are blocked with .
invalid_contract| Issue | Symptom | Fix |
|---|---|---|
| SSR hydration mismatch | Buttons render but clicks do nothing | |
| Always | Use |
Permit2 uses | Silent overflow | Cast explicitly |
| | Use |
| Missing contract whitelist | | Add to Developer Portal permissions |
在开发者门户 > 小程序 > 权限中添加:
- Permit2代币——你的应用所转移的所有ERC-20代币
- 合约入口点——你的应用直接调用的所有合约
涉及未白名单合约的交易会被拦截,并返回错误。
invalid_contract| 问题 | 症状 | 修复方案 |
|---|---|---|
| SSR hydration不匹配 | 按钮渲染但点击无响应 | 使用 |
在 | 即使在World App中也始终返回 | 使用 |
Permit2使用 | 静默溢出 | 显式进行类型转换 |
| | 使用 |
| 缺少合约白名单 | 出现 | 在开发者门户权限中添加对应合约 |
Tip: Debugging in the webview
调试技巧:Webview中调试
There are no browser devtools in World App's webview. Add eruda for a mobile console:
html
<!-- In layout.tsx body -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
eruda.init();
</script>World App的Webview中没有浏览器开发者工具。可以添加eruda来获取移动端控制台:
html
<!-- 在layout.tsx的body中添加 -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
eruda.init();
</script>Bonus: Think about World ID
额外建议:考虑使用World ID
Think about places where you could use privacy preserving sybil resistance with World ID. The World ID SDK
思考一下可以使用World ID提供隐私保护型女巫抵抗机制的场景。详情请查看World ID SDK