create-openfort 0.1.10 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +24 -23
  3. package/package.json +1 -1
  4. package/template/openfort-templates/firebase/AGENTS.md +1 -1
  5. package/template/openfort-templates/firebase/package.json +3 -3
  6. package/template/openfort-templates/firebase/src/App.tsx +2 -1
  7. package/template/openfort-templates/firebase/src/integrations/openfort/providers.tsx +36 -35
  8. package/template/openfort-templates/firebase/src/lib/contracts.ts +34 -0
  9. package/template/openfort-templates/firebase/src/ui/openfort/blockchain/ActionsCard.tsx +25 -13
  10. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletCreation.tsx +108 -63
  11. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletListCard.tsx +211 -41
  12. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx +45 -21
  13. package/template/openfort-templates/headless/AGENTS.md +1 -1
  14. package/template/openfort-templates/headless/package.json +2 -2
  15. package/template/openfort-templates/headless/src/components/cards/actions.tsx +30 -21
  16. package/template/openfort-templates/headless/src/components/cards/profile.tsx +0 -2
  17. package/template/openfort-templates/headless/src/components/cards/wallets.tsx +230 -67
  18. package/template/openfort-templates/headless/src/components/createWallet.tsx +115 -73
  19. package/template/openfort-templates/headless/src/components/passwordRecovery.tsx +48 -23
  20. package/template/openfort-templates/headless/src/components/providers.tsx +30 -25
  21. package/template/openfort-templates/headless/src/lib/contracts.ts +43 -0
  22. package/template/openfort-templates/openfort-ui/AGENTS.md +1 -1
  23. package/template/openfort-templates/openfort-ui/package.json +3 -3
  24. package/template/openfort-templates/openfort-ui/src/components/cards/actions.tsx +30 -15
  25. package/template/openfort-templates/openfort-ui/src/components/cards/auth.tsx +1 -1
  26. package/template/openfort-templates/openfort-ui/src/components/cards/wallets.tsx +232 -67
  27. package/template/openfort-templates/openfort-ui/src/components/createWallet.tsx +115 -73
  28. package/template/openfort-templates/openfort-ui/src/components/passwordRecovery.tsx +48 -23
  29. package/template/openfort-templates/openfort-ui/src/components/providers.tsx +34 -30
  30. package/template/openfort-templates/openfort-ui/src/lib/contracts.ts +35 -0
  31. package/template/openfort-templates/solana-headless/biome.json +70 -0
  32. package/template/openfort-templates/solana-headless/index.html +16 -0
  33. package/template/openfort-templates/solana-headless/package.json +34 -0
  34. package/template/openfort-templates/solana-headless/public/githubLogo.svg +5 -0
  35. package/template/openfort-templates/solana-headless/public/openfort.svg +13 -0
  36. package/template/openfort-templates/solana-headless/public/solanaLogo.svg +11 -0
  37. package/template/openfort-templates/solana-headless/src/App.tsx +7 -0
  38. package/template/openfort-templates/solana-headless/src/components/cards/auth.tsx +167 -0
  39. package/template/openfort-templates/solana-headless/src/components/cards/head.tsx +359 -0
  40. package/template/openfort-templates/solana-headless/src/components/cards/history.tsx +134 -0
  41. package/template/openfort-templates/solana-headless/src/components/cards/main.tsx +140 -0
  42. package/template/openfort-templates/solana-headless/src/components/cards/profile.tsx +80 -0
  43. package/template/openfort-templates/solana-headless/src/components/cards/send.tsx +242 -0
  44. package/template/openfort-templates/solana-headless/src/components/cards/sign.tsx +48 -0
  45. package/template/openfort-templates/solana-headless/src/components/cards/wallets.tsx +199 -0
  46. package/template/openfort-templates/solana-headless/src/components/createWallet.tsx +117 -0
  47. package/template/openfort-templates/solana-headless/src/components/passwordRecovery.tsx +167 -0
  48. package/template/openfort-templates/solana-headless/src/components/providers.tsx +23 -0
  49. package/template/openfort-templates/solana-headless/src/components/ui/Sheet.tsx +47 -0
  50. package/template/openfort-templates/solana-headless/src/components/ui/Tabs.tsx +111 -0
  51. package/template/openfort-templates/solana-headless/src/components/ui/TruncateData.tsx +31 -0
  52. package/template/openfort-templates/solana-headless/src/hooks/useSolanaMessageSigner.ts +37 -0
  53. package/template/openfort-templates/solana-headless/src/index.css +180 -0
  54. package/template/openfort-templates/solana-headless/src/lib/errors.ts +4 -0
  55. package/template/openfort-templates/solana-headless/src/lib/solana/balance.ts +17 -0
  56. package/template/openfort-templates/solana-headless/src/lib/solana/index.ts +4 -0
  57. package/template/openfort-templates/solana-headless/src/lib/solana/kora.ts +137 -0
  58. package/template/openfort-templates/solana-headless/src/lib/solana/transaction.ts +146 -0
  59. package/template/openfort-templates/solana-headless/src/lib/solana/transactionHistory.ts +39 -0
  60. package/template/openfort-templates/solana-headless/src/main.tsx +13 -0
  61. package/template/openfort-templates/solana-headless/src/vite-env.d.ts +1 -0
  62. package/template/openfort-templates/solana-headless/tsconfig.app.json +24 -0
  63. package/template/openfort-templates/solana-headless/tsconfig.json +7 -0
  64. package/template/openfort-templates/solana-headless/tsconfig.node.json +22 -0
  65. package/template/openfort-templates/solana-headless/vite.config.ts +8 -0
