@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.
Files changed (152) hide show
  1. package/BUTTON_FIXES.md +59 -0
  2. package/DOCS_INDEX.md +332 -0
  3. package/DOCUMENTATION.md +252 -0
  4. package/DOCUMENTATION_BUILD_SUMMARY.md +376 -0
  5. package/DOCUMENTATION_COMPLETE.md +311 -0
  6. package/FEATURES.md +333 -0
  7. package/Lumina-sdk/src/components/lumina-provider.tsx +46 -0
  8. package/Lumina-sdk/src/components/transaction-confirm.tsx +242 -0
  9. package/Lumina-sdk/src/components/wallet-display.tsx +157 -0
  10. package/Lumina-sdk/src/components/wallet-login.tsx +163 -0
  11. package/Lumina-sdk/src/hooks/use-mobile.ts +19 -0
  12. package/Lumina-sdk/src/hooks/use-toast.ts +191 -0
  13. package/Lumina-sdk/src/index.ts +0 -0
  14. package/Lumina-sdk/src/lib/api.ts +66 -0
  15. package/Lumina-sdk/src/lib/utils.ts +6 -0
  16. package/Lumina-sdk/src/package.json +42 -0
  17. package/Lumina-sdk/src/tsconfig.json +19 -0
  18. package/NEW_FILES_MANIFEST.txt +146 -0
  19. package/README.md +298 -0
  20. package/app/dashboard/analytics/page.tsx +218 -0
  21. package/app/dashboard/api-keys/page.tsx +260 -0
  22. package/app/dashboard/billing/page.tsx +412 -0
  23. package/app/dashboard/integration/page.tsx +185 -0
  24. package/app/dashboard/layout.tsx +18 -0
  25. package/app/dashboard/page.tsx +244 -0
  26. package/app/dashboard/settings/page.tsx +285 -0
  27. package/app/dashboard/users/page.tsx +148 -0
  28. package/app/docs/api/authentication/page.tsx +246 -0
  29. package/app/docs/api/endpoints/page.tsx +397 -0
  30. package/app/docs/api/errors/page.tsx +305 -0
  31. package/app/docs/api/overview/page.tsx +306 -0
  32. package/app/docs/examples/basic-setup/page.tsx +256 -0
  33. package/app/docs/examples/multi-chain/page.tsx +331 -0
  34. package/app/docs/examples/nextjs-full-stack/page.tsx +332 -0
  35. package/app/docs/getting-started/environment-setup/page.tsx +243 -0
  36. package/app/docs/getting-started/installation/page.tsx +187 -0
  37. package/app/docs/getting-started/introduction/page.tsx +178 -0
  38. package/app/docs/getting-started/quick-start/page.tsx +199 -0
  39. package/app/docs/guides/nextjs/page.tsx +358 -0
  40. package/app/docs/guides/react/page.tsx +230 -0
  41. package/app/docs/guides/security/page.tsx +284 -0
  42. package/app/docs/layout.tsx +32 -0
  43. package/app/docs/page.tsx +180 -0
  44. package/app/docs/sdk/lumina-provider/page.tsx +186 -0
  45. package/app/docs/sdk/transaction-confirm/page.tsx +331 -0
  46. package/app/docs/sdk/wallet-display/page.tsx +224 -0
  47. package/app/docs/sdk/wallet-login/page.tsx +207 -0
  48. package/app/docs/troubleshooting/common-issues/page.tsx +301 -0
  49. package/app/docs/troubleshooting/faq/page.tsx +105 -0
  50. package/app/globals.css +125 -0
  51. package/app/invite/[token]/page.tsx +78 -0
  52. package/app/layout.tsx +36 -0
  53. package/app/login/page.tsx +175 -0
  54. package/app/page.tsx +336 -0
  55. package/app/sdk-demo/page.tsx +239 -0
  56. package/components/dashboard-sidebar.tsx +113 -0
  57. package/components/docs/breadcrumb.tsx +51 -0
  58. package/components/docs/callout.tsx +53 -0
  59. package/components/docs/code-block.tsx +77 -0
  60. package/components/docs/docs-sidebar.tsx +214 -0
  61. package/components/docs/table-of-contents.tsx +83 -0
  62. package/components/sdk/lumina-provider.tsx +46 -0
  63. package/components/sdk/transaction-confirm.tsx +242 -0
  64. package/components/sdk/wallet-display.tsx +157 -0
  65. package/components/sdk/wallet-login.tsx +163 -0
  66. package/components/theme-provider.tsx +11 -0
  67. package/components/ui/accordion.tsx +66 -0
  68. package/components/ui/alert-dialog.tsx +157 -0
  69. package/components/ui/alert.tsx +66 -0
  70. package/components/ui/aspect-ratio.tsx +11 -0
  71. package/components/ui/avatar.tsx +53 -0
  72. package/components/ui/badge.tsx +46 -0
  73. package/components/ui/breadcrumb.tsx +109 -0
  74. package/components/ui/button-group.tsx +83 -0
  75. package/components/ui/button.tsx +60 -0
  76. package/components/ui/calendar.tsx +213 -0
  77. package/components/ui/card.tsx +92 -0
  78. package/components/ui/carousel.tsx +241 -0
  79. package/components/ui/chart.tsx +351 -0
  80. package/components/ui/checkbox.tsx +32 -0
  81. package/components/ui/collapsible.tsx +33 -0
  82. package/components/ui/command.tsx +184 -0
  83. package/components/ui/context-menu.tsx +252 -0
  84. package/components/ui/dialog.tsx +143 -0
  85. package/components/ui/drawer.tsx +135 -0
  86. package/components/ui/dropdown-menu.tsx +257 -0
  87. package/components/ui/empty.tsx +104 -0
  88. package/components/ui/field.tsx +244 -0
  89. package/components/ui/form.tsx +167 -0
  90. package/components/ui/hover-card.tsx +44 -0
  91. package/components/ui/input-group.tsx +169 -0
  92. package/components/ui/input-otp.tsx +77 -0
  93. package/components/ui/input.tsx +21 -0
  94. package/components/ui/item.tsx +193 -0
  95. package/components/ui/kbd.tsx +28 -0
  96. package/components/ui/label.tsx +24 -0
  97. package/components/ui/menubar.tsx +276 -0
  98. package/components/ui/navigation-menu.tsx +166 -0
  99. package/components/ui/pagination.tsx +127 -0
  100. package/components/ui/popover.tsx +48 -0
  101. package/components/ui/progress.tsx +31 -0
  102. package/components/ui/radio-group.tsx +45 -0
  103. package/components/ui/resizable.tsx +56 -0
  104. package/components/ui/scroll-area.tsx +58 -0
  105. package/components/ui/select.tsx +185 -0
  106. package/components/ui/separator.tsx +28 -0
  107. package/components/ui/sheet.tsx +139 -0
  108. package/components/ui/sidebar.tsx +726 -0
  109. package/components/ui/skeleton.tsx +13 -0
  110. package/components/ui/slider.tsx +59 -0
  111. package/components/ui/sonner.tsx +25 -0
  112. package/components/ui/spinner.tsx +16 -0
  113. package/components/ui/switch.tsx +29 -0
  114. package/components/ui/table.tsx +116 -0
  115. package/components/ui/tabs.tsx +66 -0
  116. package/components/ui/textarea.tsx +18 -0
  117. package/components/ui/toast.tsx +129 -0
  118. package/components/ui/toaster.tsx +35 -0
  119. package/components/ui/toggle-group.tsx +73 -0
  120. package/components/ui/toggle.tsx +47 -0
  121. package/components/ui/tooltip.tsx +61 -0
  122. package/components/ui/use-mobile.tsx +19 -0
  123. package/components/ui/use-toast.ts +191 -0
  124. package/components.json +21 -0
  125. package/hooks/use-mobile.ts +19 -0
  126. package/hooks/use-toast.ts +191 -0
  127. package/lib/api.ts +66 -0
  128. package/lib/utils.ts +6 -0
  129. package/next-env.d.ts +6 -0
  130. package/next.config.mjs +11 -0
  131. package/package.json +73 -0
  132. package/pnpm-workspace.yaml +5 -0
  133. package/postcss.config.mjs +8 -0
  134. package/public/apple-icon.png +0 -0
  135. package/public/fav.jpeg +0 -0
  136. package/public/fav.png +0 -0
  137. package/public/icon-dark-32x32.png +0 -0
  138. package/public/icon-light-32x32.png +0 -0
  139. package/public/icon.png +0 -0
  140. package/public/icon.svg +26 -0
  141. package/public/logo.jpeg +0 -0
  142. package/public/logo.png +0 -0
  143. package/public/logo2.jpeg +0 -0
  144. package/public/logo2.png +0 -0
  145. package/public/placeholder-logo.png +0 -0
  146. package/public/placeholder-logo.svg +1 -0
  147. package/public/placeholder-user.jpg +0 -0
  148. package/public/placeholder.jpg +0 -0
  149. package/public/placeholder.svg +1 -0
  150. package/styles/globals.css +209 -0
  151. package/tailwind.config.ts +15 -0
  152. 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
+ }