@jerydam/lumina-sdk 0.1.0
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/BUTTON_FIXES.md +59 -0
- package/DOCS_INDEX.md +332 -0
- package/DOCUMENTATION.md +252 -0
- package/DOCUMENTATION_BUILD_SUMMARY.md +376 -0
- package/DOCUMENTATION_COMPLETE.md +311 -0
- package/FEATURES.md +333 -0
- package/Lumina-sdk/src/components/lumina-provider.tsx +46 -0
- package/Lumina-sdk/src/components/transaction-confirm.tsx +242 -0
- package/Lumina-sdk/src/components/wallet-display.tsx +157 -0
- package/Lumina-sdk/src/components/wallet-login.tsx +163 -0
- package/Lumina-sdk/src/hooks/use-mobile.ts +19 -0
- package/Lumina-sdk/src/hooks/use-toast.ts +191 -0
- package/Lumina-sdk/src/index.ts +0 -0
- package/Lumina-sdk/src/lib/api.ts +66 -0
- package/Lumina-sdk/src/lib/utils.ts +6 -0
- package/Lumina-sdk/src/package.json +42 -0
- package/Lumina-sdk/src/tsconfig.json +19 -0
- package/NEW_FILES_MANIFEST.txt +146 -0
- package/README.md +298 -0
- package/app/dashboard/analytics/page.tsx +218 -0
- package/app/dashboard/api-keys/page.tsx +260 -0
- package/app/dashboard/billing/page.tsx +412 -0
- package/app/dashboard/integration/page.tsx +185 -0
- package/app/dashboard/layout.tsx +18 -0
- package/app/dashboard/page.tsx +244 -0
- package/app/dashboard/settings/page.tsx +285 -0
- package/app/dashboard/users/page.tsx +148 -0
- package/app/docs/api/authentication/page.tsx +246 -0
- package/app/docs/api/endpoints/page.tsx +397 -0
- package/app/docs/api/errors/page.tsx +305 -0
- package/app/docs/api/overview/page.tsx +306 -0
- package/app/docs/examples/basic-setup/page.tsx +256 -0
- package/app/docs/examples/multi-chain/page.tsx +331 -0
- package/app/docs/examples/nextjs-full-stack/page.tsx +332 -0
- package/app/docs/getting-started/environment-setup/page.tsx +243 -0
- package/app/docs/getting-started/installation/page.tsx +187 -0
- package/app/docs/getting-started/introduction/page.tsx +178 -0
- package/app/docs/getting-started/quick-start/page.tsx +199 -0
- package/app/docs/guides/nextjs/page.tsx +358 -0
- package/app/docs/guides/react/page.tsx +230 -0
- package/app/docs/guides/security/page.tsx +284 -0
- package/app/docs/layout.tsx +32 -0
- package/app/docs/page.tsx +180 -0
- package/app/docs/sdk/lumina-provider/page.tsx +186 -0
- package/app/docs/sdk/transaction-confirm/page.tsx +331 -0
- package/app/docs/sdk/wallet-display/page.tsx +224 -0
- package/app/docs/sdk/wallet-login/page.tsx +207 -0
- package/app/docs/troubleshooting/common-issues/page.tsx +301 -0
- package/app/docs/troubleshooting/faq/page.tsx +105 -0
- package/app/globals.css +125 -0
- package/app/invite/[token]/page.tsx +78 -0
- package/app/layout.tsx +36 -0
- package/app/login/page.tsx +175 -0
- package/app/page.tsx +336 -0
- package/app/sdk-demo/page.tsx +239 -0
- package/components/dashboard-sidebar.tsx +113 -0
- package/components/docs/breadcrumb.tsx +51 -0
- package/components/docs/callout.tsx +53 -0
- package/components/docs/code-block.tsx +77 -0
- package/components/docs/docs-sidebar.tsx +214 -0
- package/components/docs/table-of-contents.tsx +83 -0
- package/components/sdk/lumina-provider.tsx +46 -0
- package/components/sdk/transaction-confirm.tsx +242 -0
- package/components/sdk/wallet-display.tsx +157 -0
- package/components/sdk/wallet-login.tsx +163 -0
- package/components/theme-provider.tsx +11 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/calendar.tsx +213 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +351 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +184 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/drawer.tsx +135 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +104 -0
- package/components/ui/field.tsx +244 -0
- package/components/ui/form.tsx +167 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-group.tsx +169 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/item.tsx +193 -0
- package/components/ui/kbd.tsx +28 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +166 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +45 -0
- package/components/ui/resizable.tsx +56 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/select.tsx +185 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +139 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +59 -0
- package/components/ui/sonner.tsx +25 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/toast.tsx +129 -0
- package/components/ui/toaster.tsx +35 -0
- package/components/ui/toggle-group.tsx +73 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/use-mobile.tsx +19 -0
- package/components/ui/use-toast.ts +191 -0
- package/components.json +21 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-toast.ts +191 -0
- package/lib/api.ts +66 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +11 -0
- package/package.json +73 -0
- package/pnpm-workspace.yaml +5 -0
- package/postcss.config.mjs +8 -0
- package/public/apple-icon.png +0 -0
- package/public/fav.jpeg +0 -0
- package/public/fav.png +0 -0
- package/public/icon-dark-32x32.png +0 -0
- package/public/icon-light-32x32.png +0 -0
- package/public/icon.png +0 -0
- package/public/icon.svg +26 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/public/logo2.jpeg +0 -0
- package/public/logo2.png +0 -0
- package/public/placeholder-logo.png +0 -0
- package/public/placeholder-logo.svg +1 -0
- package/public/placeholder-user.jpg +0 -0
- package/public/placeholder.jpg +0 -0
- package/public/placeholder.svg +1 -0
- package/styles/globals.css +209 -0
- package/tailwind.config.ts +15 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Copy, ExternalLink, Send, Download, Loader2 } from 'lucide-react'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { Card } from '@/components/ui/card'
|
|
6
|
+
import { useState, useEffect } from 'react'
|
|
7
|
+
import { useLumina } from './lumina-provider'
|
|
8
|
+
|
|
9
|
+
interface WalletDisplayProps {
|
|
10
|
+
signerAddress: string // We need the signer to fetch details from your backend
|
|
11
|
+
smartAccountAddress: string
|
|
12
|
+
balance?: string
|
|
13
|
+
token?: string
|
|
14
|
+
network?: string
|
|
15
|
+
chainId?: string
|
|
16
|
+
onSend?: () => void
|
|
17
|
+
onReceive?: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function WalletDisplay({
|
|
21
|
+
signerAddress,
|
|
22
|
+
smartAccountAddress,
|
|
23
|
+
balance = '0.00',
|
|
24
|
+
token = 'ETH',
|
|
25
|
+
network = 'Ethereum',
|
|
26
|
+
chainId = '1',
|
|
27
|
+
onSend,
|
|
28
|
+
onReceive,
|
|
29
|
+
}: WalletDisplayProps) {
|
|
30
|
+
const { fetchApi } = useLumina()
|
|
31
|
+
const [copied, setCopied] = useState(false)
|
|
32
|
+
const [isDeployed, setIsDeployed] = useState<boolean | null>(null)
|
|
33
|
+
const [isLoadingState, setIsLoadingState] = useState(true)
|
|
34
|
+
|
|
35
|
+
// Fetch wallet details on mount
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let isMounted = true
|
|
38
|
+
|
|
39
|
+
const syncWalletState = async () => {
|
|
40
|
+
try {
|
|
41
|
+
// 1. Fetch current DB state
|
|
42
|
+
const walletData = await fetchApi(`/v1/wallets/${signerAddress}/${chainId}`)
|
|
43
|
+
|
|
44
|
+
if (!isMounted) return
|
|
45
|
+
|
|
46
|
+
setIsDeployed(walletData.is_deployed)
|
|
47
|
+
|
|
48
|
+
// 2. If the DB thinks it's not deployed, double-check on-chain
|
|
49
|
+
// This is crucial because users might fund their counterfactual address
|
|
50
|
+
if (!walletData.is_deployed) {
|
|
51
|
+
const deployCheck = await fetchApi(`/v1/wallets/${signerAddress}/${chainId}/check-deployment`, {
|
|
52
|
+
method: 'POST'
|
|
53
|
+
})
|
|
54
|
+
if (isMounted) setIsDeployed(deployCheck.is_deployed)
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("Failed to sync wallet state:", error)
|
|
58
|
+
} finally {
|
|
59
|
+
if (isMounted) setIsLoadingState(false)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
syncWalletState()
|
|
64
|
+
return () => { isMounted = false }
|
|
65
|
+
}, [signerAddress, chainId, fetchApi])
|
|
66
|
+
|
|
67
|
+
const handleCopy = () => {
|
|
68
|
+
navigator.clipboard.writeText(smartAccountAddress)
|
|
69
|
+
setCopied(true)
|
|
70
|
+
setTimeout(() => setCopied(false), 2000)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Card className="glassmorphism-dark p-8 border-border w-full max-w-md mx-auto">
|
|
75
|
+
<div className="text-center mb-8">
|
|
76
|
+
<h2 className="text-2xl font-bold mb-2">Your Wallet</h2>
|
|
77
|
+
<div className="flex items-center justify-center gap-2">
|
|
78
|
+
<p className="text-foreground/60 text-sm">{network} Network</p>
|
|
79
|
+
{isLoadingState ? (
|
|
80
|
+
<Loader2 className="w-3 h-3 animate-spin text-foreground/60" />
|
|
81
|
+
) : (
|
|
82
|
+
<span className={`text-[10px] px-2 py-0.5 rounded-full ${isDeployed ? 'bg-emerald-500/20 text-emerald-400' : 'bg-amber-500/20 text-amber-400'}`}>
|
|
83
|
+
{isDeployed ? 'Deployed' : 'Counterfactual'}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Balance Display */}
|
|
90
|
+
<div className="text-center mb-8 p-6 bg-linear-to-b from-emerald-600/20 to-emerald-600/5 rounded-lg border border-emerald-600/30">
|
|
91
|
+
<p className="text-foreground/60 text-sm mb-2">Available Balance</p>
|
|
92
|
+
<p className="text-4xl font-bold text-emerald-400 mb-2">{balance}</p>
|
|
93
|
+
<p className="text-foreground/60">{token}</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Address Display */}
|
|
97
|
+
<div className="mb-6 p-4 bg-white/5 rounded-lg border border-border/50">
|
|
98
|
+
<p className="text-xs text-foreground/60 mb-2">Smart Account Address</p>
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<code className="text-sm font-mono flex-1 truncate">{smartAccountAddress}</code>
|
|
101
|
+
<button
|
|
102
|
+
onClick={handleCopy}
|
|
103
|
+
className="p-2 hover:bg-white/10 rounded-lg transition"
|
|
104
|
+
title="Copy address"
|
|
105
|
+
>
|
|
106
|
+
<Copy size={16} className="text-foreground/60" />
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
{copied && <p className="text-xs text-emerald-500 mt-2">Copied!</p>}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Action Buttons */}
|
|
113
|
+
<div className="grid grid-cols-2 gap-3 mb-6">
|
|
114
|
+
<Button
|
|
115
|
+
onClick={onReceive}
|
|
116
|
+
variant="outline"
|
|
117
|
+
className="gap-2"
|
|
118
|
+
>
|
|
119
|
+
<Download size={18} />
|
|
120
|
+
Receive
|
|
121
|
+
</Button>
|
|
122
|
+
<Button
|
|
123
|
+
onClick={onSend}
|
|
124
|
+
className="bg-emerald-600 hover:bg-emerald-700 gap-2"
|
|
125
|
+
>
|
|
126
|
+
<Send size={18} />
|
|
127
|
+
Send
|
|
128
|
+
</Button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Network Info */}
|
|
132
|
+
<div className="p-4 bg-white/5 rounded-lg border border-border/50 space-y-2">
|
|
133
|
+
<div className="flex justify-between text-sm">
|
|
134
|
+
<span className="text-foreground/60">Network</span>
|
|
135
|
+
<span className="font-medium">{network}</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex justify-between text-sm border-t border-border/30 pt-2">
|
|
138
|
+
<span className="text-foreground/60">Network Status</span>
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
|
|
141
|
+
<span className="font-medium">Operational</span>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* View on Explorer */}
|
|
147
|
+
<Button
|
|
148
|
+
variant="outline"
|
|
149
|
+
className="w-full mt-6 gap-2"
|
|
150
|
+
onClick={() => window.open(`https://etherscan.io/address/${smartAccountAddress}`, '_blank')}
|
|
151
|
+
>
|
|
152
|
+
<ExternalLink size={18} />
|
|
153
|
+
View on Explorer
|
|
154
|
+
</Button>
|
|
155
|
+
</Card>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { Loader2, Lock, Mail } from 'lucide-react'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Input } from '@/components/ui/input'
|
|
8
|
+
import { Card } from '@/components/ui/card'
|
|
9
|
+
import { apiClient } from '@/lib/api' // Our centralized API client
|
|
10
|
+
export interface WalletLoginProps {
|
|
11
|
+
authMethods?: string[];
|
|
12
|
+
onSuccess?: (address: string) => void;
|
|
13
|
+
onError?: (err: Error) => void;
|
|
14
|
+
}
|
|
15
|
+
export default function WalletLogin({ authMethods, onSuccess, onError }: WalletLoginProps) { const router = useRouter()
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
17
|
+
const [isRegistering, setIsRegistering] = useState(false)
|
|
18
|
+
const [error, setError] = useState('')
|
|
19
|
+
|
|
20
|
+
const [formData, setFormData] = useState({
|
|
21
|
+
name: '',
|
|
22
|
+
email: '',
|
|
23
|
+
password: ''
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
27
|
+
e.preventDefault()
|
|
28
|
+
setIsLoading(true)
|
|
29
|
+
setError('')
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (isRegistering) {
|
|
33
|
+
// 1. Register a new developer
|
|
34
|
+
await apiClient.dashboard('/v1/developers/register', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
name: formData.name,
|
|
38
|
+
email: formData.email,
|
|
39
|
+
password: formData.password
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
// Auto-login after registration
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Log in to get the JWT access token
|
|
46
|
+
const data = await apiClient.dashboard('/v1/developers/login', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
email: formData.email,
|
|
50
|
+
password: formData.password
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// 3. Securely store the JWT for future dashboard API calls
|
|
55
|
+
if (typeof window !== 'undefined') {
|
|
56
|
+
localStorage.setItem('lumina_dev_token', data.access_token)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Redirect to the main dashboard
|
|
60
|
+
router.push('/dashboard')
|
|
61
|
+
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
setError(err.message || 'Authentication failed. Please check your credentials.')
|
|
64
|
+
} finally {
|
|
65
|
+
setIsLoading(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
|
|
71
|
+
<Card className="glassmorphism-dark p-8 border-border w-full max-w-md">
|
|
72
|
+
<div className="mb-8 text-center">
|
|
73
|
+
<h1 className="text-3xl font-bold mb-2">Lumina Portal</h1>
|
|
74
|
+
<p className="text-foreground/60 text-sm">
|
|
75
|
+
{isRegistering ? 'Create your developer account' : 'Sign in to manage your workspace'}
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{error && (
|
|
80
|
+
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/50 rounded-lg text-red-400 text-sm text-center">
|
|
81
|
+
{error}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
86
|
+
{isRegistering && (
|
|
87
|
+
<div>
|
|
88
|
+
<label className="block text-sm font-medium mb-2">Full Name</label>
|
|
89
|
+
<Input
|
|
90
|
+
type="text"
|
|
91
|
+
placeholder="Jane Doe"
|
|
92
|
+
value={formData.name}
|
|
93
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
94
|
+
required={isRegistering}
|
|
95
|
+
className="bg-white/5 border-border"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<div>
|
|
101
|
+
<label className="block text-sm font-medium mb-2">Email Address</label>
|
|
102
|
+
<div className="relative">
|
|
103
|
+
<Mail className="absolute left-3 top-2.5 text-foreground/40" size={18} />
|
|
104
|
+
<Input
|
|
105
|
+
type="email"
|
|
106
|
+
placeholder="developer@company.com"
|
|
107
|
+
value={formData.email}
|
|
108
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
109
|
+
required
|
|
110
|
+
className="pl-10 bg-white/5 border-border"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div>
|
|
116
|
+
<label className="block text-sm font-medium mb-2">Password</label>
|
|
117
|
+
<div className="relative">
|
|
118
|
+
<Lock className="absolute left-3 top-2.5 text-foreground/40" size={18} />
|
|
119
|
+
<Input
|
|
120
|
+
type="password"
|
|
121
|
+
placeholder="••••••••••••"
|
|
122
|
+
value={formData.password}
|
|
123
|
+
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
124
|
+
required
|
|
125
|
+
minLength={12}
|
|
126
|
+
className="pl-10 bg-white/5 border-border"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
{isRegistering && (
|
|
130
|
+
<p className="text-xs text-foreground/50 mt-2">Must be at least 12 characters long.</p>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<Button
|
|
135
|
+
type="submit"
|
|
136
|
+
disabled={isLoading}
|
|
137
|
+
className="w-full bg-emerald-600 hover:bg-emerald-700 mt-6"
|
|
138
|
+
>
|
|
139
|
+
{isLoading ? (
|
|
140
|
+
<Loader2 className="animate-spin" size={18} />
|
|
141
|
+
) : (
|
|
142
|
+
isRegistering ? 'Create Account' : 'Sign In'
|
|
143
|
+
)}
|
|
144
|
+
</Button>
|
|
145
|
+
</form>
|
|
146
|
+
|
|
147
|
+
<div className="mt-6 text-center text-sm text-foreground/60">
|
|
148
|
+
{isRegistering ? 'Already have an account?' : "Don't have an account?"}{' '}
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => {
|
|
152
|
+
setIsRegistering(!isRegistering)
|
|
153
|
+
setError('')
|
|
154
|
+
}}
|
|
155
|
+
className="text-emerald-400 hover:text-emerald-300 font-medium"
|
|
156
|
+
>
|
|
157
|
+
{isRegistering ? 'Sign In' : 'Register'}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</Card>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener('change', onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener('change', onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Inspired by react-hot-toast library
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
|
|
6
|
+
import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
|
|
7
|
+
|
|
8
|
+
const TOAST_LIMIT = 1
|
|
9
|
+
const TOAST_REMOVE_DELAY = 1000000
|
|
10
|
+
|
|
11
|
+
type ToasterToast = ToastProps & {
|
|
12
|
+
id: string
|
|
13
|
+
title?: React.ReactNode
|
|
14
|
+
description?: React.ReactNode
|
|
15
|
+
action?: ToastActionElement
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const actionTypes = {
|
|
19
|
+
ADD_TOAST: 'ADD_TOAST',
|
|
20
|
+
UPDATE_TOAST: 'UPDATE_TOAST',
|
|
21
|
+
DISMISS_TOAST: 'DISMISS_TOAST',
|
|
22
|
+
REMOVE_TOAST: 'REMOVE_TOAST',
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
let count = 0
|
|
26
|
+
|
|
27
|
+
function genId() {
|
|
28
|
+
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
|
29
|
+
return count.toString()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ActionType = typeof actionTypes
|
|
33
|
+
|
|
34
|
+
type Action =
|
|
35
|
+
| {
|
|
36
|
+
type: ActionType['ADD_TOAST']
|
|
37
|
+
toast: ToasterToast
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
type: ActionType['UPDATE_TOAST']
|
|
41
|
+
toast: Partial<ToasterToast>
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: ActionType['DISMISS_TOAST']
|
|
45
|
+
toastId?: ToasterToast['id']
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
type: ActionType['REMOVE_TOAST']
|
|
49
|
+
toastId?: ToasterToast['id']
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface State {
|
|
53
|
+
toasts: ToasterToast[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
57
|
+
|
|
58
|
+
const addToRemoveQueue = (toastId: string) => {
|
|
59
|
+
if (toastTimeouts.has(toastId)) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const timeout = setTimeout(() => {
|
|
64
|
+
toastTimeouts.delete(toastId)
|
|
65
|
+
dispatch({
|
|
66
|
+
type: 'REMOVE_TOAST',
|
|
67
|
+
toastId: toastId,
|
|
68
|
+
})
|
|
69
|
+
}, TOAST_REMOVE_DELAY)
|
|
70
|
+
|
|
71
|
+
toastTimeouts.set(toastId, timeout)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const reducer = (state: State, action: Action): State => {
|
|
75
|
+
switch (action.type) {
|
|
76
|
+
case 'ADD_TOAST':
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'UPDATE_TOAST':
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
toasts: state.toasts.map((t) =>
|
|
86
|
+
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'DISMISS_TOAST': {
|
|
91
|
+
const { toastId } = action
|
|
92
|
+
|
|
93
|
+
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
94
|
+
// but I'll keep it here for simplicity
|
|
95
|
+
if (toastId) {
|
|
96
|
+
addToRemoveQueue(toastId)
|
|
97
|
+
} else {
|
|
98
|
+
state.toasts.forEach((toast) => {
|
|
99
|
+
addToRemoveQueue(toast.id)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
toasts: state.toasts.map((t) =>
|
|
106
|
+
t.id === toastId || toastId === undefined
|
|
107
|
+
? {
|
|
108
|
+
...t,
|
|
109
|
+
open: false,
|
|
110
|
+
}
|
|
111
|
+
: t,
|
|
112
|
+
),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
case 'REMOVE_TOAST':
|
|
116
|
+
if (action.toastId === undefined) {
|
|
117
|
+
return {
|
|
118
|
+
...state,
|
|
119
|
+
toasts: [],
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...state,
|
|
124
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const listeners: Array<(state: State) => void> = []
|
|
130
|
+
|
|
131
|
+
let memoryState: State = { toasts: [] }
|
|
132
|
+
|
|
133
|
+
function dispatch(action: Action) {
|
|
134
|
+
memoryState = reducer(memoryState, action)
|
|
135
|
+
listeners.forEach((listener) => {
|
|
136
|
+
listener(memoryState)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type Toast = Omit<ToasterToast, 'id'>
|
|
141
|
+
|
|
142
|
+
function toast({ ...props }: Toast) {
|
|
143
|
+
const id = genId()
|
|
144
|
+
|
|
145
|
+
const update = (props: ToasterToast) =>
|
|
146
|
+
dispatch({
|
|
147
|
+
type: 'UPDATE_TOAST',
|
|
148
|
+
toast: { ...props, id },
|
|
149
|
+
})
|
|
150
|
+
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
|
|
151
|
+
|
|
152
|
+
dispatch({
|
|
153
|
+
type: 'ADD_TOAST',
|
|
154
|
+
toast: {
|
|
155
|
+
...props,
|
|
156
|
+
id,
|
|
157
|
+
open: true,
|
|
158
|
+
onOpenChange: (open) => {
|
|
159
|
+
if (!open) dismiss()
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
id: id,
|
|
166
|
+
dismiss,
|
|
167
|
+
update,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function useToast() {
|
|
172
|
+
const [state, setState] = React.useState<State>(memoryState)
|
|
173
|
+
|
|
174
|
+
React.useEffect(() => {
|
|
175
|
+
listeners.push(setState)
|
|
176
|
+
return () => {
|
|
177
|
+
const index = listeners.indexOf(setState)
|
|
178
|
+
if (index > -1) {
|
|
179
|
+
listeners.splice(index, 1)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}, [state])
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
...state,
|
|
186
|
+
toast,
|
|
187
|
+
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { useToast, toast }
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// lib/api.ts
|
|
2
|
+
|
|
3
|
+
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://lumina-z3v8.onrender.com';
|
|
4
|
+
|
|
5
|
+
export const apiClient = {
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// SDK Client (Uses X-API-Key)
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
sdk: async (endpoint: string, apiKey: string, options: RequestInit = {}) => {
|
|
10
|
+
if (!apiKey) throw new Error("API Key is required for SDK calls");
|
|
11
|
+
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'X-API-Key': apiKey,
|
|
15
|
+
...options.headers,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers });
|
|
19
|
+
|
|
20
|
+
// Attempt to parse JSON safely, even on error
|
|
21
|
+
let data;
|
|
22
|
+
try {
|
|
23
|
+
data = await response.json();
|
|
24
|
+
} catch {
|
|
25
|
+
data = { detail: response.statusText };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(data.detail || `SDK API Error: ${response.status}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return data;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Dashboard Client (Uses Bearer JWT)
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
dashboard: async (endpoint: string, options: RequestInit = {}) => {
|
|
39
|
+
// Note: Assuming you store the dev token in localStorage upon login
|
|
40
|
+
const token = typeof window !== 'undefined' ? localStorage.getItem('lumina_dev_token') : null;
|
|
41
|
+
|
|
42
|
+
const headers: Record<string, string> = {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
...options.headers as Record<string, string>,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (token) {
|
|
48
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers });
|
|
52
|
+
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = await response.json();
|
|
56
|
+
} catch {
|
|
57
|
+
data = { detail: response.statusText };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(data.detail || `Dashboard API Error: ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lumina/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lumina SDK - Web3 wallet auth and identity verification",
|
|
5
|
+
"private": false,
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md"],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --external react --external react-dom",
|
|
19
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
20
|
+
"prepublishOnly": "pnpm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["web3", "wallet", "auth", "kyc", "lumina"],
|
|
23
|
+
"author": "Oyeniran Jeremiah Damilare <jerydan148@gmail.com>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=18",
|
|
27
|
+
"react-dom": ">=18",
|
|
28
|
+
"next": ">=13"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5.7.3",
|
|
33
|
+
"@types/react": "^19",
|
|
34
|
+
"@types/react-dom": "^19"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"clsx": "^2.1.1",
|
|
38
|
+
"tailwind-merge": "^3.3.1",
|
|
39
|
+
"viem": "^2.48.11",
|
|
40
|
+
"lucide-react": "^0.564.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["./src/*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|