@@ -0,0 +1,242 @@
1
+ import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
2
+ import type { Address } from '@solana/kit'
3
+ import { useEffect, useState } from 'react'
4
+ import { fetchSolanaBalance, sendGaslessSolTransaction, sendSolTransaction } from '../../lib/solana'
5
+ import { TruncateData } from '../ui/TruncateData'
6
+
7
+ function CopyButton({ text }: { text: string }) {
8
+ const [copied, setCopied] = useState(false)
9
+ const handleCopy = () => {
10
+ navigator.clipboard.writeText(text)
11
+ setCopied(true)
12
+ setTimeout(() => setCopied(false), 1500)
13
+ }
14
+ return (
15
+ <button
16
+ type="button"
17
+ onClick={handleCopy}
18
+ className="text-xs text-zinc-400 hover:text-zinc-200 transition-colors cursor-pointer shrink-0"
19
+ >
20
+ {copied ? 'Copied!' : 'Copy'}
21
+ </button>
22
+ )
23
+ }
24
+
25
+ function getSolanaExplorerUrl(signature: string, cluster: string): string {
26
+ const clusterParam = cluster === 'mainnet-beta' ? '' : `?cluster=${cluster}`
27
+ return `https://explorer.solana.com/tx/${signature}${clusterParam}`
28
+ }
29
+
30
+ export const Send = () => {
31
+ const solana = useSolanaEmbeddedWallet()
32
+ const { address, cluster, rpcUrl } = solana
33
+ const [isPending, setIsPending] = useState(false)
34
+ const [txSignature, setTxSignature] = useState<string | null>(null)
35
+ const [error, setError] = useState<string | null>(null)
36
+ const [balance, setBalance] = useState<number | null>(null)
37
+ const [isLoadingBalance, setIsLoadingBalance] = useState(false)
38
+
39
+ const rpc = rpcUrl ?? 'https://api.devnet.solana.com'
40
+
41
+ useEffect(() => {
42
+ if (!address) return
43
+ setIsLoadingBalance(true)
44
+ fetchSolanaBalance(rpc, address)
45
+ .then(setBalance)
46
+ .catch(() => setBalance(null))
47
+ .finally(() => setIsLoadingBalance(false))
48
+ }, [address, rpc])
49
+
50
+ const balanceSol = balance != null ? balance / 1e9 : null
51
+ const balanceFormatted =
52
+ balanceSol != null
53
+ ? balanceSol < 0.001
54
+ ? balanceSol.toExponential(2)
55
+ : balanceSol.toFixed(4)
56
+ : null
57
+
58
+ const handleSend = async (gasless: boolean) => {
59
+ if (solana.status !== 'connected' || !address) return
60
+ const form = document.getElementById('send-sol-form') as HTMLFormElement
61
+ const recipient = (form.elements.namedItem('recipient') as HTMLInputElement)?.value?.trim()
62
+ const amountStr = (form.elements.namedItem('amount') as HTMLInputElement)?.value
63
+ if (!recipient || !amountStr) return
64
+ const amountInSol = Number.parseFloat(amountStr)
65
+ if (!Number.isFinite(amountInSol) || amountInSol <= 0) return
66
+
67
+ setIsPending(true)
68
+ setError(null)
69
+ setTxSignature(null)
70
+
71
+ try {
72
+ const provider = solana.provider
73
+ if (gasless) {
74
+ const projectKey = import.meta.env.VITE_OPENFORT_PUBLISHABLE_KEY
75
+ if (!projectKey) {
76
+ setError('VITE_OPENFORT_PUBLISHABLE_KEY is not set')
77
+ return
78
+ }
79
+ const { signature } = await sendGaslessSolTransaction({
80
+ from: address as Address,
81
+ to: recipient as Address,
82
+ amountInSol,
83
+ provider,
84
+ koraConfig: {
85
+ rpcUrl: 'https://api.openfort.io/rpc/solana/devnet',
86
+ apiKey: `Bearer ${projectKey}`,
87
+ },
88
+ })
89
+ setTxSignature(signature)
90
+ } else {
91
+ const { signature } = await sendSolTransaction({
92
+ from: address as Address,
93
+ to: recipient as Address,
94
+ amountInSol,
95
+ provider,
96
+ rpcUrl: rpc,
97
+ })
98
+ setTxSignature(signature)
99
+ }
100
+ // Refresh balance after transaction
101
+ if (address) {
102
+ fetchSolanaBalance(rpc, address).then(setBalance).catch(() => null)
103
+ }
104
+ } catch (err) {
105
+ setError(err instanceof Error ? err.message : 'Transaction failed')
106
+ } finally {
107
+ setIsPending(false)
108
+ }
109
+ }
110
+
111
+ const explorerUrl =
112
+ txSignature && cluster
113
+ ? getSolanaExplorerUrl(txSignature, cluster)
114
+ : null
115
+
116
+ const isConnected = solana.status === 'connected'
117
+ const otherWallets = solana.wallets.filter((w) => w.address !== address)
118
+
119
+ return (
120
+ <div className="flex flex-col w-full">
121
+ <h1>Send SOL</h1>
122
+ <p className="mb-4 text-sm text-zinc-400">
123
+ Transfer SOL to another address. Supports regular and gasless (Kora) transfers.
124
+ </p>
125
+
126
+ {address && (
127
+ <div className="mb-4 p-3 border border-zinc-700 rounded bg-zinc-900 text-sm space-y-1">
128
+ <div className="flex items-center justify-between">
129
+ <p className="text-zinc-400">
130
+ Address:{' '}
131
+ <span className="font-mono text-zinc-200">
132
+ {address.slice(0, 8)}...{address.slice(-6)}
133
+ </span>
134
+ </p>
135
+ <CopyButton text={address} />
136
+ </div>
137
+ <div className="flex items-center justify-between">
138
+ <p className="text-zinc-400">
139
+ Balance:{' '}
140
+ <span className="text-zinc-200">
141
+ {isLoadingBalance ? '...' : balanceFormatted != null ? `${balanceFormatted} SOL` : '--'}
142
+ </span>
143
+ </p>
144
+ {cluster !== 'mainnet-beta' && (
145
+ <a
146
+ href="https://faucet.solana.com/"
147
+ target="_blank"
148
+ rel="noreferrer"
149
+ className="text-xs text-primary hover:underline shrink-0"
150
+ >
151
+ Airdrop ↗
152
+ </a>
153
+ )}
154
+ </div>
155
+ </div>
156
+ )}
157
+
158
+ {otherWallets.length > 0 && (
159
+ <div className="mb-4 p-3 border border-zinc-700 rounded bg-zinc-900 text-sm space-y-1">
160
+ <p className="text-zinc-500 text-xs mb-2">Other wallets — Fill to set as recipient</p>
161
+ {otherWallets.map((w) => (
162
+ <div key={w.address} className="flex items-center justify-between gap-2">
163
+ <span className="font-mono text-zinc-300 text-xs truncate">
164
+ {w.address.slice(0, 10)}...{w.address.slice(-6)}
165
+ </span>
166
+ <button
167
+ type="button"
168
+ onClick={() => {
169
+ const form = document.getElementById('send-sol-form') as HTMLFormElement
170
+ const input = form?.elements.namedItem('recipient') as HTMLInputElement
171
+ if (input) input.value = w.address
172
+ }}
173
+ className="text-xs text-zinc-400 hover:text-zinc-200 transition-colors cursor-pointer shrink-0"
174
+ >
175
+ Fill
176
+ </button>
177
+ </div>
178
+ ))}
179
+ </div>
180
+ )}
181
+
182
+ <form
183
+ id="send-sol-form"
184
+ className="space-y-3"
185
+ onSubmit={(e) => e.preventDefault()}
186
+ >
187
+ <input
188
+ type="text"
189
+ name="recipient"
190
+ placeholder="Recipient address"
191
+ />
192
+ <input
193
+ type="number"
194
+ name="amount"
195
+ placeholder="Amount (SOL)"
196
+ step="0.001"
197
+ min="0.001"
198
+ defaultValue="0.001"
199
+ />
200
+ <div className="flex gap-2">
201
+ <button
202
+ type="button"
203
+ className="btn flex-1"
204
+ disabled={isPending || !isConnected}
205
+ onClick={() => handleSend(false)}
206
+ >
207
+ {isPending ? 'Sending...' : 'Send SOL'}
208
+ </button>
209
+ <button
210
+ type="button"
211
+ className="btn flex-1 bg-zinc-700 hover:bg-zinc-600"
212
+ disabled={isPending || !isConnected}
213
+ onClick={() => handleSend(true)}
214
+ >
215
+ {isPending ? 'Sending...' : 'Gasless (Kora)'}
216
+ </button>
217
+ </div>
218
+ </form>
219
+
220
+ {txSignature && (
221
+ <div className="mt-4 space-y-2">
222
+ <p className="text-green-400 text-sm">Transaction sent!</p>
223
+ <TruncateData data={txSignature} />
224
+ {explorerUrl && (
225
+ <a
226
+ href={explorerUrl}
227
+ target="_blank"
228
+ rel="noreferrer"
229
+ className="text-sm text-primary hover:underline"
230
+ >
231
+ View on Solana Explorer
232
+ </a>
233
+ )}
234
+ </div>
235
+ )}
236
+
237
+ {error && (
238
+ <TruncateData data={error} className="text-red-500" />
239
+ )}
240
+ </div>
241
+ )
242
+ }
@@ -0,0 +1,48 @@
1
+ import { useSolanaMessageSigner } from '../../hooks/useSolanaMessageSigner'
2
+ import { TruncateData } from '../ui/TruncateData'
3
+
4
+ function SignMessage() {
5
+ const { data, signMessage, isPending, error } = useSolanaMessageSigner()
6
+
7
+ return (
8
+ <div>
9
+ <h2 className="mb-3">Sign Message</h2>
10
+ <form
11
+ className="space-y-2"
12
+ onSubmit={(event) => {
13
+ event.preventDefault()
14
+ const formData = new FormData(event.target as HTMLFormElement)
15
+ signMessage({ message: formData.get('message') as string })
16
+ }}
17
+ >
18
+ <textarea
19
+ name="message"
20
+ placeholder="Message to sign"
21
+ rows={3}
22
+ className="px-4 py-2 rounded-md bg-zinc-700 border-none outline-none w-full resize-none"
23
+ />
24
+ <button type="submit" className="btn" disabled={isPending}>
25
+ {isPending ? 'Signing...' : 'Sign Message'}
26
+ </button>
27
+ </form>
28
+ {error && (
29
+ <TruncateData data={error.message} className="text-red-500" />
30
+ )}
31
+ <TruncateData data={data} />
32
+ </div>
33
+ )
34
+ }
35
+
36
+ export const Sign = () => {
37
+ return (
38
+ <div className="flex flex-col w-full">
39
+ <h1>Sign Message</h1>
40
+ <p className="mb-4 text-sm text-zinc-400">
41
+ Sign messages with your Solana embedded wallet using Ed25519.
42
+ </p>
43
+ <div className="space-y-6">
44
+ <SignMessage />
45
+ </div>
46
+ </div>
47
+ )
48
+ }
@@ -0,0 +1,199 @@
1
+ import {
2
+ ChevronDownIcon,
3
+ ChevronUpIcon,
4
+ FingerPrintIcon,
5
+ KeyIcon,
6
+ LockClosedIcon,
7
+ } from '@heroicons/react/24/outline'
8
+ import {
9
+ RecoveryMethod,
10
+ useSignOut,
11
+ useUser,
12
+ } from '@openfort/react'
13
+ import {
14
+ type ConnectedEmbeddedSolanaWallet,
15
+ useSolanaEmbeddedWallet,
16
+ } from '@openfort/react/solana'
17
+ import { useState } from 'react'
18
+ import { CreateWallet, CreateWalletSheet } from '../createWallet'
19
+ import { WalletRecoverPasswordSheet } from '../passwordRecovery'
20
+
21
+ const VISIBLE_WALLET_COUNT = 4
22
+
23
+ const WalletRecoveryBadge = ({ wallet }: { wallet: ConnectedEmbeddedSolanaWallet }) => {
24
+ let Icon = LockClosedIcon
25
+ let text = 'Unknown'
26
+
27
+ switch (wallet.recoveryMethod) {
28
+ case RecoveryMethod.PASSWORD:
29
+ Icon = KeyIcon
30
+ text = 'Password'
31
+ break
32
+ case RecoveryMethod.AUTOMATIC:
33
+ Icon = LockClosedIcon
34
+ text = 'Automatic'
35
+ break
36
+ case RecoveryMethod.PASSKEY:
37
+ Icon = FingerPrintIcon
38
+ text = 'Passkey'
39
+ break
40
+ }
41
+
42
+ return (
43
+ <div className="flex items-center text-xs">
44
+ <span>{text}</span>
45
+ <Icon className="h-5 w-5 ml-2" />
46
+ </div>
47
+ )
48
+ }
49
+
50
+ const WalletItem = ({
51
+ wallet,
52
+ isActive,
53
+ isConnecting,
54
+ onClick,
55
+ }: {
56
+ wallet: ConnectedEmbeddedSolanaWallet
57
+ isActive: boolean
58
+ isConnecting: boolean
59
+ onClick: () => void
60
+ }) => (
61
+ <button
62
+ key={wallet.id + wallet.address}
63
+ className="px-4 py-3 border data-[active=true]:border-zinc-300 border-zinc-700 rounded data-[active=false]:cursor-pointer data-[active=false]:hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1 text-sm w-full"
64
+ onClick={onClick}
65
+ data-active={isActive}
66
+ disabled={isActive || isConnecting}
67
+ type="button"
68
+ >
69
+ {isConnecting && isActive ? (
70
+ <p>Connecting...</p>
71
+ ) : (
72
+ <div className="flex justify-between items-center">
73
+ <p className="font-medium mr-2 font-mono">
74
+ {`${wallet.address.substring(0, 6)}...${wallet.address.substring(wallet.address.length - 4)}`}
75
+ </p>
76
+ <WalletRecoveryBadge wallet={wallet} />
77
+ </div>
78
+ )}
79
+ </button>
80
+ )
81
+
82
+ export const Wallets = () => {
83
+ const {
84
+ wallets,
85
+ status,
86
+ activeWallet,
87
+ setActive,
88
+ } = useSolanaEmbeddedWallet()
89
+ const isLoadingWallets = status === 'fetching-wallets'
90
+ const isConnecting = status === 'connecting'
91
+ const { user, isAuthenticated } = useUser()
92
+ const { signOut } = useSignOut()
93
+ const [createWalletSheetOpen, setCreateWalletSheetOpen] = useState(false)
94
+ const [walletToRecover, setWalletToRecover] =
95
+ useState<ConnectedEmbeddedSolanaWallet | null>(null)
96
+ const [showAllWallets, setShowAllWallets] = useState(false)
97
+
98
+ const handleWalletClick = (wallet: ConnectedEmbeddedSolanaWallet) => {
99
+ const isActive = activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
100
+ if (isActive || isConnecting) return
101
+ if (wallet.recoveryMethod === RecoveryMethod.PASSWORD) {
102
+ setWalletToRecover(wallet)
103
+ } else {
104
+ setActive({ address: wallet.address })
105
+ }
106
+ }
107
+
108
+ if (!activeWallet && isConnecting) return <div>recovering ...</div>
109
+ if (isLoadingWallets || (!user && isAuthenticated)) {
110
+ return <div>Loading wallets...</div>
111
+ }
112
+ if (wallets.length === 0) {
113
+ return (
114
+ <div className="flex gap-2 flex-col w-full">
115
+ <h1>Create a wallet</h1>
116
+ <p>You do not have any wallet yet.</p>
117
+ <CreateWallet />
118
+ </div>
119
+ )
120
+ }
121
+
122
+ const visibleWallets = showAllWallets
123
+ ? wallets
124
+ : wallets.slice(0, VISIBLE_WALLET_COUNT)
125
+
126
+ return (
127
+ <div className="flex flex-col w-full">
128
+ <h1>Wallets</h1>
129
+ <p className="mb-4 text-sm text-zinc-400">
130
+ Select a wallet to connect to your account.
131
+ </p>
132
+ <div className="space-y-4 pb-4">
133
+ <h2>Your Wallets</h2>
134
+ <div className="flex flex-col space-y-2">
135
+ {visibleWallets.map((wallet) => {
136
+ const isActive =
137
+ activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
138
+
139
+ return (
140
+ <WalletItem
141
+ key={wallet.id}
142
+ wallet={wallet}
143
+ isActive={isActive}
144
+ isConnecting={isConnecting}
145
+ onClick={() => handleWalletClick(wallet)}
146
+ />
147
+ )
148
+ })}
149
+
150
+ {wallets.length > VISIBLE_WALLET_COUNT && (
151
+ <button
152
+ type="button"
153
+ className="flex items-center justify-center gap-1 text-xs text-zinc-400 hover:text-zinc-200 cursor-pointer transition-colors py-1"
154
+ onClick={() => setShowAllWallets((prev) => !prev)}
155
+ >
156
+ {showAllWallets ? (
157
+ <>
158
+ Show less <ChevronUpIcon className="h-4 w-4" />
159
+ </>
160
+ ) : (
161
+ <>
162
+ Show {wallets.length - VISIBLE_WALLET_COUNT} more{' '}
163
+ <ChevronDownIcon className="h-4 w-4" />
164
+ </>
165
+ )}
166
+ </button>
167
+ )}
168
+
169
+ <button
170
+ className="p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1"
171
+ onClick={() => setCreateWalletSheetOpen(true)}
172
+ type="button"
173
+ >
174
+ + Create Wallet
175
+ </button>
176
+ </div>
177
+ </div>
178
+
179
+ <WalletRecoverPasswordSheet
180
+ wallet={walletToRecover}
181
+ open={!!walletToRecover}
182
+ onClose={() => setWalletToRecover(null)}
183
+ />
184
+ <CreateWalletSheet
185
+ open={createWalletSheetOpen}
186
+ onClose={() => setCreateWalletSheetOpen(false)}
187
+ />
188
+
189
+ <button
190
+ onClick={() => {
191
+ signOut()
192
+ }}
193
+ className="mt-auto btn"
194
+ >
195
+ Sign Out
196
+ </button>
197
+ </div>
198
+ )
199
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ FingerPrintIcon,
3
+ KeyIcon,
4
+ LockClosedIcon,
5
+ } from '@heroicons/react/24/outline'
6
+ import { RecoveryMethod } from '@openfort/react'
7
+ import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
8
+ import { useState } from 'react'
9
+ import { CreateWalletPasswordSheet } from './passwordRecovery'
10
+ import { Sheet } from './ui/Sheet'
11
+
12
+ type CreateWalletSheetProps = {
13
+ open: boolean
14
+ onClose: () => void
15
+ onWalletCreated?: () => void
16
+ }
17
+
18
+ export const CreateWalletSheet = ({ open, onClose, onWalletCreated }: CreateWalletSheetProps) => {
19
+ return (
20
+ <Sheet
21
+ open={open}
22
+ onClose={onClose}
23
+ title="Create Wallet"
24
+ description="Please choose a recovery method for your new wallet."
25
+ >
26
+ <CreateWallet
27
+ onWalletCreated={() => {
28
+ onClose()
29
+ onWalletCreated?.()
30
+ }}
31
+ />
32
+ </Sheet>
33
+ )
34
+ }
35
+
36
+ export const CreateWallet = ({ onWalletCreated }: { onWalletCreated?: () => void }) => {
37
+ const { create, status } = useSolanaEmbeddedWallet()
38
+ const [error, setError] = useState<string | null>(null)
39
+ const [passwordSheetOpen, setPasswordSheetOpen] = useState(false)
40
+
41
+ const isCreating = status === 'creating'
42
+
43
+ if (isCreating) {
44
+ return <div>Creating wallet...</div>
45
+ }
46
+
47
+ const handleCreate = async (recoveryMethod: RecoveryMethod, password?: string) => {
48
+ try {
49
+ await create({ recoveryMethod, ...(password && { password }) })
50
+ onWalletCreated?.()
51
+ } catch (err) {
52
+ setError(err instanceof Error ? err.message : 'Failed to create wallet')
53
+ }
54
+ }
55
+
56
+ return (
57
+ <>
58
+ <div className="flex flex-col gap-4 wallet-option-group mb-4">
59
+ <p className="text-sm text-zinc-400">Select recovery method:</p>
60
+ <button
61
+ type="button"
62
+ className="wallet-option cursor-pointer"
63
+ onClick={() => handleCreate(RecoveryMethod.PASSKEY)}
64
+ >
65
+ <FingerPrintIcon />
66
+ <div className="flex flex-col text-start">
67
+ <h4>Passkey</h4>
68
+ <p className="text-sm hover-description">
69
+ Secure your wallet with biometric authentication.
70
+ </p>
71
+ </div>
72
+ </button>
73
+ <button
74
+ type="button"
75
+ className="wallet-option cursor-pointer"
76
+ onClick={() => handleCreate(RecoveryMethod.AUTOMATIC)}
77
+ >
78
+ <LockClosedIcon />
79
+ <div className="flex flex-col text-start">
80
+ <h4>Automatic recovery</h4>
81
+ <p className="text-sm hover-description">
82
+ Uses encryption session to recover your wallet.
83
+ </p>
84
+ </div>
85
+ </button>
86
+ <button
87
+ type="button"
88
+ className="wallet-option cursor-pointer"
89
+ onClick={() => setPasswordSheetOpen(true)}
90
+ >
91
+ <KeyIcon />
92
+ <div className="flex flex-col text-start">
93
+ <h4>Password</h4>
94
+ <p className="text-sm hover-description">
95
+ Create a strong password to secure your wallet.
96
+ </p>
97
+ </div>
98
+ </button>
99
+ </div>
100
+
101
+ {error && <p className="text-red-500 text-sm mb-2">Error: {error}</p>}
102
+
103
+ <p className="mb-4 text-xs text-zinc-400">
104
+ Disclaimer: This is a demo of Openfort recovery methods. In production,
105
+ it's best to choose one method for a smoother user experience.
106
+ </p>
107
+
108
+ <CreateWalletPasswordSheet
109
+ open={passwordSheetOpen}
110
+ onClose={() => setPasswordSheetOpen(false)}
111
+ create={create}
112
+ status={status}
113
+ onCreateWallet={onWalletCreated}
114
+ />
115
+ </>
116
+ )
117
+ }