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.
- package/CHANGELOG.md +16 -0
- package/dist/index.js +24 -23
- package/package.json +1 -1
- package/template/openfort-templates/firebase/AGENTS.md +1 -1
- package/template/openfort-templates/firebase/package.json +3 -3
- package/template/openfort-templates/firebase/src/App.tsx +2 -1
- package/template/openfort-templates/firebase/src/integrations/openfort/providers.tsx +36 -35
- package/template/openfort-templates/firebase/src/lib/contracts.ts +34 -0
- package/template/openfort-templates/firebase/src/ui/openfort/blockchain/ActionsCard.tsx +25 -13
- package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletCreation.tsx +108 -63
- package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletListCard.tsx +211 -41
- package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx +45 -21
- package/template/openfort-templates/headless/AGENTS.md +1 -1
- package/template/openfort-templates/headless/package.json +2 -2
- package/template/openfort-templates/headless/src/components/cards/actions.tsx +30 -21
- package/template/openfort-templates/headless/src/components/cards/profile.tsx +0 -2
- package/template/openfort-templates/headless/src/components/cards/wallets.tsx +230 -67
- package/template/openfort-templates/headless/src/components/createWallet.tsx +115 -73
- package/template/openfort-templates/headless/src/components/passwordRecovery.tsx +48 -23
- package/template/openfort-templates/headless/src/components/providers.tsx +30 -25
- package/template/openfort-templates/headless/src/lib/contracts.ts +43 -0
- package/template/openfort-templates/openfort-ui/AGENTS.md +1 -1
- package/template/openfort-templates/openfort-ui/package.json +3 -3
- package/template/openfort-templates/openfort-ui/src/components/cards/actions.tsx +30 -15
- package/template/openfort-templates/openfort-ui/src/components/cards/auth.tsx +1 -1
- package/template/openfort-templates/openfort-ui/src/components/cards/wallets.tsx +232 -67
- package/template/openfort-templates/openfort-ui/src/components/createWallet.tsx +115 -73
- package/template/openfort-templates/openfort-ui/src/components/passwordRecovery.tsx +48 -23
- package/template/openfort-templates/openfort-ui/src/components/providers.tsx +34 -30
- package/template/openfort-templates/openfort-ui/src/lib/contracts.ts +35 -0
- package/template/openfort-templates/solana-headless/biome.json +70 -0
- package/template/openfort-templates/solana-headless/index.html +16 -0
- package/template/openfort-templates/solana-headless/package.json +34 -0
- package/template/openfort-templates/solana-headless/public/githubLogo.svg +5 -0
- package/template/openfort-templates/solana-headless/public/openfort.svg +13 -0
- package/template/openfort-templates/solana-headless/public/solanaLogo.svg +11 -0
- package/template/openfort-templates/solana-headless/src/App.tsx +7 -0
- package/template/openfort-templates/solana-headless/src/components/cards/auth.tsx +167 -0
- package/template/openfort-templates/solana-headless/src/components/cards/head.tsx +359 -0
- package/template/openfort-templates/solana-headless/src/components/cards/history.tsx +134 -0
- package/template/openfort-templates/solana-headless/src/components/cards/main.tsx +140 -0
- package/template/openfort-templates/solana-headless/src/components/cards/profile.tsx +80 -0
- package/template/openfort-templates/solana-headless/src/components/cards/send.tsx +242 -0
- package/template/openfort-templates/solana-headless/src/components/cards/sign.tsx +48 -0
- package/template/openfort-templates/solana-headless/src/components/cards/wallets.tsx +199 -0
- package/template/openfort-templates/solana-headless/src/components/createWallet.tsx +117 -0
- package/template/openfort-templates/solana-headless/src/components/passwordRecovery.tsx +167 -0
- package/template/openfort-templates/solana-headless/src/components/providers.tsx +23 -0
- package/template/openfort-templates/solana-headless/src/components/ui/Sheet.tsx +47 -0
- package/template/openfort-templates/solana-headless/src/components/ui/Tabs.tsx +111 -0
- package/template/openfort-templates/solana-headless/src/components/ui/TruncateData.tsx +31 -0
- package/template/openfort-templates/solana-headless/src/hooks/useSolanaMessageSigner.ts +37 -0
- package/template/openfort-templates/solana-headless/src/index.css +180 -0
- package/template/openfort-templates/solana-headless/src/lib/errors.ts +4 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/balance.ts +17 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/index.ts +4 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/kora.ts +137 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/transaction.ts +146 -0
- package/template/openfort-templates/solana-headless/src/lib/solana/transactionHistory.ts +39 -0
- package/template/openfort-templates/solana-headless/src/main.tsx +13 -0
- package/template/openfort-templates/solana-headless/src/vite-env.d.ts +1 -0
- package/template/openfort-templates/solana-headless/tsconfig.app.json +24 -0
- package/template/openfort-templates/solana-headless/tsconfig.json +7 -0
- package/template/openfort-templates/solana-headless/tsconfig.node.json +22 -0
- package/template/openfort-templates/solana-headless/vite.config.ts +8 -0
|
@@ -28,7 +28,7 @@ export const Auth = () => {
|
|
|
28
28
|
Click the button below to sign in with Openfort. This will open a
|
|
29
29
|
modal where you can choose your authentication method.
|
|
30
30
|
</p>
|
|
31
|
-
<OpenfortButton
|
|
31
|
+
<OpenfortButton label="Sign In" />
|
|
32
32
|
</div>
|
|
33
33
|
<div className="mt-8 space-y-2">
|
|
34
34
|
<h2>Code Example</h2>
|
|
@@ -1,40 +1,175 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CheckIcon,
|
|
3
|
+
ChevronDownIcon,
|
|
4
|
+
ChevronUpIcon,
|
|
5
|
+
ClipboardDocumentIcon,
|
|
2
6
|
FingerPrintIcon,
|
|
3
7
|
KeyIcon,
|
|
4
8
|
LockClosedIcon,
|
|
5
9
|
} from '@heroicons/react/24/outline'
|
|
6
10
|
import {
|
|
11
|
+
AccountTypeEnum,
|
|
7
12
|
RecoveryMethod,
|
|
8
|
-
type
|
|
13
|
+
type ConnectedEmbeddedEthereumWallet,
|
|
9
14
|
useSignOut,
|
|
10
15
|
useUser,
|
|
11
|
-
useWallets,
|
|
12
16
|
} from '@openfort/react'
|
|
13
|
-
import {
|
|
17
|
+
import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
|
|
18
|
+
import { useMemo, useState } from 'react'
|
|
14
19
|
import { useAccount } from 'wagmi'
|
|
15
20
|
import { CreateWallet, CreateWalletSheet } from '../createWallet'
|
|
16
21
|
import { WalletRecoverPasswordSheet } from '../passwordRecovery'
|
|
17
22
|
|
|
23
|
+
const ACCOUNT_TYPE_LABELS: Record<AccountTypeEnum, string> = {
|
|
24
|
+
[AccountTypeEnum.EOA]: 'EOA',
|
|
25
|
+
[AccountTypeEnum.SMART_ACCOUNT]: 'SM',
|
|
26
|
+
[AccountTypeEnum.DELEGATED_ACCOUNT]: 'DE',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VISIBLE_WALLET_COUNT = 4
|
|
30
|
+
|
|
31
|
+
const WalletRecoveryBadge = ({ wallet }: { wallet: ConnectedEmbeddedEthereumWallet }) => {
|
|
32
|
+
let Icon = LockClosedIcon
|
|
33
|
+
let text = 'Unknown'
|
|
34
|
+
|
|
35
|
+
switch (wallet.recoveryMethod) {
|
|
36
|
+
case RecoveryMethod.PASSWORD:
|
|
37
|
+
Icon = KeyIcon
|
|
38
|
+
text = 'Password'
|
|
39
|
+
break
|
|
40
|
+
case RecoveryMethod.AUTOMATIC:
|
|
41
|
+
Icon = LockClosedIcon
|
|
42
|
+
text = 'Automatic'
|
|
43
|
+
break
|
|
44
|
+
case RecoveryMethod.PASSKEY:
|
|
45
|
+
Icon = FingerPrintIcon
|
|
46
|
+
text = 'Passkey'
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="flex items-center text-xs">
|
|
52
|
+
<span>{text}</span>
|
|
53
|
+
<Icon className="h-5 w-5 ml-2" />
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const WalletItem = ({
|
|
59
|
+
wallet,
|
|
60
|
+
isActive,
|
|
61
|
+
isConnecting,
|
|
62
|
+
nested,
|
|
63
|
+
onClick,
|
|
64
|
+
}: {
|
|
65
|
+
wallet: ConnectedEmbeddedEthereumWallet
|
|
66
|
+
isActive: boolean
|
|
67
|
+
isConnecting: boolean
|
|
68
|
+
nested?: boolean
|
|
69
|
+
onClick: () => void
|
|
70
|
+
}) => (
|
|
71
|
+
<div className={nested ? 'ml-5 flex items-center gap-1' : undefined}>
|
|
72
|
+
{nested && <span className="text-zinc-500 text-sm">↳</span>}
|
|
73
|
+
<button
|
|
74
|
+
key={wallet.id + wallet.address}
|
|
75
|
+
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"
|
|
76
|
+
onClick={onClick}
|
|
77
|
+
data-active={isActive}
|
|
78
|
+
disabled={isActive || isConnecting}
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
{isConnecting && isActive ? (
|
|
82
|
+
<p>Connecting...</p>
|
|
83
|
+
) : (
|
|
84
|
+
<div className="flex justify-between items-center">
|
|
85
|
+
<p className="font-medium mr-2">
|
|
86
|
+
{`${wallet.address.substring(0, 6)}...${wallet.address.substring(wallet.address.length - 4)}`}
|
|
87
|
+
</p>
|
|
88
|
+
<div className="flex items-center gap-2">
|
|
89
|
+
{wallet.accountType && (
|
|
90
|
+
<span className="text-[10px] font-semibold leading-none border border-zinc-600 rounded px-1 py-0.5 opacity-70">
|
|
91
|
+
{ACCOUNT_TYPE_LABELS[wallet.accountType]}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
<WalletRecoveryBadge wallet={wallet} />
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
|
|
18
102
|
export const Wallets = () => {
|
|
19
103
|
const {
|
|
20
104
|
wallets,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} =
|
|
105
|
+
status,
|
|
106
|
+
activeWallet,
|
|
107
|
+
setActive,
|
|
108
|
+
exportPrivateKey,
|
|
109
|
+
} = useEthereumEmbeddedWallet()
|
|
110
|
+
const isLoadingWallets = status === 'fetching-wallets'
|
|
111
|
+
const isConnecting = status === 'connecting'
|
|
26
112
|
const { user, isAuthenticated } = useUser()
|
|
27
113
|
const { isConnected } = useAccount()
|
|
28
114
|
const [createWalletSheetOpen, setCreateWalletSheetOpen] = useState(false)
|
|
29
|
-
const [walletToRecover, setWalletToRecover] =
|
|
30
|
-
null
|
|
31
|
-
)
|
|
115
|
+
const [walletToRecover, setWalletToRecover] =
|
|
116
|
+
useState<ConnectedEmbeddedEthereumWallet | null>(null)
|
|
117
|
+
const [showAllWallets, setShowAllWallets] = useState(false)
|
|
118
|
+
const [exportedKey, setExportedKey] = useState<string | null>(null)
|
|
119
|
+
const [isExporting, setIsExporting] = useState(false)
|
|
120
|
+
const [exportError, setExportError] = useState<string | null>(null)
|
|
121
|
+
const [copied, setCopied] = useState(false)
|
|
32
122
|
const { signOut } = useSignOut()
|
|
33
123
|
|
|
124
|
+
// Group wallets: EOAs at top level, Smart/Delegated accounts nested under their owner
|
|
125
|
+
const { topLevel, childrenByOwner } = useMemo(() => {
|
|
126
|
+
const ownerAddresses = new Set(wallets.map((w) => w.address.toLowerCase()))
|
|
127
|
+
const childrenByOwner = new Map<string, ConnectedEmbeddedEthereumWallet[]>()
|
|
128
|
+
const topLevel: ConnectedEmbeddedEthereumWallet[] = []
|
|
129
|
+
|
|
130
|
+
for (const wallet of wallets) {
|
|
131
|
+
const owner = wallet.ownerAddress?.toLowerCase()
|
|
132
|
+
if (owner && ownerAddresses.has(owner) && owner !== wallet.address.toLowerCase()) {
|
|
133
|
+
const existing = childrenByOwner.get(owner) ?? []
|
|
134
|
+
existing.push(wallet)
|
|
135
|
+
childrenByOwner.set(owner, existing)
|
|
136
|
+
} else {
|
|
137
|
+
topLevel.push(wallet)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { topLevel, childrenByOwner }
|
|
142
|
+
}, [wallets])
|
|
143
|
+
|
|
144
|
+
const handleExportKey = async () => {
|
|
145
|
+
setIsExporting(true)
|
|
146
|
+
setExportError(null)
|
|
147
|
+
setExportedKey(null)
|
|
148
|
+
try {
|
|
149
|
+
const key = await exportPrivateKey()
|
|
150
|
+
setExportedKey(key)
|
|
151
|
+
} catch {
|
|
152
|
+
setExportError('Cannot export private key for this wallet.')
|
|
153
|
+
} finally {
|
|
154
|
+
setIsExporting(false)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const handleWalletClick = (wallet: ConnectedEmbeddedEthereumWallet) => {
|
|
159
|
+
const isActive = activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
|
|
160
|
+
if (isActive || isConnecting) return
|
|
161
|
+
if (wallet.recoveryMethod === RecoveryMethod.PASSWORD) {
|
|
162
|
+
setWalletToRecover(wallet)
|
|
163
|
+
} else {
|
|
164
|
+
setActive({ address: wallet.address })
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!activeWallet && isConnecting) return <div>recovering ...</div>
|
|
34
169
|
if (isLoadingWallets || (!user && isAuthenticated)) {
|
|
35
170
|
return <div>Loading wallets...</div>
|
|
36
171
|
}
|
|
37
|
-
if (
|
|
172
|
+
if (wallets.length === 0) {
|
|
38
173
|
return (
|
|
39
174
|
<div className="flex gap-2 flex-col w-full">
|
|
40
175
|
<h1>Create a wallet</h1>
|
|
@@ -44,46 +179,9 @@ export const Wallets = () => {
|
|
|
44
179
|
)
|
|
45
180
|
}
|
|
46
181
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const method = wallet.recoveryMethod
|
|
51
|
-
|
|
52
|
-
switch (method) {
|
|
53
|
-
case RecoveryMethod.PASSWORD:
|
|
54
|
-
Icon = KeyIcon
|
|
55
|
-
text = 'Password'
|
|
56
|
-
break
|
|
57
|
-
case RecoveryMethod.AUTOMATIC:
|
|
58
|
-
Icon = LockClosedIcon
|
|
59
|
-
text = 'Automatic'
|
|
60
|
-
break
|
|
61
|
-
case RecoveryMethod.PASSKEY:
|
|
62
|
-
Icon = FingerPrintIcon
|
|
63
|
-
text = 'Passkey'
|
|
64
|
-
break
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<div className="flex items-center text-xs">
|
|
69
|
-
<span>{text}</span>
|
|
70
|
-
<Icon className="h-5 w-5 ml-2" />
|
|
71
|
-
</div>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const handleWalletClick = (wallet: UserWallet) => {
|
|
76
|
-
if (wallet.isActive || isConnecting) return
|
|
77
|
-
const method = wallet.recoveryMethod
|
|
78
|
-
if (method === RecoveryMethod.PASSWORD) {
|
|
79
|
-
setWalletToRecover(wallet)
|
|
80
|
-
} else {
|
|
81
|
-
setActiveWallet({
|
|
82
|
-
walletId: 'xyz.openfort',
|
|
83
|
-
address: wallet.address,
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
}
|
|
182
|
+
const visibleTopLevel = showAllWallets
|
|
183
|
+
? topLevel
|
|
184
|
+
: topLevel.slice(0, VISIBLE_WALLET_COUNT)
|
|
87
185
|
|
|
88
186
|
return (
|
|
89
187
|
<div className="flex flex-col w-full">
|
|
@@ -94,34 +192,101 @@ export const Wallets = () => {
|
|
|
94
192
|
<div className="space-y-4 pb-4">
|
|
95
193
|
<h2>Your Wallets</h2>
|
|
96
194
|
<div className="flex flex-col space-y-2">
|
|
97
|
-
{
|
|
195
|
+
{visibleTopLevel.map((wallet) => {
|
|
196
|
+
const isActive =
|
|
197
|
+
activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
|
|
198
|
+
const children = childrenByOwner.get(wallet.address.toLowerCase())
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div key={wallet.id} className="space-y-1">
|
|
202
|
+
<WalletItem
|
|
203
|
+
wallet={wallet}
|
|
204
|
+
isActive={isActive}
|
|
205
|
+
isConnecting={isConnecting}
|
|
206
|
+
onClick={() => handleWalletClick(wallet)}
|
|
207
|
+
/>
|
|
208
|
+
{children?.map((child) => {
|
|
209
|
+
const isChildActive =
|
|
210
|
+
activeWallet?.address.toLowerCase() === child.address.toLowerCase()
|
|
211
|
+
return (
|
|
212
|
+
<WalletItem
|
|
213
|
+
key={child.id}
|
|
214
|
+
wallet={child}
|
|
215
|
+
isActive={isChildActive}
|
|
216
|
+
isConnecting={isConnecting}
|
|
217
|
+
nested
|
|
218
|
+
onClick={() => handleWalletClick(child)}
|
|
219
|
+
/>
|
|
220
|
+
)
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
})}
|
|
225
|
+
|
|
226
|
+
{topLevel.length > VISIBLE_WALLET_COUNT && (
|
|
98
227
|
<button
|
|
99
|
-
|
|
100
|
-
className="
|
|
101
|
-
onClick={() =>
|
|
102
|
-
data-active={wallet.isActive}
|
|
103
|
-
disabled={wallet.isActive || isConnecting}
|
|
228
|
+
type="button"
|
|
229
|
+
className="flex items-center justify-center gap-1 text-xs text-zinc-400 hover:text-zinc-200 cursor-pointer transition-colors py-1"
|
|
230
|
+
onClick={() => setShowAllWallets((prev) => !prev)}
|
|
104
231
|
>
|
|
105
|
-
{
|
|
106
|
-
|
|
232
|
+
{showAllWallets ? (
|
|
233
|
+
<>
|
|
234
|
+
Show less <ChevronUpIcon className="h-4 w-4" />
|
|
235
|
+
</>
|
|
107
236
|
) : (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{renderWalletRecovery(wallet)}
|
|
113
|
-
</div>
|
|
237
|
+
<>
|
|
238
|
+
Show {topLevel.length - VISIBLE_WALLET_COUNT} more{' '}
|
|
239
|
+
<ChevronDownIcon className="h-4 w-4" />
|
|
240
|
+
</>
|
|
114
241
|
)}
|
|
115
242
|
</button>
|
|
116
|
-
)
|
|
243
|
+
)}
|
|
117
244
|
|
|
118
245
|
<button
|
|
119
246
|
className="p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1"
|
|
120
247
|
onClick={() => setCreateWalletSheetOpen(true)}
|
|
248
|
+
type="button"
|
|
121
249
|
>
|
|
122
250
|
+ Create Wallet
|
|
123
251
|
</button>
|
|
124
252
|
</div>
|
|
253
|
+
|
|
254
|
+
{activeWallet && (
|
|
255
|
+
<div className="mt-4 space-y-2">
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
className="w-full p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors text-sm flex items-center justify-center gap-2"
|
|
259
|
+
onClick={handleExportKey}
|
|
260
|
+
disabled={isExporting}
|
|
261
|
+
>
|
|
262
|
+
<KeyIcon className="h-4 w-4" />
|
|
263
|
+
{isExporting ? 'Exporting...' : 'Export Private Key'}
|
|
264
|
+
</button>
|
|
265
|
+
{exportError && (
|
|
266
|
+
<p className="text-red-400 text-xs">{exportError}</p>
|
|
267
|
+
)}
|
|
268
|
+
{exportedKey && (
|
|
269
|
+
<div className="flex items-center gap-2 p-3 border border-zinc-700 rounded bg-zinc-800 text-xs break-all font-mono">
|
|
270
|
+
<span className="flex-1">{exportedKey}</span>
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
className="shrink-0 p-1 rounded hover:bg-zinc-700 transition-colors cursor-pointer"
|
|
274
|
+
onClick={() => {
|
|
275
|
+
navigator.clipboard.writeText(exportedKey)
|
|
276
|
+
setCopied(true)
|
|
277
|
+
setTimeout(() => setCopied(false), 2000)
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
{copied ? (
|
|
281
|
+
<CheckIcon className="h-4 w-4 text-green-400" />
|
|
282
|
+
) : (
|
|
283
|
+
<ClipboardDocumentIcon className="h-4 w-4 text-zinc-400" />
|
|
284
|
+
)}
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
125
290
|
</div>
|
|
126
291
|
<WalletRecoverPasswordSheet
|
|
127
292
|
wallet={walletToRecover}
|
|
@@ -2,119 +2,161 @@ import {
|
|
|
2
2
|
FingerPrintIcon,
|
|
3
3
|
KeyIcon,
|
|
4
4
|
LockClosedIcon,
|
|
5
|
+
WalletIcon,
|
|
5
6
|
} from '@heroicons/react/24/outline'
|
|
6
|
-
import {
|
|
7
|
+
import { AccountTypeEnum, RecoveryMethod } from '@openfort/react'
|
|
8
|
+
import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
|
|
7
9
|
import { useState } from 'react'
|
|
8
10
|
import { CreateWalletPasswordSheet } from './passwordRecovery'
|
|
9
11
|
import { Sheet } from './ui/Sheet'
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}: {
|
|
13
|
+
type CreateStep = 'choose-account-type' | 'choose-recovery-method'
|
|
14
|
+
|
|
15
|
+
type CreateWalletSheetProps = {
|
|
15
16
|
open: boolean
|
|
16
17
|
onClose: () => void
|
|
17
|
-
|
|
18
|
+
onWalletCreated?: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CreateWalletSheet = ({ open, onClose, onWalletCreated }: CreateWalletSheetProps) => {
|
|
18
22
|
return (
|
|
19
23
|
<Sheet
|
|
20
24
|
open={open}
|
|
21
|
-
onClose={
|
|
22
|
-
onClose()
|
|
23
|
-
}}
|
|
25
|
+
onClose={onClose}
|
|
24
26
|
title="Create Wallet"
|
|
25
27
|
description="Please choose a recovery method for your new wallet."
|
|
26
28
|
>
|
|
27
29
|
<CreateWallet
|
|
28
30
|
onWalletCreated={() => {
|
|
29
31
|
onClose()
|
|
32
|
+
onWalletCreated?.()
|
|
30
33
|
}}
|
|
31
34
|
/>
|
|
32
35
|
</Sheet>
|
|
33
36
|
)
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
export const CreateWallet = ({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
export const CreateWallet = ({ onWalletCreated }: { onWalletCreated?: () => void }) => {
|
|
40
|
+
const { create, status } = useEthereumEmbeddedWallet()
|
|
41
|
+
const [error, setError] = useState<string | null>(null)
|
|
42
|
+
const [step, setStep] = useState<CreateStep>('choose-account-type')
|
|
43
|
+
const [accountType, setAccountType] = useState<AccountTypeEnum>(AccountTypeEnum.SMART_ACCOUNT)
|
|
41
44
|
const [passwordSheetOpen, setPasswordSheetOpen] = useState(false)
|
|
42
45
|
|
|
43
|
-
const
|
|
44
|
-
onSuccess: () => {
|
|
45
|
-
onWalletCreated?.()
|
|
46
|
-
},
|
|
47
|
-
})
|
|
46
|
+
const isCreating = status === 'creating'
|
|
48
47
|
|
|
49
48
|
if (isCreating) {
|
|
50
49
|
return <div>Creating wallet...</div>
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
const handleCreate = async (recoveryMethod: RecoveryMethod, password?: string) => {
|
|
53
|
+
try {
|
|
54
|
+
await create({ recoveryMethod, accountType, ...(password && { password }) })
|
|
55
|
+
onWalletCreated?.()
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setError(err instanceof Error ? err.message : 'Failed to create wallet')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
return (
|
|
54
62
|
<>
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
className="
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
className="wallet-option
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
63
|
+
{step === 'choose-account-type' && (
|
|
64
|
+
<div className="flex flex-col gap-4 wallet-option-group mb-4">
|
|
65
|
+
<p className="text-sm text-zinc-400">Select account type:</p>
|
|
66
|
+
{Object.values(AccountTypeEnum).map((type) => (
|
|
67
|
+
<button
|
|
68
|
+
key={type}
|
|
69
|
+
type="button"
|
|
70
|
+
className="wallet-option cursor-pointer"
|
|
71
|
+
onClick={() => {
|
|
72
|
+
setAccountType(type)
|
|
73
|
+
setStep('choose-recovery-method')
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<WalletIcon className="h-6 w-6 shrink-0" />
|
|
77
|
+
<div className="flex flex-col text-start">
|
|
78
|
+
<h4>{type}</h4>
|
|
79
|
+
<p className="text-sm hover-description">
|
|
80
|
+
{type === AccountTypeEnum.EOA && 'A standard externally owned account.'}
|
|
81
|
+
{type === AccountTypeEnum.SMART_ACCOUNT &&
|
|
82
|
+
'A smart contract wallet (also creates an EOA owner).'}
|
|
83
|
+
{type === AccountTypeEnum.DELEGATED_ACCOUNT && 'A delegated smart account.'}
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{step === 'choose-recovery-method' && (
|
|
92
|
+
<>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
className="text-xs text-zinc-400 mb-4 hover:text-zinc-200 flex items-center gap-1"
|
|
96
|
+
onClick={() => setStep('choose-account-type')}
|
|
97
|
+
>
|
|
98
|
+
← Back ·{' '}
|
|
99
|
+
<span className="text-zinc-300">{accountType}</span>
|
|
100
|
+
</button>
|
|
101
|
+
<div className="flex flex-col gap-4 wallet-option-group mb-4">
|
|
102
|
+
<p className="text-sm text-zinc-400">Select recovery method:</p>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className="wallet-option cursor-pointer"
|
|
106
|
+
onClick={() => handleCreate(RecoveryMethod.PASSKEY)}
|
|
107
|
+
>
|
|
108
|
+
<FingerPrintIcon />
|
|
109
|
+
<div className="flex flex-col text-start">
|
|
110
|
+
<h4>Passkey</h4>
|
|
111
|
+
<p className="text-sm hover-description">
|
|
112
|
+
Secure your wallet with biometric authentication.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
</button>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
className="wallet-option cursor-pointer"
|
|
119
|
+
onClick={() => handleCreate(RecoveryMethod.AUTOMATIC)}
|
|
120
|
+
>
|
|
121
|
+
<LockClosedIcon />
|
|
122
|
+
<div className="flex flex-col text-start">
|
|
123
|
+
<h4>Automatic recovery</h4>
|
|
124
|
+
<p className="text-sm hover-description">
|
|
125
|
+
Uses encryption session to recover your wallet.
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
</button>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
className="wallet-option cursor-pointer"
|
|
132
|
+
onClick={() => setPasswordSheetOpen(true)}
|
|
133
|
+
>
|
|
134
|
+
<KeyIcon />
|
|
135
|
+
<div className="flex flex-col text-start">
|
|
136
|
+
<h4>Password</h4>
|
|
137
|
+
<p className="text-sm hover-description">
|
|
138
|
+
Create a strong password to secure your wallet.
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
</button>
|
|
102
142
|
</div>
|
|
103
|
-
|
|
104
|
-
</div>
|
|
105
|
-
{error && (
|
|
106
|
-
<p className="text-red-500 text-sm mb-2">Error: {error.message}</p>
|
|
143
|
+
</>
|
|
107
144
|
)}
|
|
145
|
+
|
|
146
|
+
{error && <p className="text-red-500 text-sm mb-2">Error: {error}</p>}
|
|
147
|
+
|
|
108
148
|
<p className="mb-4 text-xs text-zinc-400">
|
|
109
149
|
Disclaimer: This is a demo of Openfort recovery methods. In production,
|
|
110
150
|
it's best to choose one method for a smoother user experience.
|
|
111
151
|
</p>
|
|
152
|
+
|
|
112
153
|
<CreateWalletPasswordSheet
|
|
113
154
|
open={passwordSheetOpen}
|
|
114
155
|
onClose={() => setPasswordSheetOpen(false)}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
156
|
+
create={create}
|
|
157
|
+
status={status}
|
|
158
|
+
accountType={accountType}
|
|
159
|
+
onCreateWallet={onWalletCreated}
|
|
118
160
|
/>
|
|
119
161
|
</>
|
|
120
162
|
)
|