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,359 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react'
2
+
3
+ type RoutePoint = {
4
+ x: number
5
+ y: number
6
+ delay: number
7
+ }
8
+
9
+ type Route = {
10
+ start: RoutePoint
11
+ end: RoutePoint
12
+ color: string
13
+ outDelay?: number
14
+ }
15
+
16
+ function withOpacity(color: string, opacity: number) {
17
+ if (color.startsWith('rgb')) {
18
+ const nums = color.match(/[\d.]+/g)?.map(Number)
19
+ if (!nums || nums.length < 3) throw new Error('Invalid rgb format')
20
+ const [r, g, b] = nums
21
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`
22
+ }
23
+
24
+ throw new Error(`Unsupported color format: ${color}`)
25
+ }
26
+
27
+ const GlowCanvas = ({
28
+ accentColor,
29
+ backgroundColor,
30
+ }: {
31
+ accentColor: string
32
+ backgroundColor: string
33
+ }) => {
34
+ const canvasRef = useRef<HTMLCanvasElement | null>(null)
35
+ const mousePosRef = useRef<{ x: number; y: number } | null>(null)
36
+ const animationRef = useRef<number | null>(null)
37
+
38
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
39
+
40
+ const color = useMemo(() => withOpacity(accentColor, 0.7), [accentColor])
41
+
42
+ useEffect(() => {
43
+ const canvas = canvasRef.current
44
+ if (!canvas) return
45
+
46
+ const resizeObserver = new ResizeObserver((entries) => {
47
+ const { width, height } = entries[0].contentRect
48
+ setDimensions({ width, height })
49
+ canvas.width = width
50
+ canvas.height = height
51
+ })
52
+
53
+ resizeObserver.observe(canvas.parentElement as Element)
54
+ return () => resizeObserver.disconnect()
55
+ }, [])
56
+
57
+ useEffect(() => {
58
+ if (!dimensions.width || !dimensions.height) return
59
+
60
+ let startTime = Date.now()
61
+
62
+ const canvas = canvasRef.current
63
+ if (!canvas) return
64
+ const ctx = canvas.getContext('2d')
65
+ if (!ctx) return
66
+
67
+ const gap = 14
68
+ const dotRadius = 1.5
69
+
70
+ const generateDots = (width: number, height: number) => {
71
+ const dots: { x: number; y: number; radius: number; opacity: number }[] =
72
+ []
73
+
74
+ for (let x = 0; x < width; x += gap) {
75
+ for (let y = 0; y < height; y += gap) {
76
+ if (Math.random() > 0.4) {
77
+ dots.push({
78
+ x,
79
+ y,
80
+ radius: dotRadius,
81
+ opacity: Math.random() * 0.4 + 0.1,
82
+ })
83
+ }
84
+ }
85
+ }
86
+ return dots
87
+ }
88
+ const dots = generateDots(dimensions.width, dimensions.height)
89
+
90
+ const draw = () => {
91
+ ctx.clearRect(0, 0, dimensions.width, dimensions.height)
92
+
93
+ if (mousePosRef.current) {
94
+ const { x, y } = mousePosRef.current
95
+
96
+ ctx.shadowBlur = 25
97
+ ctx.shadowColor = backgroundColor
98
+
99
+ dots.forEach((dot) => {
100
+ const dx = dot.x - x
101
+ const dy = dot.y - y
102
+ const distance = Math.sqrt(dx * dx + dy * dy)
103
+
104
+ ctx.beginPath()
105
+ ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2)
106
+
107
+ const opacityFactor = 1 - distance / 100
108
+
109
+ ctx.fillStyle = withOpacity(accentColor, opacityFactor)
110
+ ctx.fill()
111
+ })
112
+ }
113
+
114
+ // Draw the dots
115
+ dots.forEach((dot) => {
116
+ ctx.beginPath()
117
+ ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2)
118
+ ctx.fillStyle = withOpacity(backgroundColor, dot.opacity)
119
+ ctx.fill()
120
+ })
121
+
122
+ drawRoutes()
123
+ // If all routes are complete, restart the animation
124
+ const currentTime = (Date.now() - startTime) / 1000
125
+ if (currentTime > 25) {
126
+ // Reset after 15 seconds
127
+ startTime = Date.now()
128
+ }
129
+
130
+ animationRef.current = requestAnimationFrame(draw)
131
+ }
132
+
133
+ // Set up routes that will animate across the map
134
+ const yOffset = 50
135
+ const scale = 12
136
+ const h = 11 * scale
137
+ const w = 18 * scale
138
+ const xOffset = (dimensions.width - w) / 2
139
+
140
+ const routes: Route[] = [
141
+ {
142
+ start: { x: xOffset, y: yOffset + h, delay: 0 },
143
+ end: { x: xOffset, y: yOffset, delay: 2 },
144
+ color,
145
+ outDelay: 10,
146
+ },
147
+ {
148
+ start: { x: xOffset, y: yOffset, delay: 2 },
149
+ end: { x: xOffset + w, y: yOffset, delay: 4 },
150
+ color,
151
+ outDelay: 10,
152
+ },
153
+ {
154
+ start: { x: xOffset + w, y: yOffset, delay: 4 },
155
+ end: { x: xOffset + w, y: yOffset + h, delay: 6 },
156
+ color,
157
+ outDelay: 10,
158
+ },
159
+
160
+ {
161
+ start: { x: xOffset + w / 4, y: yOffset + h / 3, delay: 4 },
162
+ end: { x: xOffset + w / 4, y: yOffset + h, delay: 6 },
163
+ color,
164
+ outDelay: 10,
165
+ },
166
+ {
167
+ start: { x: xOffset + (3 * w) / 4, y: yOffset + h / 3, delay: 2 },
168
+ end: { x: xOffset + w / 4, y: yOffset + h / 3, delay: 4 },
169
+ color,
170
+ outDelay: 10,
171
+ },
172
+ {
173
+ start: { x: xOffset + (3 * w) / 4, y: yOffset + h, delay: 0 },
174
+ end: { x: xOffset + (3 * w) / 4, y: yOffset + h / 3, delay: 2 },
175
+ color,
176
+ outDelay: 10,
177
+ },
178
+
179
+ {
180
+ start: { x: xOffset + (2 * w) / 4, y: yOffset + h, delay: 6 },
181
+ end: { x: xOffset + (2 * w) / 4, y: yOffset + (2 * h) / 3, delay: 7 },
182
+ color,
183
+ outDelay: 10,
184
+ },
185
+
186
+ {
187
+ start: { x: 200, y: 280, delay: 2 },
188
+ end: { x: 260, y: 420, delay: 4 },
189
+ color,
190
+ },
191
+ {
192
+ start: { x: 50, y: 550, delay: 2 },
193
+ end: { x: 150, y: 380, delay: 7 },
194
+ outDelay: 4,
195
+ color,
196
+ },
197
+ {
198
+ start: { x: 280, y: 260, delay: 7.5 },
199
+ end: { x: 80, y: 280, delay: 12.5 },
200
+ color,
201
+ },
202
+
203
+ {
204
+ start: { x: 380, y: 460, delay: 15 },
205
+ end: { x: 180, y: 520, delay: 20 },
206
+ color,
207
+ },
208
+ ]
209
+
210
+ function drawRoutes() {
211
+ if (!ctx) return
212
+
213
+ const debug = false
214
+
215
+ const currentTime = (Date.now() - startTime) / 1000 // Time in seconds
216
+ routes.forEach((route) => {
217
+ const elapsed = debug ? 100 : currentTime - route.start.delay
218
+ if (elapsed <= 0) return
219
+
220
+ const duration = route.end.delay - route.start.delay // animation duration
221
+ const progress = Math.min(elapsed / duration, 1) // normal progress
222
+
223
+ // Fade from end to start after the line is fully drawn
224
+ let startProgress = 0 // the starting point along the line
225
+ const outDelay = route.outDelay || 0
226
+ if (elapsed > duration + outDelay) {
227
+ const fadeProgress = Math.min(
228
+ (elapsed - (duration + outDelay)) / duration,
229
+ 1,
230
+ )
231
+ startProgress = fadeProgress // move the start point forward
232
+ }
233
+
234
+ // Line vector
235
+ const dx = route.end.x - route.start.x
236
+ const dy = route.end.y - route.start.y
237
+
238
+ for (let x = 0; x < dimensions.width; x += gap) {
239
+ for (let y = 0; y < dimensions.height; y += gap) {
240
+ const t =
241
+ ((x - route.start.x) * dx + (y - route.start.y) * dy) /
242
+ (dx * dx + dy * dy)
243
+ if (t < startProgress || t > progress) continue // only show dots in the visible segment
244
+
245
+ const closestX = route.start.x + dx * t
246
+ const closestY = route.start.y + dy * t
247
+ const distance = Math.hypot(x - closestX, y - closestY)
248
+
249
+ if (distance < gap) {
250
+ ctx.beginPath()
251
+ ctx.arc(x, y, dotRadius, 0, Math.PI * 2)
252
+
253
+ // Fade in: opacity goes from 0 → 1 as `progress` grows
254
+ const opacity = Math.min(
255
+ 1,
256
+ Math.max(0, Math.min(t - startProgress, progress - t) * 5),
257
+ )
258
+ ctx.fillStyle = withOpacity(route.color, opacity)
259
+
260
+ ctx.fill()
261
+ }
262
+ }
263
+ }
264
+
265
+ if (debug) {
266
+ const xStart = route.start.x
267
+ const yStart = route.start.y
268
+ const xEnd = route.start.x + dx * progress
269
+ const yEnd = route.start.y + dy * progress
270
+
271
+ ctx.beginPath()
272
+ ctx.moveTo(xStart, yStart)
273
+ ctx.lineTo(xEnd, yEnd)
274
+ ctx.strokeStyle = route.color
275
+ ctx.lineWidth = 1.5
276
+ ctx.stroke()
277
+
278
+ ctx.beginPath()
279
+ ctx.arc(xEnd, yEnd, 3, 0, Math.PI * 2)
280
+ ctx.fillStyle = route.color
281
+ ctx.fill()
282
+ }
283
+ })
284
+ }
285
+
286
+ const handleMouseMove = (e: MouseEvent) => {
287
+ const rect = canvas.getBoundingClientRect()
288
+ mousePosRef.current = {
289
+ x: e.x - rect.left,
290
+ y: e.y - rect.top,
291
+ }
292
+ }
293
+
294
+ document.addEventListener('mousemove', handleMouseMove)
295
+
296
+ draw()
297
+
298
+ return () => {
299
+ document.removeEventListener('mousemove', handleMouseMove)
300
+ if (animationRef.current) cancelAnimationFrame(animationRef.current)
301
+ }
302
+ }, [dimensions, accentColor, backgroundColor, color])
303
+
304
+ return (
305
+ <canvas
306
+ ref={canvasRef}
307
+ className="absolute inset-0 w-full h-full pointer-events-none"
308
+ />
309
+ )
310
+ }
311
+
312
+ export const Head = ({
313
+ onStart,
314
+ sample,
315
+ color,
316
+ backgroundColor = color,
317
+ logo,
318
+ href,
319
+ subtitle,
320
+ }: {
321
+ onStart: () => void
322
+ sample: string
323
+ color: string
324
+ backgroundColor?: string
325
+ logo: string
326
+ href: string
327
+ subtitle?: string
328
+ }) => {
329
+ return (
330
+ <div
331
+ className="bg-zinc-900 w-(--card-width) flex flex-col items-center justify-center text-center relative"
332
+ style={{ '--color-sample': color } as React.CSSProperties}
333
+ >
334
+ <div className='z-10'>
335
+ <a href="https://openfort.io/" target="_blank" rel="noopener">
336
+ <img src="/openfort.svg" className="logo" alt="Openfort logo" />
337
+ </a>
338
+ <a href={href} target="_blank">
339
+ <img src={logo} className="logo sample-logo" alt={`${sample} logo`} />
340
+ </a>
341
+ </div>
342
+ <h1 className="relative z-10 text-4xl font-bold mb-4">
343
+ <span style={{ color: '#FC3627' }}>Openfort</span> +{' '}
344
+ <span style={{ color }}>{sample}</span>
345
+ </h1>
346
+ {subtitle && <p className="relative z-10 mb-6 text-sm max-w-2/3">{subtitle}</p>}
347
+ <button
348
+ className="relative z-10 lg:hidden mt-4 px-6 py-2 border border-zinc-500 rounded hover:bg-zinc-500/10 transition-colors cursor-pointer"
349
+ onClick={onStart}
350
+ >
351
+ Click here to Start
352
+ </button>
353
+ <p className="absolute z-10 text-zinc-400 mb-6 text-sm bottom-0">
354
+ Sign in to explore openfort capabilities.
355
+ </p>
356
+ <GlowCanvas accentColor={color} backgroundColor={backgroundColor} />
357
+ </div>
358
+ )
359
+ }
@@ -0,0 +1,134 @@
1
+ import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
2
+ import { useEffect, useState } from 'react'
3
+ import { getTransactionHistory } from '../../lib/solana'
4
+ import { TruncateData } from '../ui/TruncateData'
5
+
6
+ interface TransactionHistoryItem {
7
+ signature: string
8
+ slot: number
9
+ blockTime: number | null
10
+ err: unknown | null
11
+ memo: string | null
12
+ }
13
+
14
+ function getSolanaExplorerUrl(signature: string, cluster: string): string {
15
+ const clusterParam = cluster === 'mainnet-beta' ? '' : `?cluster=${cluster}`
16
+ return `https://explorer.solana.com/tx/${signature}${clusterParam}`
17
+ }
18
+
19
+ const COLLAPSED_COUNT = 4
20
+
21
+ export const History = () => {
22
+ const { address, cluster, rpcUrl } = useSolanaEmbeddedWallet()
23
+ const [transactions, setTransactions] = useState<TransactionHistoryItem[]>([])
24
+ const [isLoading, setIsLoading] = useState(false)
25
+ const [error, setError] = useState<string | null>(null)
26
+ const [showAll, setShowAll] = useState(false)
27
+
28
+ const rpc = rpcUrl ?? 'https://api.devnet.solana.com'
29
+
30
+ const fetchHistory = () => {
31
+ if (!address) return
32
+ setIsLoading(true)
33
+ setError(null)
34
+ getTransactionHistory(address, 20, rpc)
35
+ .then(setTransactions)
36
+ .catch((err) => setError(err instanceof Error ? err.message : 'Failed to load history'))
37
+ .finally(() => setIsLoading(false))
38
+ }
39
+
40
+ useEffect(() => {
41
+ fetchHistory()
42
+ // eslint-disable-next-line react-hooks/exhaustive-deps
43
+ }, [address, rpc])
44
+
45
+ const visible = showAll ? transactions : transactions.slice(0, COLLAPSED_COUNT)
46
+ const hasMore = transactions.length > COLLAPSED_COUNT
47
+
48
+ const formatTime = (blockTime: number | null) => {
49
+ if (!blockTime) return '--'
50
+ return new Date(blockTime * 1000).toLocaleString()
51
+ }
52
+
53
+ return (
54
+ <div className="flex flex-col w-full">
55
+ <div className="flex items-center justify-between mb-4">
56
+ <div>
57
+ <h1>Transaction History</h1>
58
+ <p className="text-sm text-zinc-400">Recent transactions for your Solana wallet.</p>
59
+ </div>
60
+ <button
61
+ type="button"
62
+ onClick={fetchHistory}
63
+ disabled={isLoading}
64
+ className="p-2 border border-zinc-700 rounded hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors cursor-pointer text-xs"
65
+ >
66
+ {isLoading ? '...' : 'Refresh'}
67
+ </button>
68
+ </div>
69
+
70
+ {error && <TruncateData data={error} className="text-red-500" />}
71
+
72
+ {isLoading ? (
73
+ <p className="text-sm text-zinc-400">Loading...</p>
74
+ ) : transactions.length === 0 ? (
75
+ <p className="text-sm text-zinc-400">No transactions found.</p>
76
+ ) : (
77
+ <div className="space-y-2">
78
+ {visible.map((tx) => {
79
+ const explorerUrl = cluster
80
+ ? getSolanaExplorerUrl(tx.signature, cluster)
81
+ : null
82
+ const isError = tx.err != null
83
+
84
+ return (
85
+ <div
86
+ key={tx.signature}
87
+ className="flex items-center justify-between text-sm border-b border-zinc-700 pb-2 last:border-0"
88
+ >
89
+ <div className="flex flex-col gap-0.5 min-w-0 flex-1 mr-2">
90
+ <span className="font-mono text-xs truncate">
91
+ {explorerUrl ? (
92
+ <a
93
+ href={explorerUrl}
94
+ target="_blank"
95
+ rel="noreferrer"
96
+ className="text-primary hover:underline"
97
+ >
98
+ {tx.signature.slice(0, 20)}...
99
+ </a>
100
+ ) : (
101
+ `${tx.signature.slice(0, 20)}...`
102
+ )}
103
+ </span>
104
+ <span className="text-xs text-zinc-400">{formatTime(tx.blockTime)}</span>
105
+ </div>
106
+ <span
107
+ className={`text-xs font-medium px-2 py-0.5 rounded-full shrink-0 ${
108
+ isError
109
+ ? 'bg-red-900/30 text-red-400'
110
+ : 'bg-green-900/30 text-green-400'
111
+ }`}
112
+ >
113
+ {isError ? 'Failed' : 'Success'}
114
+ </span>
115
+ </div>
116
+ )
117
+ })}
118
+
119
+ {hasMore && (
120
+ <button
121
+ type="button"
122
+ onClick={() => setShowAll((v) => !v)}
123
+ className="text-sm text-primary hover:underline w-full text-center pt-1 cursor-pointer"
124
+ >
125
+ {showAll
126
+ ? 'Show less'
127
+ : `Show more (${transactions.length - COLLAPSED_COUNT})`}
128
+ </button>
129
+ )}
130
+ </div>
131
+ )}
132
+ </div>
133
+ )
134
+ }
@@ -0,0 +1,140 @@
1
+ import {
2
+ ClockIcon,
3
+ HomeIcon,
4
+ PaperAirplaneIcon,
5
+ PencilSquareIcon,
6
+ WalletIcon,
7
+ } from '@heroicons/react/24/outline'
8
+ import { useUser } from '@openfort/react'
9
+ import { useState } from 'react'
10
+ import { DesktopTabGroup, MobileTabGroup, type TabType } from '../ui/Tabs'
11
+ import { Auth } from './auth'
12
+ import { Head } from './head'
13
+ import { History } from './history'
14
+ import { Profile } from './profile'
15
+ import { Send } from './send'
16
+ import { Sign } from './sign'
17
+ import { Wallets } from './wallets'
18
+
19
+ interface LayoutProps {
20
+ children: React.ReactNode
21
+ step: number
22
+ tabs?: TabType[]
23
+ currentTab?: TabType
24
+ setCurrentTab?: (tab: TabType) => void
25
+ showTabs?: boolean
26
+ }
27
+
28
+ const Layout = ({
29
+ children,
30
+ step,
31
+ tabs,
32
+ currentTab,
33
+ setCurrentTab,
34
+ showTabs,
35
+ }: LayoutProps) => {
36
+ return (
37
+ <div className="min-h-screen min-w-screen bg-zinc-900 flex flex-col items-center justify-center">
38
+ <div className="relative">
39
+ <DesktopTabGroup
40
+ tabs={tabs || []}
41
+ currentTab={currentTab}
42
+ setCurrentTab={setCurrentTab}
43
+ showTabs={showTabs}
44
+ />
45
+ <div className="w-(--card-group-width) layout-card-group">
46
+ <div
47
+ className="h-(--card-group-height) grid grid-flow-col auto-cols-max transition-transform duration-500"
48
+ style={{
49
+ transform: `translateX(calc(-${step} * var(--card-width)))`,
50
+ }}
51
+ >
52
+ {children}
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ export const Main = () => {
61
+ const { isAuthenticated } = useUser()
62
+ const [step, setStep] = useState(0)
63
+
64
+ const tabs: TabType[] = [
65
+ {
66
+ name: 'Home',
67
+ component: (
68
+ <Profile
69
+ sampleGithubUrl="https://github.com/openfort-xyz/openfort-react/tree/main/examples/quickstarts/solana-headless"
70
+ description="This is a demo app using Headless Wallet with Solana and Openfort."
71
+ />
72
+ ),
73
+ icon: HomeIcon,
74
+ },
75
+ {
76
+ name: 'Sign',
77
+ component: <Sign />,
78
+ icon: PencilSquareIcon,
79
+ },
80
+ {
81
+ name: 'Send',
82
+ component: <Send />,
83
+ icon: PaperAirplaneIcon,
84
+ },
85
+ {
86
+ name: 'History',
87
+ component: <History />,
88
+ icon: ClockIcon,
89
+ },
90
+ {
91
+ name: 'Wallets',
92
+ component: <Wallets />,
93
+ icon: WalletIcon,
94
+ },
95
+ ]
96
+ const [currentTab, setCurrentTab] = useState<TabType>(tabs[0])
97
+
98
+ return (
99
+ <Layout
100
+ step={step}
101
+ showTabs={!!isAuthenticated}
102
+ tabs={tabs}
103
+ currentTab={currentTab}
104
+ setCurrentTab={setCurrentTab}
105
+ >
106
+ <Head
107
+ onStart={() => setStep(1)}
108
+ sample="Solana"
109
+ color="rgba(153,69,255,1)"
110
+ logo="/solanaLogo.svg"
111
+ href="https://solana.com/"
112
+ subtitle="Get started with headless Solana wallet integration using Openfort Authentication"
113
+ />
114
+ {!isAuthenticated ? (
115
+ <Auth />
116
+ ) : (
117
+ <div className="block relative overflow-y-auto overflow-x-hidden">
118
+ <div className="card flex-col min-h-full">
119
+ <div className="w-full flex-1 relative">
120
+ {tabs.map((tab) => (
121
+ <div
122
+ key={tab.name}
123
+ className={`w-full flex-1 flex${currentTab.name === tab.name ? '' : ' hidden'}`}
124
+ >
125
+ {tab.component}
126
+ </div>
127
+ ))}
128
+ </div>
129
+ <MobileTabGroup
130
+ tabs={tabs}
131
+ currentTab={currentTab}
132
+ setCurrentTab={setCurrentTab}
133
+ />
134
+ </div>
135
+ </div>
136
+ )}
137
+ <div className="card relative" />
138
+ </Layout>
139
+ )
140
+ }
@@ -0,0 +1,80 @@
1
+ import { BookOpenIcon } from '@heroicons/react/24/outline'
2
+ import { useSignOut, useUser } from '@openfort/react'
3
+ import { useSolanaEmbeddedWallet } from '@openfort/react/solana'
4
+ import { Wallets } from './wallets'
5
+
6
+ export const Profile = ({
7
+ sampleGithubUrl,
8
+ description,
9
+ }: {
10
+ sampleGithubUrl: string
11
+ description: string
12
+ }) => {
13
+ const { user } = useUser()
14
+ const isLocal = window.location.hostname === 'localhost'
15
+ const { signOut } = useSignOut()
16
+ const { status } = useSolanaEmbeddedWallet()
17
+
18
+ if (status !== 'connected' && status !== 'connecting') {
19
+ return <Wallets />
20
+ }
21
+
22
+ return (
23
+ <div className="flex flex-col flex-1 gap-4">
24
+ <h1 className="truncate">Welcome, {user?.name || user?.email}</h1>
25
+ <p className="text-zinc-400 text-sm">
26
+ {description}
27
+ <br />
28
+ You can sign messages and send SOL transactions.
29
+ </p>
30
+ <div className="border border-zinc-700 rounded p-4">
31
+ <h2 className="mb-2">Get started</h2>
32
+ <p className="mb-2 text-zinc-400 text-sm">
33
+ Start by creating a wallet, signing messages and sending SOL.
34
+ </p>
35
+ {isLocal ? (
36
+ <p className="mb-2 text-sm">
37
+ Edit <code>src/components/main.tsx</code> to customize the app.
38
+ </p>
39
+ ) : (
40
+ <p className="mb-2 text-sm">
41
+ Clone this project and test it yourself, it is open source!
42
+ </p>
43
+ )}
44
+ <div className="flex gap-4 mt-4">
45
+ <a
46
+ href={sampleGithubUrl}
47
+ className="btn bg-inherit border border-zinc-600 hover:border-zinc-400 text-zinc-400 hover:text-white"
48
+ target="_blank"
49
+ rel="noreferrer"
50
+ >
51
+ <img
52
+ src="/githubLogo.svg"
53
+ className="w-5 h-5 mr-2"
54
+ alt="GitHub logo"
55
+ />
56
+ View on github
57
+ </a>
58
+ <a
59
+ href="https://www.openfort.io/docs/products/embedded-wallet/react"
60
+ target="_blank"
61
+ rel="noreferrer"
62
+ className="btn"
63
+ >
64
+ <BookOpenIcon className="h-5 w-5 mr-2" />
65
+ View docs
66
+ </a>
67
+ </div>
68
+ </div>
69
+
70
+ <button
71
+ onClick={() => {
72
+ signOut()
73
+ }}
74
+ className="btn mt-auto"
75
+ >
76
+ Sign Out
77
+ </button>
78
+ </div>
79
+ )
80
+ }