web-to-miniapp

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrate 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-react
bash
npm install @worldcoin/minikit-js @worldcoin/minikit-react

Step 2 — Disable SSR for pages that use MiniKit

步骤2 — 禁用使用MiniKit页面的SSR

MiniKit depends on
window.WorldApp
which doesn't exist on the server. SSR causes hydration mismatches that silently break all React event handlers — buttons render but do nothing.
Wrap the page component in a dynamic import with
ssr: false
:
tsx
// 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.tsx
.
MiniKit依赖
window.WorldApp
,而该对象在服务器端不存在。SSR会导致hydration不匹配,进而静默破坏所有React事件处理器——按钮会渲染但点击无响应。
使用
ssr: false
的动态导入包裹页面组件:
tsx
// 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.tsx
中。

Step 3 — Add MiniKitProvider

步骤3 — 添加MiniKitProvider

Create
src/app/providers.tsx
:
tsx
'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.tsx
:
tsx
import Providers from './providers';
// ...
<body>
  <Providers>{children}</Providers>
</body>;
创建
src/app/providers.tsx
tsx
'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.tsx
中包裹子组件:
tsx
import Providers from './providers';
// ...
<body>
  <Providers>{children}</Providers>
</body>;

Step 4 — Dual-provider wallet connection

步骤4 — 双Provider钱包连接

Detect World App with
MiniKit.isInWorldApp()
and use
getWorldAppProvider()
as the EIP-1193 provider. Fall back to
window.ethereum
for browser wallets.
tsx
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()
is a standard EIP-1193 provider. Under the hood:
  • eth_requestAccounts
    MiniKit.walletAuth()
    (SIWE sign-in)
  • eth_sendTransaction
    MiniKit.sendTransaction()
    (returns
    userOpHash
    )
  • eth_chainId
    0x1e0
    (World Chain 480)
All existing
writeContract
/
readContract
calls work unchanged through this provider.
使用
MiniKit.isInWorldApp()
检测World App环境,并将
getWorldAppProvider()
作为EIP-1193 Provider。对于浏览器钱包,则回退使用
window.ethereum
tsx
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()
是标准的EIP-1193 Provider,其底层逻辑如下:
  • eth_requestAccounts
    MiniKit.walletAuth()
    (SIWE登录)
  • eth_sendTransaction
    MiniKit.sendTransaction()
    (返回
    userOpHash
  • eth_chainId
    0x1e0
    (World Chain 480)
所有现有的
writeContract
/
readContract
调用通过该Provider均可正常工作,无需修改。

Step 5 — Bundle Approve with Contract Calls

步骤5 — 将授权操作与合约调用打包

World App resets token approvals to 0 after each transaction. A separate
approve()
followed by a
transferFrom()
in the next tx will fail, the approval is already gone. Thus you should bundle the approval and your contract call in a single
sendTransaction
:
tsx
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()
会失败,因为授权已失效。因此你需要将授权操作和合约调用打包到单个
sendTransaction
中:
tsx
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
userOpHash
, not a standard tx hash. Use
useUserOperationReceipt
from
@worldcoin/minikit-react
to poll for the receipt:
tsx
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-react
中的
useUserOperationReceipt
来轮询获取收据:
tsx
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
.
IssueSymptomFix
SSR hydration mismatchButtons render but clicks do nothing
dynamic(..., { ssr: false })
MiniKit.isInstalled()
before
install()
Always
false
even in World App
Use
useMiniKit()
hook or
window.WorldApp
Permit2 uses
uint160
amounts
Silent overflowCast explicitly
eth_sendTransaction
returns
userOpHash
waitForTransactionReceipt
times out
Use
useUserOperationReceipt
from
@worldcoin/minikit-react
Missing contract whitelist
invalid_contract
error
Add to Developer Portal permissions
开发者门户 > 小程序 > 权限中添加:
  • Permit2代币——你的应用所转移的所有ERC-20代币
  • 合约入口点——你的应用直接调用的所有合约
涉及未白名单合约的交易会被拦截,并返回
invalid_contract
错误。
问题症状修复方案
SSR hydration不匹配按钮渲染但点击无响应使用
dynamic(..., { ssr: false })
install()
前调用
MiniKit.isInstalled()
即使在World App中也始终返回
false
使用
useMiniKit()
钩子或
window.WorldApp
Permit2使用
uint160
金额
静默溢出显式进行类型转换
eth_sendTransaction
返回
userOpHash
waitForTransactionReceipt
超时
使用
@worldcoin/minikit-react
中的
useUserOperationReceipt
缺少合约白名单出现
invalid_contract
错误
在开发者门户权限中添加对应合约

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