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
|
@@ -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
|
+
}
|