@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,260 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Plus, Copy, Trash2, Lock, Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react'
|
|
4
|
+
import { Card } from '@/components/ui/card'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { useState, useEffect } from 'react'
|
|
8
|
+
import { apiClient } from '@/lib/api'
|
|
9
|
+
|
|
10
|
+
interface ApiKey {
|
|
11
|
+
id: string
|
|
12
|
+
label: string
|
|
13
|
+
is_active: boolean
|
|
14
|
+
created_at: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function APIKeysPage() {
|
|
18
|
+
const [keys, setKeys] = useState<ApiKey[]>([])
|
|
19
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
20
|
+
const [isDeleting, setIsDeleting] = useState<string | null>(null)
|
|
21
|
+
|
|
22
|
+
// Modal States
|
|
23
|
+
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
24
|
+
const [newKeyLabel, setNewKeyLabel] = useState('')
|
|
25
|
+
const [isCreating, setIsCreating] = useState(false)
|
|
26
|
+
|
|
27
|
+
// New Key Display State (The "Show Once" Modal)
|
|
28
|
+
const [newlyCreatedKey, setNewlyCreatedKey] = useState<string | null>(null)
|
|
29
|
+
const [copied, setCopied] = useState(false)
|
|
30
|
+
|
|
31
|
+
// Fetch keys on component mount
|
|
32
|
+
const fetchKeys = async () => {
|
|
33
|
+
try {
|
|
34
|
+
setIsLoading(true)
|
|
35
|
+
const data = await apiClient.dashboard('/v1/developers/keys')
|
|
36
|
+
setKeys(data.keys || [])
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to fetch keys:', error)
|
|
39
|
+
} finally {
|
|
40
|
+
setIsLoading(false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
fetchKeys()
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const handleCreateKey = async (e: React.FormEvent) => {
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
if (!newKeyLabel.trim()) return
|
|
51
|
+
|
|
52
|
+
setIsCreating(true)
|
|
53
|
+
try {
|
|
54
|
+
const data = await apiClient.dashboard('/v1/developers/keys', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
body: JSON.stringify({ label: newKeyLabel })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Show the raw key immediately, close the creation modal
|
|
60
|
+
setNewlyCreatedKey(data.raw_key)
|
|
61
|
+
setShowCreateModal(false)
|
|
62
|
+
setNewKeyLabel('')
|
|
63
|
+
|
|
64
|
+
// Refresh the background list
|
|
65
|
+
fetchKeys()
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Failed to create key:', error)
|
|
68
|
+
} finally {
|
|
69
|
+
setIsCreating(false)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleDeleteKey = async (keyId: string) => {
|
|
74
|
+
if (!confirm('Are you sure you want to revoke this API key? This action cannot be undone and will break any apps using it.')) return
|
|
75
|
+
|
|
76
|
+
setIsDeleting(keyId)
|
|
77
|
+
try {
|
|
78
|
+
await apiClient.dashboard(`/v1/developers/keys/${keyId}`, {
|
|
79
|
+
method: 'DELETE'
|
|
80
|
+
})
|
|
81
|
+
// Update local state to remove the deleted key instantly
|
|
82
|
+
setKeys(keys.filter(k => k.id !== keyId))
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Failed to delete key:', error)
|
|
85
|
+
} finally {
|
|
86
|
+
setIsDeleting(null)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const copyToClipboard = (text: string) => {
|
|
91
|
+
navigator.clipboard.writeText(text)
|
|
92
|
+
setCopied(true)
|
|
93
|
+
setTimeout(() => setCopied(false), 2000)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="space-y-8 relative">
|
|
98
|
+
{/* Header */}
|
|
99
|
+
<div className="flex justify-between items-start">
|
|
100
|
+
<div>
|
|
101
|
+
<h1 className="text-4xl font-bold mb-2">API Keys</h1>
|
|
102
|
+
<p className="text-foreground/60">Manage API keys for secure SDK integration with Lumina</p>
|
|
103
|
+
</div>
|
|
104
|
+
<Button
|
|
105
|
+
onClick={() => setShowCreateModal(true)}
|
|
106
|
+
className="bg-emerald-600 hover:bg-emerald-700 gap-2"
|
|
107
|
+
>
|
|
108
|
+
<Plus size={18} />
|
|
109
|
+
Create Key
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Info Card */}
|
|
114
|
+
<Card className="glassmorphism-dark p-6 border-emerald-600/30 bg-emerald-600/5">
|
|
115
|
+
<div className="flex gap-4">
|
|
116
|
+
<Lock className="text-emerald-500 shrink-0 mt-1" size={24} />
|
|
117
|
+
<div>
|
|
118
|
+
<h3 className="font-semibold mb-1">Keep your API keys secure</h3>
|
|
119
|
+
<p className="text-foreground/70 text-sm">
|
|
120
|
+
Your API keys grant full access to your developer balance and smart account deployments.
|
|
121
|
+
Never commit them to GitHub or expose them in client-side code (except via the secure Lumina SDK).
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</Card>
|
|
126
|
+
|
|
127
|
+
{/* API Keys List */}
|
|
128
|
+
<div className="space-y-4">
|
|
129
|
+
{isLoading ? (
|
|
130
|
+
<div className="flex justify-center p-12">
|
|
131
|
+
<Loader2 className="w-8 h-8 animate-spin text-emerald-500" />
|
|
132
|
+
</div>
|
|
133
|
+
) : keys.length === 0 ? (
|
|
134
|
+
<Card className="glassmorphism-dark p-12 text-center border-border border-dashed">
|
|
135
|
+
<p className="text-foreground/60">No API keys found. Create one to get started.</p>
|
|
136
|
+
</Card>
|
|
137
|
+
) : (
|
|
138
|
+
keys.map((apiKey) => (
|
|
139
|
+
<Card key={apiKey.id} className="glassmorphism-dark p-6 border-border">
|
|
140
|
+
<div className="flex items-start justify-between mb-4">
|
|
141
|
+
<div>
|
|
142
|
+
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
143
|
+
{apiKey.label}
|
|
144
|
+
{!apiKey.is_active && (
|
|
145
|
+
<span className="text-xs px-2 py-0.5 rounded bg-red-500/20 text-red-400">Revoked</span>
|
|
146
|
+
)}
|
|
147
|
+
</h3>
|
|
148
|
+
<p className="text-sm text-foreground/60 mt-1">
|
|
149
|
+
Created on {new Date(apiKey.created_at).toLocaleDateString()}
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
<Button
|
|
153
|
+
onClick={() => handleDeleteKey(apiKey.id)}
|
|
154
|
+
variant="outline"
|
|
155
|
+
disabled={!apiKey.is_active || isDeleting === apiKey.id}
|
|
156
|
+
className="text-red-500 hover:bg-red-600/10 hover:border-red-600/50 gap-2 transition"
|
|
157
|
+
>
|
|
158
|
+
{isDeleting === apiKey.id ? <Loader2 className="animate-spin" size={16} /> : <Trash2 size={16} />}
|
|
159
|
+
Revoke Key
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div>
|
|
164
|
+
<label className="text-xs font-medium text-foreground/60 mb-2 block">API Key</label>
|
|
165
|
+
<div className="flex gap-2">
|
|
166
|
+
<code className="flex-1 bg-white/5 p-3 rounded-lg text-sm font-mono text-foreground/40 select-none">
|
|
167
|
+
pk_••••••••••••••••••••••••••••••••
|
|
168
|
+
</code>
|
|
169
|
+
<div className="flex items-center px-4 text-xs text-amber-500/80 bg-amber-500/10 rounded-lg">
|
|
170
|
+
Hidden for security
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</Card>
|
|
175
|
+
))
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
MODALS (Usually handled by Radix/Shadcn, built inline here for portability)
|
|
181
|
+
───────────────────────────────────────────────────────────────────────────── */}
|
|
182
|
+
|
|
183
|
+
{/* Create Key Modal */}
|
|
184
|
+
{showCreateModal && (
|
|
185
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
|
|
186
|
+
<Card className="glassmorphism-dark w-full max-w-md p-6 border-border shadow-2xl">
|
|
187
|
+
<h2 className="text-xl font-bold mb-2">Create new API Key</h2>
|
|
188
|
+
<p className="text-sm text-foreground/60 mb-6">Give your key a label so you can identify it later.</p>
|
|
189
|
+
|
|
190
|
+
<form onSubmit={handleCreateKey}>
|
|
191
|
+
<div className="mb-6">
|
|
192
|
+
<label className="block text-sm font-medium mb-2">Key Label</label>
|
|
193
|
+
<Input
|
|
194
|
+
autoFocus
|
|
195
|
+
placeholder="e.g., Production Environment"
|
|
196
|
+
value={newKeyLabel}
|
|
197
|
+
onChange={(e) => setNewKeyLabel(e.target.value)}
|
|
198
|
+
className="bg-white/5 border-border"
|
|
199
|
+
required
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="flex justify-end gap-3">
|
|
203
|
+
<Button type="button" variant="outline" onClick={() => setShowCreateModal(false)}>
|
|
204
|
+
Cancel
|
|
205
|
+
</Button>
|
|
206
|
+
<Button type="submit" disabled={isCreating || !newKeyLabel.trim()} className="bg-emerald-600 hover:bg-emerald-700">
|
|
207
|
+
{isCreating ? <Loader2 className="animate-spin mr-2" size={16} /> : null}
|
|
208
|
+
Generate Key
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
</form>
|
|
212
|
+
</Card>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{/* Show Once Success Modal */}
|
|
217
|
+
{newlyCreatedKey && (
|
|
218
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/90 backdrop-blur-md p-4">
|
|
219
|
+
<Card className="glassmorphism-dark w-full max-w-lg p-8 border-emerald-600/50 shadow-2xl">
|
|
220
|
+
<div className="flex items-center gap-3 mb-4 text-emerald-400">
|
|
221
|
+
<CheckCircle2 size={32} />
|
|
222
|
+
<h2 className="text-2xl font-bold text-foreground">API Key Generated</h2>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg flex gap-3 mb-6">
|
|
226
|
+
<AlertTriangle className="text-amber-500 shrink-0 mt-0.5" size={20} />
|
|
227
|
+
<div className="text-sm">
|
|
228
|
+
<p className="font-semibold text-amber-500 mb-1">Please copy this key now!</p>
|
|
229
|
+
<p className="text-foreground/80">
|
|
230
|
+
For your security, we only store the hash of this key. <strong>You will not be able to see this key again</strong> once you close this window.
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="flex gap-2 mb-8">
|
|
236
|
+
<code className="flex-1 bg-white/5 border border-emerald-500/30 p-4 rounded-lg text-sm font-mono break-all text-emerald-400">
|
|
237
|
+
{newlyCreatedKey}
|
|
238
|
+
</code>
|
|
239
|
+
<Button
|
|
240
|
+
onClick={() => copyToClipboard(newlyCreatedKey)}
|
|
241
|
+
className="h-auto px-6 bg-emerald-600 hover:bg-emerald-700 gap-2"
|
|
242
|
+
>
|
|
243
|
+
{copied ? <CheckCircle2 size={18} /> : <Copy size={18} />}
|
|
244
|
+
{copied ? 'Copied' : 'Copy'}
|
|
245
|
+
</Button>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<Button
|
|
249
|
+
onClick={() => setNewlyCreatedKey(null)}
|
|
250
|
+
variant="outline"
|
|
251
|
+
className="w-full"
|
|
252
|
+
>
|
|
253
|
+
I have saved my API key securely
|
|
254
|
+
</Button>
|
|
255
|
+
</Card>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Card } from '@/components/ui/card'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { Input } from '@/components/ui/input'
|
|
6
|
+
import {
|
|
7
|
+
TrendingUp, Zap, Loader2, CheckCircle2, AlertCircle,
|
|
8
|
+
Wallet, Copy, ExternalLink, RefreshCw, Plus, Trash2
|
|
9
|
+
} from 'lucide-react'
|
|
10
|
+
import { useState, useEffect } from 'react'
|
|
11
|
+
import { apiClient } from '@/lib/api'
|
|
12
|
+
|
|
13
|
+
// Old Interfaces
|
|
14
|
+
interface DeveloperMe {
|
|
15
|
+
plan: string
|
|
16
|
+
monthly_ops: number | null
|
|
17
|
+
gas_markup_bps: number
|
|
18
|
+
}
|
|
19
|
+
interface DeveloperStats {
|
|
20
|
+
ops_this_month: number
|
|
21
|
+
monthly_limit: number | null
|
|
22
|
+
}
|
|
23
|
+
interface GasBalance {
|
|
24
|
+
chain_id: string
|
|
25
|
+
balance_wei: number
|
|
26
|
+
reserved_wei: number
|
|
27
|
+
available_wei: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// New Crypto Billing Interfaces
|
|
31
|
+
interface PaymentWallet {
|
|
32
|
+
wallet_address: string
|
|
33
|
+
chain_id: string | null
|
|
34
|
+
label: string | null
|
|
35
|
+
created_at: string
|
|
36
|
+
}
|
|
37
|
+
interface TreasuryInfo {
|
|
38
|
+
treasury_address: string
|
|
39
|
+
accepted_tokens: Record<string, { symbol: string, address: string, decimals: number }[]>
|
|
40
|
+
instructions: string
|
|
41
|
+
}
|
|
42
|
+
interface SubscriptionInfo {
|
|
43
|
+
current_plan: string
|
|
44
|
+
active_subscription: any
|
|
45
|
+
renew_instructions: any
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function BillingPage() {
|
|
49
|
+
const [devData, setDevData] = useState<DeveloperMe | null>(null)
|
|
50
|
+
const [stats, setStats] = useState<DeveloperStats | null>(null)
|
|
51
|
+
const [gasBalance, setGasBalance] = useState<GasBalance | null>(null)
|
|
52
|
+
|
|
53
|
+
// New Crypto Billing State
|
|
54
|
+
const [treasury, setTreasury] = useState<TreasuryInfo | null>(null)
|
|
55
|
+
const [subInfo, setSubInfo] = useState<SubscriptionInfo | null>(null)
|
|
56
|
+
const [wallets, setWallets] = useState<PaymentWallet[]>([])
|
|
57
|
+
|
|
58
|
+
// UI State
|
|
59
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
60
|
+
const [error, setError] = useState('')
|
|
61
|
+
const [successMsg, setSuccessMsg] = useState('')
|
|
62
|
+
const [copied, setCopied] = useState(false)
|
|
63
|
+
|
|
64
|
+
// Forms State
|
|
65
|
+
const [newWallet, setNewWallet] = useState('')
|
|
66
|
+
const [isRegisteringWallet, setIsRegisteringWallet] = useState(false)
|
|
67
|
+
const [txHash, setTxHash] = useState('')
|
|
68
|
+
const [verifyChain, setVerifyChain] = useState('8453') // Default Base
|
|
69
|
+
const [isVerifying, setIsVerifying] = useState(false)
|
|
70
|
+
|
|
71
|
+
const defaultChainId = '8453'
|
|
72
|
+
|
|
73
|
+
const fetchBillingData = async () => {
|
|
74
|
+
try {
|
|
75
|
+
setIsLoading(true)
|
|
76
|
+
const [meRes, statsRes, balanceRes, treasuryRes, subRes, walletsRes] = await Promise.all([
|
|
77
|
+
apiClient.dashboard('/v1/developers/me'),
|
|
78
|
+
apiClient.dashboard('/v1/developers/stats'),
|
|
79
|
+
apiClient.dashboard(`/v1/developers/balance/${defaultChainId}`),
|
|
80
|
+
apiClient.dashboard('/v1/billing/deposit-address'),
|
|
81
|
+
apiClient.dashboard('/v1/billing/subscription'),
|
|
82
|
+
apiClient.dashboard('/v1/billing/wallets')
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
setDevData(meRes)
|
|
86
|
+
setStats(statsRes)
|
|
87
|
+
setGasBalance(balanceRes)
|
|
88
|
+
setTreasury(treasuryRes)
|
|
89
|
+
setSubInfo(subRes)
|
|
90
|
+
setWallets(walletsRes.wallets || [])
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
console.error('Failed to fetch billing data:', err)
|
|
93
|
+
setError('Failed to load billing information.')
|
|
94
|
+
} finally {
|
|
95
|
+
setIsLoading(false)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
fetchBillingData()
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
// ── Crypto Billing Actions ──────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
// 1. Update the submission handler
|
|
106
|
+
const handleRegisterWallet = async (e: React.FormEvent) => {
|
|
107
|
+
e.preventDefault()
|
|
108
|
+
setError('')
|
|
109
|
+
setSuccessMsg('')
|
|
110
|
+
|
|
111
|
+
// Check if they have a Web3 wallet installed in their browser
|
|
112
|
+
if (typeof window === 'undefined' || !(window as any).ethereum) {
|
|
113
|
+
setError('Please install a Web3 wallet (like MetaMask) to verify ownership.')
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setIsRegisteringWallet(true)
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const ethereum = (window as any).ethereum
|
|
121
|
+
|
|
122
|
+
// Step 1: Request account connection
|
|
123
|
+
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
|
|
124
|
+
const address = accounts[0]
|
|
125
|
+
|
|
126
|
+
// Step 2: Prepare the verification message with a timestamp nonce
|
|
127
|
+
const nonce = Date.now().toString()
|
|
128
|
+
const message = `Registering this wallet for Lumina Billing.\nAddress: ${address}\nNonce: ${nonce}`
|
|
129
|
+
|
|
130
|
+
// Step 3: Request the personal signature from the user
|
|
131
|
+
const signature = await ethereum.request({
|
|
132
|
+
method: 'personal_sign',
|
|
133
|
+
params: [message, address],
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Step 4: Send the proof to your backend
|
|
137
|
+
await apiClient.dashboard('/v1/billing/wallets', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
wallet_address: address,
|
|
141
|
+
message: message,
|
|
142
|
+
signature: signature
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
setSuccessMsg('Payment wallet successfully verified and registered.')
|
|
147
|
+
await fetchBillingData() // Refresh the list of wallets
|
|
148
|
+
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
console.error(err)
|
|
151
|
+
// Handle user rejection (e.g., they clicked "Cancel" in MetaMask)
|
|
152
|
+
if (err.code === 4001) {
|
|
153
|
+
setError('Signature request was rejected.')
|
|
154
|
+
} else {
|
|
155
|
+
setError(err.message || 'Failed to verify and register wallet.')
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
setIsRegisteringWallet(false)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const handleRemoveWallet = async (address: string) => {
|
|
163
|
+
try {
|
|
164
|
+
await apiClient.dashboard(`/v1/billing/wallets/${address}`, { method: 'DELETE' })
|
|
165
|
+
setWallets(wallets.filter(w => w.wallet_address !== address))
|
|
166
|
+
} catch (err: any) {
|
|
167
|
+
setError(err.message || 'Failed to remove wallet.')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const handleVerifyTx = async (e: React.FormEvent) => {
|
|
172
|
+
e.preventDefault()
|
|
173
|
+
if (!txHash) return
|
|
174
|
+
setIsVerifying(true)
|
|
175
|
+
setError('')
|
|
176
|
+
setSuccessMsg('')
|
|
177
|
+
try {
|
|
178
|
+
const res = await apiClient.dashboard('/v1/billing/verify-tx', {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
body: JSON.stringify({ tx_hash: txHash, chain_id: verifyChain })
|
|
181
|
+
})
|
|
182
|
+
if (res.verified) {
|
|
183
|
+
setSuccessMsg(`Transaction verified! Processed ${res.deposits.length} deposit(s).`)
|
|
184
|
+
setTxHash('')
|
|
185
|
+
await fetchBillingData() // Refresh balances and sub
|
|
186
|
+
}
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
setError(err.message || 'Failed to verify transaction.')
|
|
189
|
+
} finally {
|
|
190
|
+
setIsVerifying(false)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handleCopy = (text: string) => {
|
|
195
|
+
navigator.clipboard.writeText(text)
|
|
196
|
+
setCopied(true)
|
|
197
|
+
setTimeout(() => setCopied(false), 2000)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const formatWei = (wei: number | undefined) => {
|
|
201
|
+
if (wei === undefined) return '0.0000'
|
|
202
|
+
return (wei / 1e18).toFixed(4)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (isLoading) {
|
|
206
|
+
return (
|
|
207
|
+
<div className="min-h-[60vh] flex items-center justify-center">
|
|
208
|
+
<Loader2 className="w-8 h-8 animate-spin text-emerald-500" />
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const isPro = devData?.plan === 'pro'
|
|
214
|
+
const isStarter = devData?.plan === 'starter'
|
|
215
|
+
const expiryDate = subInfo?.active_subscription?.expires_at
|
|
216
|
+
? new Date(subInfo.active_subscription.expires_at).toLocaleDateString()
|
|
217
|
+
: null
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className="space-y-8">
|
|
221
|
+
{/* Header */}
|
|
222
|
+
<div>
|
|
223
|
+
<h1 className="text-4xl font-bold mb-2">Billing & Gas Ledger</h1>
|
|
224
|
+
<p className="text-foreground/60">Pay for subscriptions and sponsor gas entirely on-chain.</p>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{error && (
|
|
228
|
+
<div className="p-4 bg-red-500/10 border border-red-500/50 rounded-lg text-red-400 flex items-center gap-3">
|
|
229
|
+
<AlertCircle size={20} />
|
|
230
|
+
{error}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
{successMsg && (
|
|
234
|
+
<div className="p-4 bg-emerald-500/10 border border-emerald-500/50 rounded-lg text-emerald-400 flex items-center gap-3">
|
|
235
|
+
<CheckCircle2 size={20} />
|
|
236
|
+
{successMsg}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{/* Current Plan Overview */}
|
|
241
|
+
<Card className="glassmorphism-dark p-8 border-emerald-600/30 bg-emerald-600/5">
|
|
242
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
243
|
+
<div>
|
|
244
|
+
<h2 className="text-2xl font-bold mb-2 capitalize">{devData?.plan || 'Free'} Plan</h2>
|
|
245
|
+
<p className="text-foreground/60 mb-4">
|
|
246
|
+
Gas Sponsorship Markup: <strong className="text-foreground">{(devData?.gas_markup_bps || 0) / 100}%</strong>
|
|
247
|
+
</p>
|
|
248
|
+
<div className="space-y-2 mb-6">
|
|
249
|
+
<p className="text-sm flex items-center gap-2">
|
|
250
|
+
<CheckCircle2 size={16} className="text-emerald-500" />
|
|
251
|
+
{devData?.monthly_ops === null ? 'Unlimited' : `${(devData?.monthly_ops || 0).toLocaleString()}`} UserOperations / month
|
|
252
|
+
</p>
|
|
253
|
+
<p className="text-sm flex items-center gap-2">
|
|
254
|
+
<CheckCircle2 size={16} className="text-emerald-500" /> Multi-chain Paymaster Access
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{expiryDate && (
|
|
259
|
+
<p className="text-sm text-emerald-400 mt-4">
|
|
260
|
+
Valid until {expiryDate}
|
|
261
|
+
</p>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div className="md:text-right flex flex-col justify-center">
|
|
266
|
+
<div className="inline-block p-4 rounded-lg bg-white/5 border border-border self-end max-w-sm text-left">
|
|
267
|
+
<p className="font-semibold mb-2">How to upgrade/renew</p>
|
|
268
|
+
<p className="text-sm text-foreground/60">
|
|
269
|
+
{subInfo?.renew_instructions?.starter || subInfo?.renew_instructions?.renew}
|
|
270
|
+
</p>
|
|
271
|
+
<Button
|
|
272
|
+
onClick={() => document.getElementById('treasury-section')?.scrollIntoView({ behavior: 'smooth' })}
|
|
273
|
+
variant="outline"
|
|
274
|
+
className="mt-4 w-full border-emerald-600/30 text-emerald-400 hover:bg-emerald-600/10"
|
|
275
|
+
>
|
|
276
|
+
View Treasury Details
|
|
277
|
+
</Button>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</Card>
|
|
282
|
+
|
|
283
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
284
|
+
|
|
285
|
+
{/* Usage Stats */}
|
|
286
|
+
<Card className="glassmorphism-dark p-6 border-border">
|
|
287
|
+
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
288
|
+
<Zap size={20} className="text-amber-500" />
|
|
289
|
+
Monthly Operations Limit
|
|
290
|
+
</h3>
|
|
291
|
+
<div className="mb-4">
|
|
292
|
+
<div className="flex justify-between mb-2">
|
|
293
|
+
<span className="text-foreground/60">
|
|
294
|
+
{stats?.ops_this_month?.toLocaleString() || 0} / {(!stats || stats.monthly_limit === null) ? 'Unlimited' : stats.monthly_limit.toLocaleString()} Ops
|
|
295
|
+
</span>
|
|
296
|
+
<span className="font-semibold">
|
|
297
|
+
{(!stats || stats.monthly_limit === null)
|
|
298
|
+
? '0%'
|
|
299
|
+
: `${Math.min(100, Math.round(((stats.ops_this_month || 0) / stats.monthly_limit) * 100))}%`}
|
|
300
|
+
</span>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="h-3 bg-white/5 rounded-full overflow-hidden">
|
|
303
|
+
<div
|
|
304
|
+
className="h-full bg-linear-to-r from-amber-500 to-amber-400 transition-all duration-500"
|
|
305
|
+
style={{ width: `${(!stats || stats.monthly_limit === null) ? 0 : Math.min(100, ((stats.ops_this_month || 0) / stats.monthly_limit) * 100)}%` }}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
<p className="text-sm text-foreground/60">Limit resets at the end of the 30-day billing cycle.</p>
|
|
310
|
+
</Card>
|
|
311
|
+
|
|
312
|
+
{/* Gas Ledger */}
|
|
313
|
+
<Card className="glassmorphism-dark p-6 border-border flex flex-col justify-between">
|
|
314
|
+
<div>
|
|
315
|
+
<h3 className="text-lg font-semibold mb-4 flex items-center justify-between">
|
|
316
|
+
Gas Ledger Balance
|
|
317
|
+
<span className="text-xs px-2 py-1 bg-white/10 rounded-full font-mono">Base (8453)</span>
|
|
318
|
+
</h3>
|
|
319
|
+
|
|
320
|
+
<div className="grid grid-cols-2 gap-4 mb-6">
|
|
321
|
+
<div className="p-4 bg-white/5 rounded-lg border border-border/50">
|
|
322
|
+
<p className="text-sm text-foreground/60 mb-1">Available to Sponsor</p>
|
|
323
|
+
<p className="text-2xl font-bold text-emerald-400">{formatWei(gasBalance?.available_wei)} ETH</p>
|
|
324
|
+
</div>
|
|
325
|
+
<div className="p-4 bg-white/5 rounded-lg border border-border/50">
|
|
326
|
+
<p className="text-sm text-foreground/60 mb-1">Reserved In-Flight</p>
|
|
327
|
+
<p className="text-2xl font-bold text-amber-400">{formatWei(gasBalance?.reserved_wei)} ETH</p>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<p className="text-sm text-foreground/60">
|
|
332
|
+
To top up gas, simply send any stablecoin amount (that doesn't match a plan price) to the treasury.
|
|
333
|
+
</p>
|
|
334
|
+
</Card>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* ── Crypto Payment Section ── */}
|
|
338
|
+
<div id="treasury-section" className="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-4">
|
|
339
|
+
|
|
340
|
+
{/* Treasury Address & Instructions */}
|
|
341
|
+
<Card className="glassmorphism-dark p-6 border-emerald-600/30">
|
|
342
|
+
<h3 className="text-xl font-semibold mb-2">Deposit Funds</h3>
|
|
343
|
+
<p className="text-sm text-foreground/60 mb-6">Send stablecoins from your registered wallets to this address.</p>
|
|
344
|
+
|
|
345
|
+
<div className="mb-6">
|
|
346
|
+
<label className="text-xs font-medium text-foreground/60 mb-2 block">Lumina Treasury Address</label>
|
|
347
|
+
<div className="flex gap-2">
|
|
348
|
+
<code className="flex-1 bg-white/5 p-3 rounded-lg text-sm font-mono truncate border border-border/50">
|
|
349
|
+
{treasury?.treasury_address || 'Loading...'}
|
|
350
|
+
</code>
|
|
351
|
+
<Button onClick={() => handleCopy(treasury?.treasury_address || '')} variant="outline" className="px-3">
|
|
352
|
+
{copied ? <CheckCircle2 size={18} className="text-emerald-500" /> : <Copy size={18} />}
|
|
353
|
+
</Button>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div className="space-y-3 mb-6">
|
|
358
|
+
<h4 className="text-sm font-medium">Accepted Networks & Tokens</h4>
|
|
359
|
+
{treasury && Object.entries(treasury.accepted_tokens).map(([chainId, tokens]) => (
|
|
360
|
+
<div key={chainId} className="flex justify-between items-center text-sm p-2 bg-white/5 rounded">
|
|
361
|
+
<span className="font-mono text-foreground/60">Chain {chainId}</span>
|
|
362
|
+
<span className="font-semibold">{tokens.map(t => t.symbol).join(', ')}</span>
|
|
363
|
+
</div>
|
|
364
|
+
))}
|
|
365
|
+
</div>
|
|
366
|
+
</Card>
|
|
367
|
+
|
|
368
|
+
{/* Payment Wallets & Verification */}
|
|
369
|
+
<Card className="glassmorphism-dark p-6 border-border">
|
|
370
|
+
<h3 className="text-lg font-semibold mb-1">Registered Payment Wallets</h3>
|
|
371
|
+
<p className="text-sm text-foreground/60 mb-4">
|
|
372
|
+
Connect and sign to prove ownership of the wallets you will send funds from.
|
|
373
|
+
</p>
|
|
374
|
+
|
|
375
|
+
<form onSubmit={handleRegisterWallet} className="mb-6">
|
|
376
|
+
<Button
|
|
377
|
+
type="submit"
|
|
378
|
+
disabled={isRegisteringWallet}
|
|
379
|
+
className="w-full bg-emerald-600 hover:bg-emerald-700 gap-2"
|
|
380
|
+
>
|
|
381
|
+
{isRegisteringWallet ? (
|
|
382
|
+
<><Loader2 className="animate-spin" size={18} /> Verifying Signature...</>
|
|
383
|
+
) : (
|
|
384
|
+
<><Wallet size={18} /> Connect & Verify New Wallet</>
|
|
385
|
+
)}
|
|
386
|
+
</Button>
|
|
387
|
+
</form>
|
|
388
|
+
|
|
389
|
+
<div className="space-y-2 max-h-40 overflow-y-auto pr-2">
|
|
390
|
+
{wallets.length === 0 ? (
|
|
391
|
+
<p className="text-sm text-foreground/40 text-center py-4">
|
|
392
|
+
No wallets registered yet. You must verify one before paying.
|
|
393
|
+
</p>
|
|
394
|
+
) : (
|
|
395
|
+
wallets.map(w => (
|
|
396
|
+
<div key={w.wallet_address} className="flex justify-between items-center p-3 bg-white/5 rounded-lg border border-border/50">
|
|
397
|
+
<span className="font-mono text-xs">{w.wallet_address}</span>
|
|
398
|
+
<button
|
|
399
|
+
onClick={() => handleRemoveWallet(w.wallet_address)}
|
|
400
|
+
className="text-red-400 hover:text-red-300 transition-colors"
|
|
401
|
+
>
|
|
402
|
+
<Trash2 size={16} />
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
))
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
</Card>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
)
|
|
412
|
+
}
|