Skip to content

Commit 6e5f665

Browse files
authored
Merge pull request #27 from base/feat/demo-reset-flow
feat: demo reset flow and Base Sepolia defaults
2 parents 75f3f36 + 076a7d8 commit 6e5f665

7 files changed

Lines changed: 420 additions & 91 deletions

File tree

components/Providers.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,8 @@ import { WagmiProvider } from "wagmi";
77
import { config } from "../lib/wagmi";
88

99
const onchainKitConfig: AppConfig = {
10-
appearance: {
11-
mode: "auto",
12-
},
13-
wallet: {
14-
display: "classic",
15-
preference: "all",
16-
},
10+
appearance: { mode: "auto" },
11+
wallet: { display: "classic", preference: "all" },
1712
} as const;
1813

1914
export function Providers({ children }: PropsWithChildren) {

components/Wallet.tsx

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,58 @@
11
'use client'
22

33
import { useEffect, useMemo, useState } from 'react'
4-
import { useAccount } from 'wagmi'
5-
import { useConnect } from 'wagmi'
6-
import { useDisconnect } from 'wagmi'
7-
import { config } from '../lib/wagmi'
8-
4+
import { useAccount, useConnect, useDisconnect, useSwitchChain } from 'wagmi'
5+
import { config, baseSepolia } from '../lib/wagmi'
6+
import { useToast } from './ToastProvider'
97

108
export function WalletComponent() {
11-
const { isConnected, address } = useAccount()
9+
const { isConnected, address, connector } = useAccount()
10+
const [walletChainId, setWalletChainId] = useState<number | undefined>()
1211
const { connect, connectors } = useConnect({ config })
12+
const { switchChainAsync } = useSwitchChain()
1313
const { disconnect } = useDisconnect()
14+
const { showToast } = useToast()
1415
const [selectedConnectorId, setSelectedConnectorId] = useState<string>()
1516

17+
useEffect(() => {
18+
if (!connector) {
19+
setWalletChainId(undefined)
20+
return
21+
}
22+
23+
let cancelled = false
24+
25+
const refresh = async () => {
26+
try {
27+
const id = await connector.getChainId()
28+
if (!cancelled) setWalletChainId(id)
29+
} catch {
30+
if (!cancelled) setWalletChainId(undefined)
31+
}
32+
}
33+
34+
void refresh()
35+
36+
let provider: { on?: (event: string, handler: () => void) => void; removeListener?: (event: string, handler: () => void) => void } | undefined
37+
const onChainChanged = () => { void refresh() }
38+
39+
void connector.getProvider().then((p) => {
40+
provider = p as typeof provider
41+
provider?.on?.('chainChanged', onChainChanged)
42+
})
43+
44+
return () => {
45+
cancelled = true
46+
provider?.removeListener?.('chainChanged', onChainChanged)
47+
}
48+
}, [connector])
49+
50+
// Base Account often stays on mainnet (8453) after connect; nudge to Sepolia.
51+
useEffect(() => {
52+
if (!isConnected || walletChainId === undefined || walletChainId === baseSepolia.id) return
53+
void switchChainAsync({ chainId: baseSepolia.id }).catch(() => {})
54+
}, [isConnected, walletChainId, switchChainAsync])
55+
1656
useEffect(() => {
1757
if (selectedConnectorId || connectors.length === 0) return
1858

@@ -31,8 +71,32 @@ export function WalletComponent() {
3171
)
3272

3373
if (isConnected && address) {
74+
const copyAddress = async (event: React.MouseEvent) => {
75+
event.stopPropagation()
76+
try {
77+
await navigator.clipboard.writeText(address)
78+
showToast('Address copied', 'success')
79+
} catch {
80+
showToast('Could not copy address', 'error')
81+
}
82+
}
83+
3484
return (
35-
<div>
85+
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
86+
<div
87+
style={{
88+
padding: '0.5rem 0.75rem',
89+
border: '1px solid #d1d5db',
90+
borderRadius: '8px',
91+
background: 'white',
92+
color: '#374151',
93+
fontSize: '0.875rem',
94+
fontWeight: '500',
95+
fontFamily: 'monospace',
96+
}}
97+
>
98+
Chain {walletChainId ?? '…'}
99+
</div>
36100
<div
37101
onClick={() => disconnect()}
38102
style={{
@@ -44,11 +108,27 @@ export function WalletComponent() {
44108
fontSize: '0.875rem',
45109
fontWeight: '500',
46110
fontFamily: 'monospace',
47-
cursor: 'pointer'
111+
cursor: 'pointer',
48112
}}
49113
>
50114
{address.slice(0, 6)}...{address.slice(-4)}
51115
</div>
116+
<button
117+
type="button"
118+
onClick={copyAddress}
119+
style={{
120+
padding: '0.5rem 0.75rem',
121+
border: '1px solid #d1d5db',
122+
borderRadius: '8px',
123+
background: 'white',
124+
color: '#374151',
125+
fontSize: '0.875rem',
126+
fontWeight: '500',
127+
cursor: 'pointer',
128+
}}
129+
>
130+
Copy
131+
</button>
52132
</div>
53133
)
54134
}
@@ -90,7 +170,10 @@ export function WalletComponent() {
90170
</select>
91171
<button
92172
type="button"
93-
onClick={() => selectedConnector && connect({ connector: selectedConnector })}
173+
onClick={() =>
174+
selectedConnector &&
175+
connect({ connector: selectedConnector, chainId: baseSepolia.id })
176+
}
94177
disabled={!selectedConnector}
95178
style={{
96179
padding: '0.5rem 1rem',

lib/onchainTxErrors.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const REVERT_MESSAGES: Record<string, string> = {
2+
'0x646cf558': 'This identity has already claimed.',
3+
'0x0455eeee': 'Nothing to reset — this identity has not claimed yet.',
4+
'0x3c091c33': 'Token expired. Sign in again and retry the claim.',
5+
'0x8baa579f':
6+
'Onchain signature rejected. Ensure verify/.env ONCHAIN_SIGNING_KEY is 64 hex chars (no 0x), matches the registered signer, and restart the verify backend.',
7+
'0xd0b145db': 'Verify signer is not registered on the deployed VerifyRegistry.',
8+
}
9+
10+
const TEXT_MATCHES: Array<{ match: string; message: string }> = [
11+
{ match: 'AlreadyClaimed', message: REVERT_MESSAGES['0x646cf558'] },
12+
{ match: 'ClaimNotFound', message: REVERT_MESSAGES['0x0455eeee'] },
13+
{ match: 'TokenExpired', message: REVERT_MESSAGES['0x3c091c33'] },
14+
{ match: 'InvalidSignature', message: REVERT_MESSAGES['0x8baa579f'] },
15+
{ match: 'UntrustedSigner', message: REVERT_MESSAGES['0xd0b145db'] },
16+
{
17+
match: 'ConnectorChainMismatch',
18+
message:
19+
'Wallet chain does not match the connection. Disconnect, reconnect, and switch your wallet to the correct network before retrying.',
20+
},
21+
{
22+
match: 'insufficient funds',
23+
message: 'Insufficient Base Sepolia ETH to pay for this transaction.',
24+
},
25+
]
26+
27+
function collectErrorText(error: unknown): string {
28+
let str = ''
29+
let e: unknown = error
30+
while (e instanceof Error) {
31+
str += `${e.message} `
32+
e = (e as Error & { cause?: unknown }).cause
33+
}
34+
if (typeof error === 'string') str += error
35+
return str
36+
}
37+
38+
export function isUserRejectedWalletError(error: unknown): boolean {
39+
const message = collectErrorText(error).toLowerCase()
40+
return (
41+
message.includes('user rejected') ||
42+
message.includes('user denied') ||
43+
message.includes('rejected the request')
44+
)
45+
}
46+
47+
export function parseOnchainTxError(error: unknown): string {
48+
if (isUserRejectedWalletError(error)) {
49+
return ''
50+
}
51+
52+
const str = collectErrorText(error)
53+
54+
for (const [selector, message] of Object.entries(REVERT_MESSAGES)) {
55+
if (str.includes(selector) || str.includes(selector.slice(2))) {
56+
return message
57+
}
58+
}
59+
60+
for (const { match, message } of TEXT_MATCHES) {
61+
if (str.includes(match)) {
62+
return message
63+
}
64+
}
65+
66+
return 'Transaction failed. Please try again.'
67+
}

lib/verifyApiErrors.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const BACKEND_MESSAGE_COPY: Record<string, string> = {
2+
verification_not_found:
3+
'No Coinbase verification for this wallet. Complete verify first.',
4+
verification_traits_not_satisfied:
5+
'Verification does not meet the required traits.',
6+
credential_not_found:
7+
'No verified account found for this wallet. Complete verification first.',
8+
no_valid_credential:
9+
'No verified account found for this wallet. Complete verification first.',
10+
invalid_signature:
11+
'SIWE signature invalid. Sign in again and retry.',
12+
server_error:
13+
'Verify backend failed to sign the token. Check verify/.env ONCHAIN_SIGNING_KEY (64-char hex, no 0x prefix) and restart the backend.',
14+
}
15+
16+
function readBackendCode(body: string): string | undefined {
17+
try {
18+
const parsed = JSON.parse(body) as { message?: string; error?: string }
19+
return parsed.message || parsed.error
20+
} catch {
21+
return undefined
22+
}
23+
}
24+
25+
export function parseVerifyBackendError(status: number, body: string): string {
26+
const code = readBackendCode(body)
27+
if (code && BACKEND_MESSAGE_COPY[code]) {
28+
return BACKEND_MESSAGE_COPY[code]
29+
}
30+
if (code) {
31+
return code.replace(/_/g, ' ')
32+
}
33+
34+
switch (status) {
35+
case 400:
36+
return 'The verification request was invalid. Sign in again and retry.'
37+
case 401:
38+
return 'This app is not authorized to call Base Verify. Check your API keys.'
39+
case 404:
40+
return 'No Coinbase verification for this wallet. Complete verify first.'
41+
case 412:
42+
return 'Verification does not meet the required traits.'
43+
case 429:
44+
return 'Too many requests. Wait a moment and try again.'
45+
case 500:
46+
return 'Verify backend error. Check backend logs and ONCHAIN_SIGNING_KEY config.'
47+
default:
48+
return `Failed to get onchain verify token (${status})`
49+
}
50+
}

lib/wagmi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { createConfig, http } from "wagmi";
22
import { base, baseSepolia } from "wagmi/chains";
33
import { baseAccount } from "wagmi/connectors";
44

5+
export { baseSepolia };
6+
57
const baseAccountConnector = baseAccount({
68
appName: "Base Verify Demo",
79
appLogoUrl: "https://baseverifydemo.com/icon.png",

pages/api/onchain/verify-token.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextApiRequest, NextApiResponse } from "next";
22
import { config } from "../../../lib/config";
3+
import { parseVerifyBackendError } from "../../../lib/verifyApiErrors";
34

45
export default async function handler(
56
req: NextApiRequest,
@@ -49,7 +50,7 @@ export default async function handler(
4950
verifyResponse.statusText
5051
);
5152
return res.status(verifyResponse.status).json({
52-
error: "Failed to get onchain verify token",
53+
error: parseVerifyBackendError(verifyResponse.status, responseBody),
5354
});
5455
}
5556

0 commit comments

Comments
 (0)