create-lightning-scaffold 1.0.2 → 1.0.5
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/README.md +39 -1
- package/package.json +1 -1
- package/templates/base/.env.example.ejs +5 -0
- package/templates/mobile/components/History.tsx.ejs +57 -52
- package/templates/mobile/components/Recovery.tsx.ejs +78 -67
- package/templates/mobile/components/Swap.tsx.ejs +58 -152
- package/templates/state/redux/index.ts +2 -7
- package/templates/state/zustand/index.ts +0 -5
- package/templates/vite/src/components/History.tsx.ejs +16 -16
- package/templates/vite/src/components/Recovery.tsx.ejs +24 -18
- package/templates/vite/src/components/Swap.tsx.ejs +52 -91
- package/templates/web/components/History.tsx.ejs +17 -17
- package/templates/web/components/Recovery.tsx.ejs +27 -19
- package/templates/web/components/Swap.tsx.ejs +58 -134
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { useWallet } from '@lazorkit/wallet'
|
|
3
|
-
import { Connection, PublicKey
|
|
3
|
+
import { Connection, PublicKey } from '@solana/web3.js'
|
|
4
4
|
|
|
5
5
|
const TOKENS = [
|
|
6
6
|
{ symbol: 'SOL', mint: 'So11111111111111111111111111111111111111112', decimals: 9 },
|
|
@@ -13,6 +13,7 @@ const TOKENS = [
|
|
|
13
13
|
const SLIPPAGE_OPTIONS = [50, 100, 300]
|
|
14
14
|
|
|
15
15
|
const JUPITER_API = 'https://api.jup.ag/swap/v1'
|
|
16
|
+
const JUP_API_KEY = import.meta.env.VITE_JUPITER_API_KEY || ''
|
|
16
17
|
|
|
17
18
|
function useBalances(walletAddress: string | undefined) {
|
|
18
19
|
const [balances, setBalances] = useState<Record<string, number>>({})
|
|
@@ -21,9 +22,7 @@ function useBalances(walletAddress: string | undefined) {
|
|
|
21
22
|
const rpc = import.meta.env.VITE_SOLANA_RPC || 'https://api.devnet.solana.com'
|
|
22
23
|
const conn = new Connection(rpc)
|
|
23
24
|
const pk = new PublicKey(walletAddress)
|
|
24
|
-
|
|
25
25
|
conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {})
|
|
26
|
-
|
|
27
26
|
conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') })
|
|
28
27
|
.then((res) => {
|
|
29
28
|
const tokenBals: Record<string, number> = {}
|
|
@@ -72,22 +71,20 @@ export function Swap() {
|
|
|
72
71
|
const [signing, setSigning] = useState(false)
|
|
73
72
|
const [result, setResult] = useState<{ success: boolean; message: string } | null>(null)
|
|
74
73
|
|
|
75
|
-
const switchTokens = () => {
|
|
76
|
-
setFromToken(toToken)
|
|
77
|
-
setToToken(fromToken)
|
|
78
|
-
setAmount('')
|
|
79
|
-
setQuote(null)
|
|
80
|
-
}
|
|
74
|
+
const switchTokens = () => { setFromToken(toToken); setToToken(fromToken); setAmount(''); setQuote(null) }
|
|
81
75
|
|
|
82
76
|
useEffect(() => {
|
|
83
77
|
if (!amount || parseFloat(amount) <= 0) { setQuote(null); return }
|
|
84
78
|
const timeout = setTimeout(async () => {
|
|
85
79
|
try {
|
|
86
80
|
const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals)
|
|
87
|
-
const
|
|
81
|
+
const headers: Record<string, string> = {}
|
|
82
|
+
if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY
|
|
83
|
+
const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}&restrictIntermediateTokens=true`, { headers })
|
|
88
84
|
const data = await res.json()
|
|
89
85
|
if (data.outAmount) setQuote(data)
|
|
90
|
-
|
|
86
|
+
else setQuote(null)
|
|
87
|
+
} catch (e) { console.error('Quote error:', e); setQuote(null) }
|
|
91
88
|
}, 500)
|
|
92
89
|
return () => clearTimeout(timeout)
|
|
93
90
|
}, [amount, fromToken, toToken, slippage])
|
|
@@ -96,34 +93,48 @@ export function Swap() {
|
|
|
96
93
|
|
|
97
94
|
const handleSwap = async () => {
|
|
98
95
|
if (!smartWalletPubkey || !quote) return
|
|
99
|
-
setLoading(true)
|
|
100
|
-
setResult(null)
|
|
96
|
+
setLoading(true); setResult(null)
|
|
101
97
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
99
|
+
if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY
|
|
100
|
+
const swapRes = await fetch(`${JUPITER_API}/swap-instructions`, {
|
|
101
|
+
method: 'POST', headers,
|
|
102
|
+
body: JSON.stringify({ quoteResponse: quote, userPublicKey: smartWalletPubkey.toBase58(), wrapAndUnwrapSol: true, dynamicComputeUnitLimit: true }),
|
|
103
|
+
})
|
|
104
|
+
if (!swapRes.ok) throw new Error(`Jupiter API error: ${await swapRes.text()}`)
|
|
105
|
+
const { setupInstructions = [], swapInstruction, cleanupInstruction, addressLookupTableAddresses = [] } = await swapRes.json()
|
|
106
|
+
if (!swapInstruction) throw new Error('No swap instruction returned')
|
|
107
|
+
|
|
108
|
+
const toInstruction = (ix: any) => ({
|
|
109
|
+
programId: new PublicKey(ix.programId),
|
|
110
|
+
keys: ix.accounts.map((acc: any) => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable })),
|
|
111
|
+
data: Buffer.from(ix.data, 'base64'),
|
|
106
112
|
})
|
|
107
|
-
const
|
|
108
|
-
|
|
113
|
+
const instructions = [...setupInstructions.map(toInstruction), toInstruction(swapInstruction), ...(cleanupInstruction ? [toInstruction(cleanupInstruction)] : [])]
|
|
114
|
+
|
|
115
|
+
let addressLookupTableAccounts: any[] = []
|
|
116
|
+
if (addressLookupTableAddresses.length > 0) {
|
|
117
|
+
const rpc = import.meta.env.VITE_SOLANA_RPC || 'https://api.devnet.solana.com'
|
|
118
|
+
const conn = new Connection(rpc)
|
|
119
|
+
const alts = await Promise.all(addressLookupTableAddresses.map(async (addr: string) => (await conn.getAddressLookupTable(new PublicKey(addr))).value))
|
|
120
|
+
addressLookupTableAccounts = alts.filter(Boolean)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const signature = await signAndSendTransaction({ instructions, transactionOptions: { feeToken: 'USDC', addressLookupTableAccounts } })
|
|
109
124
|
setResult({ success: true, message: `Swapped! ${signature.slice(0, 8)}...` })
|
|
110
|
-
setAmount('')
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' })
|
|
114
|
-
} finally { setLoading(false) }
|
|
125
|
+
setAmount(''); setQuote(null)
|
|
126
|
+
} catch (e) { console.error('Swap error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' }) }
|
|
127
|
+
finally { setLoading(false) }
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
const handleSignMessage = async () => {
|
|
118
|
-
setSigning(true)
|
|
119
|
-
setResult(null)
|
|
131
|
+
setSigning(true); setResult(null)
|
|
120
132
|
try {
|
|
121
133
|
const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`
|
|
122
134
|
const { signature } = await signMessage(message)
|
|
123
135
|
setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` })
|
|
124
|
-
} catch (e) {
|
|
125
|
-
|
|
126
|
-
} finally { setSigning(false) }
|
|
136
|
+
} catch (e) { console.error('Sign error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Sign failed' }) }
|
|
137
|
+
finally { setSigning(false) }
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
const addr = wallet?.smartWallet || ''
|
|
@@ -131,43 +142,21 @@ export function Swap() {
|
|
|
131
142
|
return (
|
|
132
143
|
<% if (styling === 'tailwind') { %>
|
|
133
144
|
<div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
|
|
134
|
-
<div className="flex items-center justify-between mb-6">
|
|
135
|
-
<h1 className="text-lg font-semibold">Swap</h1>
|
|
136
|
-
<button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">{addr.slice(0, 6)}...{addr.slice(-4)}</button>
|
|
137
|
-
</div>
|
|
145
|
+
<div className="flex items-center justify-between mb-6"><h1 className="text-lg font-semibold">Swap</h1><button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">{addr.slice(0, 6)}...{addr.slice(-4)}</button></div>
|
|
138
146
|
<div className="space-y-2">
|
|
139
147
|
<div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
140
|
-
<div className="flex items-center justify-between mb-2">
|
|
141
|
-
|
|
142
|
-
<span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span>
|
|
143
|
-
</div>
|
|
144
|
-
<div className="flex items-center gap-3">
|
|
145
|
-
<input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} className="flex-1 text-2xl font-medium bg-transparent w-full focus:outline-none text-white placeholder-neutral-600" />
|
|
146
|
-
<TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
|
|
147
|
-
</div>
|
|
148
|
+
<div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">From</span><span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
149
|
+
<div className="flex items-center gap-3"><input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} className="flex-1 text-2xl font-medium bg-transparent w-full focus:outline-none text-white placeholder-neutral-600" /><TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} /></div>
|
|
148
150
|
<button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} className="text-xs text-neutral-500 hover:text-white mt-1">Max</button>
|
|
149
151
|
</div>
|
|
150
152
|
<div className="flex justify-center"><button onClick={switchTokens} className="w-8 h-8 border border-neutral-700 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-800 bg-neutral-800">↓</button></div>
|
|
151
153
|
<div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
152
|
-
<div className="flex items-center justify-between mb-2">
|
|
153
|
-
|
|
154
|
-
<span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span>
|
|
155
|
-
</div>
|
|
156
|
-
<div className="flex items-center gap-3">
|
|
157
|
-
<span className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</span>
|
|
158
|
-
<TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
|
|
159
|
-
</div>
|
|
154
|
+
<div className="flex items-center justify-between mb-2"><span className="text-xs text-neutral-500">To</span><span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
155
|
+
<div className="flex items-center gap-3"><span className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</span><TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} /></div>
|
|
160
156
|
{quote && <p className="text-xs text-neutral-500 mt-2">via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</p>}
|
|
161
157
|
</div>
|
|
162
158
|
</div>
|
|
163
|
-
<div className="flex items-center justify-between mt-3 text-xs">
|
|
164
|
-
<span className="text-neutral-500">Slippage</span>
|
|
165
|
-
<div className="flex gap-1">
|
|
166
|
-
{SLIPPAGE_OPTIONS.map((s) => (
|
|
167
|
-
<button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white text-black' : 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'}`}>{s / 100}%</button>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
159
|
+
<div className="flex items-center justify-between mt-3 text-xs"><span className="text-neutral-500">Slippage</span><div className="flex gap-1">{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white text-black' : 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'}`}>{s / 100}%</button>))}</div></div>
|
|
171
160
|
<button onClick={handleSwap} disabled={loading || !quote} className="mt-4 w-full py-3 bg-white text-black text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50">{loading ? 'Swapping...' : 'Swap'}</button>
|
|
172
161
|
<button onClick={handleSignMessage} disabled={signing} className="mt-2 w-full py-3 bg-transparent text-white text-sm font-medium rounded-xl border border-neutral-700 hover:bg-neutral-800 disabled:opacity-50">{signing ? 'Signing...' : 'Sign Message'}</button>
|
|
173
162
|
{result && <p className={`mt-3 text-sm text-center ${result.success ? 'text-neutral-400' : 'text-red-400'}`}>{result.message}</p>}
|
|
@@ -175,35 +164,19 @@ export function Swap() {
|
|
|
175
164
|
</div>
|
|
176
165
|
<% } else { %>
|
|
177
166
|
<div style={styles.container}>
|
|
178
|
-
<div style={styles.header}>
|
|
179
|
-
<h1 style={styles.title}>Swap</h1>
|
|
180
|
-
<button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button>
|
|
181
|
-
</div>
|
|
167
|
+
<div style={styles.header}><h1 style={styles.title}>Swap</h1><button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button></div>
|
|
182
168
|
<div style={styles.card}>
|
|
183
169
|
<div style={styles.label}><span>From</span><span>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
184
|
-
<div style={styles.inputRow}>
|
|
185
|
-
<input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} />
|
|
186
|
-
<TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
|
|
187
|
-
</div>
|
|
170
|
+
<div style={styles.inputRow}><input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} /><TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} /></div>
|
|
188
171
|
<button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} style={{ ...styles.address, fontSize: 12, marginTop: 4 }}>Max</button>
|
|
189
172
|
</div>
|
|
190
173
|
<div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer' }}>↓</button></div>
|
|
191
174
|
<div style={styles.card}>
|
|
192
175
|
<div style={styles.label}><span>To</span><span>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
193
|
-
<div style={styles.inputRow}>
|
|
194
|
-
<span style={{ ...styles.input, color: '#737373' }}>{outputAmount}</span>
|
|
195
|
-
<TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
|
|
196
|
-
</div>
|
|
176
|
+
<div style={styles.inputRow}><span style={{ ...styles.input, color: '#737373' }}>{outputAmount}</span><TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} /></div>
|
|
197
177
|
{quote && <p style={styles.quote}>via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</p>}
|
|
198
178
|
</div>
|
|
199
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}>
|
|
200
|
-
<span style={{ color: '#737373' }}>Slippage</span>
|
|
201
|
-
<div style={{ display: 'flex', gap: 4 }}>
|
|
202
|
-
{SLIPPAGE_OPTIONS.map((s) => (
|
|
203
|
-
<button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>{s / 100}%</button>
|
|
204
|
-
))}
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
179
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}><span style={{ color: '#737373' }}>Slippage</span><div style={{ display: 'flex', gap: 4 }}>{SLIPPAGE_OPTIONS.map((s) => (<button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#fafafa' : '#262626', color: slippage === s ? '#0a0a0a' : '#a3a3a3' }}>{s / 100}%</button>))}</div></div>
|
|
207
180
|
<button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>{loading ? 'Swapping...' : 'Swap'}</button>
|
|
208
181
|
<button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>{signing ? 'Signing...' : 'Sign Message'}</button>
|
|
209
182
|
{result && <p style={{ ...styles.result, color: result.success ? '#737373' : '#ef4444' }}>{result.message}</p>}
|
|
@@ -219,22 +192,10 @@ function TokenSelect({ token, tokens, onChange, exclude }: { token: typeof TOKEN
|
|
|
219
192
|
<div style={{ position: 'relative' }}>
|
|
220
193
|
<% if (styling === 'tailwind') { %>
|
|
221
194
|
<button onClick={() => setOpen(!open)} className="flex items-center gap-2 px-3 py-2 bg-neutral-700 rounded-lg text-sm font-medium text-white">{token.symbol}<span className="text-neutral-400">▼</span></button>
|
|
222
|
-
{open && (
|
|
223
|
-
<div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">
|
|
224
|
-
{tokens.filter((t) => t.symbol !== exclude).map((t) => (
|
|
225
|
-
<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>
|
|
226
|
-
))}
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
195
|
+
{open && (<div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} className="block w-full px-4 py-2 text-left text-sm text-white hover:bg-neutral-700">{t.symbol}</button>))}</div>)}
|
|
229
196
|
<% } else { %>
|
|
230
197
|
<button onClick={() => setOpen(!open)} style={styles.tokenBtn}>{token.symbol} <span style={{ color: '#a3a3a3' }}>▼</span></button>
|
|
231
|
-
{open && (
|
|
232
|
-
<div style={styles.dropdown as React.CSSProperties}>
|
|
233
|
-
{tokens.filter((t) => t.symbol !== exclude).map((t) => (
|
|
234
|
-
<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} style={styles.dropdownItem}>{t.symbol}</button>
|
|
235
|
-
))}
|
|
236
|
-
</div>
|
|
237
|
-
)}
|
|
198
|
+
{open && (<div style={styles.dropdown as React.CSSProperties}>{tokens.filter((t) => t.symbol !== exclude).map((t) => (<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} style={styles.dropdownItem}>{t.symbol}</button>))}</div>)}
|
|
238
199
|
<% } %>
|
|
239
200
|
</div>
|
|
240
201
|
)
|
|
@@ -4,20 +4,20 @@ import { useWallet } from "@lazorkit/wallet";
|
|
|
4
4
|
import { Connection, PublicKey } from "@solana/web3.js";
|
|
5
5
|
<% if (styling !== 'tailwind') { %>
|
|
6
6
|
const styles: Record<string, React.CSSProperties> = {
|
|
7
|
-
container: { width: '100%', maxWidth: 360 },
|
|
7
|
+
container: { width: '100%', maxWidth: 360, background: '#0a0a0a', borderRadius: 16, padding: 24, color: '#fafafa' },
|
|
8
8
|
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
|
|
9
|
-
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
9
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0, color: '#fafafa' },
|
|
10
10
|
list: { display: 'flex', flexDirection: 'column', gap: 8 },
|
|
11
|
-
item: { border: '1px solid #
|
|
11
|
+
item: { border: '1px solid #262626', borderRadius: 12, padding: 12, background: '#171717' },
|
|
12
12
|
row: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
|
|
13
|
-
sig: { fontFamily: 'monospace', fontSize: 13, color: '#
|
|
14
|
-
time: { fontSize: 12, color: '#
|
|
13
|
+
sig: { fontFamily: 'monospace', fontSize: 13, color: '#fafafa' },
|
|
14
|
+
time: { fontSize: 12, color: '#737373', marginTop: 4 },
|
|
15
15
|
status: { fontSize: 11, padding: '2px 6px', borderRadius: 4 },
|
|
16
|
-
success: { backgroundColor: '#
|
|
17
|
-
failed: { backgroundColor: '#
|
|
18
|
-
empty: { textAlign: 'center', padding: 32, color: '#
|
|
16
|
+
success: { backgroundColor: '#14532d', color: '#4ade80' },
|
|
17
|
+
failed: { backgroundColor: '#7f1d1d', color: '#f87171' },
|
|
18
|
+
empty: { textAlign: 'center', padding: 32, color: '#737373', fontSize: 14 },
|
|
19
19
|
back: { fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
20
|
-
link: { color: '#
|
|
20
|
+
link: { color: '#fafafa', textDecoration: 'none' },
|
|
21
21
|
};
|
|
22
22
|
<% } %>
|
|
23
23
|
interface Props {
|
|
@@ -27,7 +27,7 @@ interface Props {
|
|
|
27
27
|
interface TxInfo {
|
|
28
28
|
signature: string;
|
|
29
29
|
slot: number;
|
|
30
|
-
blockTime: number | null;
|
|
30
|
+
blockTime: number | null | undefined;
|
|
31
31
|
err: any;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -58,26 +58,26 @@ export function History({ onBack }: Props) {
|
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
60
|
<% if (styling === 'tailwind') { %>
|
|
61
|
-
<div className="w-full max-w-sm">
|
|
61
|
+
<div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
|
|
62
62
|
<div className="flex items-center justify-between mb-4">
|
|
63
63
|
<h1 className="text-lg font-semibold">History</h1>
|
|
64
|
-
<button onClick={onBack} className="text-sm text-neutral-500 hover:text-
|
|
64
|
+
<button onClick={onBack} className="text-sm text-neutral-500 hover:text-white">← Back</button>
|
|
65
65
|
</div>
|
|
66
66
|
{loading ? (
|
|
67
|
-
<p className="text-center py-8 text-neutral-
|
|
67
|
+
<p className="text-center py-8 text-neutral-500">Loading...</p>
|
|
68
68
|
) : txs.length === 0 ? (
|
|
69
|
-
<p className="text-center py-8 text-neutral-
|
|
69
|
+
<p className="text-center py-8 text-neutral-500">No transactions yet</p>
|
|
70
70
|
) : (
|
|
71
71
|
<div className="space-y-2">
|
|
72
72
|
{txs.map((tx) => (
|
|
73
|
-
<a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" className="block border border-neutral-
|
|
73
|
+
<a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" className="block border border-neutral-700 rounded-xl p-3 bg-neutral-800 hover:bg-neutral-700">
|
|
74
74
|
<div className="flex justify-between items-center">
|
|
75
75
|
<span className="font-mono text-sm">{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</span>
|
|
76
|
-
<span className={`text-xs px-2 py-0.5 rounded ${tx.err ? "bg-red-
|
|
76
|
+
<span className={`text-xs px-2 py-0.5 rounded ${tx.err ? "bg-red-900 text-red-400" : "bg-green-900 text-green-400"}`}>
|
|
77
77
|
{tx.err ? "Failed" : "Success"}
|
|
78
78
|
</span>
|
|
79
79
|
</div>
|
|
80
|
-
<p className="text-xs text-neutral-
|
|
80
|
+
<p className="text-xs text-neutral-500 mt-1">{formatTime(tx.blockTime)}</p>
|
|
81
81
|
</a>
|
|
82
82
|
))}
|
|
83
83
|
</div>
|
|
@@ -3,15 +3,15 @@ import { useState } from "react";
|
|
|
3
3
|
import { useWallet } from "@lazorkit/wallet";
|
|
4
4
|
<% if (styling !== 'tailwind') { %>
|
|
5
5
|
const styles: Record<string, React.CSSProperties> = {
|
|
6
|
-
container: { width: '100%', maxWidth: 360 },
|
|
7
|
-
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
6
|
+
container: { width: '100%', maxWidth: 360, background: '#0a0a0a', borderRadius: 16, padding: 24, color: '#fafafa' },
|
|
7
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0, color: '#fafafa' },
|
|
8
8
|
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', lineHeight: 1.5 },
|
|
9
|
-
card: { marginTop: 16, border: '1px solid #
|
|
10
|
-
cardTitle: { fontSize: 14, fontWeight: 500, margin: 0 },
|
|
9
|
+
card: { marginTop: 16, border: '1px solid #262626', borderRadius: 12, padding: 16, background: '#171717' },
|
|
10
|
+
cardTitle: { fontSize: 14, fontWeight: 500, margin: 0, color: '#fafafa' },
|
|
11
11
|
cardDesc: { marginTop: 4, fontSize: 13, color: '#737373' },
|
|
12
|
-
button: { marginTop: 12, width: '100%', backgroundColor: '#
|
|
13
|
-
buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#
|
|
14
|
-
info: { marginTop: 16, padding: 12, backgroundColor: '#
|
|
12
|
+
button: { marginTop: 12, width: '100%', backgroundColor: '#fafafa', color: '#0a0a0a', padding: '12px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
13
|
+
buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#fafafa', padding: '12px 16px', borderRadius: 10, border: '1px solid #262626', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
14
|
+
info: { marginTop: 16, padding: 12, backgroundColor: '#171717', borderRadius: 8, fontSize: 12, color: '#a3a3a3' },
|
|
15
15
|
back: { marginTop: 16, fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
16
16
|
};
|
|
17
17
|
<% } %>
|
|
@@ -25,11 +25,19 @@ export function Recovery({ onBack }: Props) {
|
|
|
25
25
|
const portalUrl = process.env.NEXT_PUBLIC_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
|
|
26
26
|
|
|
27
27
|
const handleAddDevice = () => {
|
|
28
|
-
|
|
28
|
+
// Open portal with wallet context
|
|
29
|
+
const url = wallet?.smartWallet
|
|
30
|
+
? `${portalUrl}?wallet=${wallet.smartWallet}&action=add-device`
|
|
31
|
+
: portalUrl;
|
|
32
|
+
window.open(url, "_blank");
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
const handleManageDevices = () => {
|
|
32
|
-
|
|
36
|
+
// Open portal with wallet context
|
|
37
|
+
const url = wallet?.smartWallet
|
|
38
|
+
? `${portalUrl}?wallet=${wallet.smartWallet}&action=manage-devices`
|
|
39
|
+
: portalUrl;
|
|
40
|
+
window.open(url, "_blank");
|
|
33
41
|
};
|
|
34
42
|
|
|
35
43
|
const copyAddress = () => {
|
|
@@ -42,39 +50,39 @@ export function Recovery({ onBack }: Props) {
|
|
|
42
50
|
|
|
43
51
|
return (
|
|
44
52
|
<% if (styling === 'tailwind') { %>
|
|
45
|
-
<div className="w-full max-w-sm">
|
|
53
|
+
<div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
|
|
46
54
|
<h1 className="text-lg font-semibold">Recovery & Backup</h1>
|
|
47
55
|
<p className="mt-2 text-sm text-neutral-500">
|
|
48
56
|
Add backup passkeys from other devices to ensure you never lose access.
|
|
49
57
|
</p>
|
|
50
|
-
<div className="mt-4 border border-neutral-
|
|
58
|
+
<div className="mt-4 border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
51
59
|
<h3 className="text-sm font-medium">Add Backup Device</h3>
|
|
52
60
|
<p className="mt-1 text-xs text-neutral-500">
|
|
53
61
|
Register a passkey from another phone, tablet, or computer.
|
|
54
62
|
</p>
|
|
55
|
-
<button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-
|
|
63
|
+
<button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-white text-black text-sm font-medium rounded-lg hover:opacity-90">
|
|
56
64
|
Add Device
|
|
57
65
|
</button>
|
|
58
66
|
</div>
|
|
59
|
-
<div className="mt-3 border border-neutral-
|
|
67
|
+
<div className="mt-3 border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
60
68
|
<h3 className="text-sm font-medium">Manage Devices</h3>
|
|
61
69
|
<p className="mt-1 text-xs text-neutral-500">
|
|
62
70
|
View and remove registered passkeys.
|
|
63
71
|
</p>
|
|
64
|
-
<button onClick={handleManageDevices} className="mt-3 w-full py-3 bg-transparent text-
|
|
72
|
+
<button onClick={handleManageDevices} className="mt-3 w-full py-3 bg-transparent text-white text-sm font-medium rounded-lg border border-neutral-700 hover:bg-neutral-700">
|
|
65
73
|
View Devices
|
|
66
74
|
</button>
|
|
67
75
|
</div>
|
|
68
|
-
<div className="mt-4 p-3 bg-neutral-
|
|
69
|
-
<p className="text-xs text-neutral-
|
|
76
|
+
<div className="mt-4 p-3 bg-neutral-800 rounded-lg">
|
|
77
|
+
<p className="text-xs text-neutral-400">
|
|
70
78
|
<strong>Wallet:</strong>{" "}
|
|
71
|
-
<button onClick={copyAddress} className="font-mono hover:text-
|
|
79
|
+
<button onClick={copyAddress} className="font-mono hover:text-white">
|
|
72
80
|
{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}
|
|
73
81
|
</button>
|
|
74
|
-
{copied && <span className="ml-2 text-green-
|
|
82
|
+
{copied && <span className="ml-2 text-green-400">Copied!</span>}
|
|
75
83
|
</p>
|
|
76
84
|
</div>
|
|
77
|
-
<button onClick={onBack} className="mt-4 text-sm text-neutral-500 hover:text-
|
|
85
|
+
<button onClick={onBack} className="mt-4 text-sm text-neutral-500 hover:text-white">
|
|
78
86
|
← Back
|
|
79
87
|
</button>
|
|
80
88
|
</div>
|