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
|
@@ -15,6 +15,7 @@ const TOKENS = [
|
|
|
15
15
|
const SLIPPAGE_OPTIONS = [50, 100, 300];
|
|
16
16
|
|
|
17
17
|
const JUPITER_API = 'https://api.jup.ag/swap/v1';
|
|
18
|
+
const JUP_API_KEY = process.env.EXPO_PUBLIC_JUPITER_API_KEY || '';
|
|
18
19
|
|
|
19
20
|
function useBalances(walletAddress: string | undefined) {
|
|
20
21
|
const [balances, setBalances] = useState<Record<string, number>>({});
|
|
@@ -23,9 +24,7 @@ function useBalances(walletAddress: string | undefined) {
|
|
|
23
24
|
const rpc = process.env.EXPO_PUBLIC_SOLANA_RPC || 'https://api.devnet.solana.com';
|
|
24
25
|
const conn = new Connection(rpc);
|
|
25
26
|
const pk = new PublicKey(walletAddress);
|
|
26
|
-
|
|
27
27
|
conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {});
|
|
28
|
-
|
|
29
28
|
conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') })
|
|
30
29
|
.then((res) => {
|
|
31
30
|
const tokenBals: Record<string, number> = {};
|
|
@@ -39,13 +38,6 @@ function useBalances(walletAddress: string | undefined) {
|
|
|
39
38
|
}, [walletAddress]);
|
|
40
39
|
return balances;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
const TOKENS = [
|
|
44
|
-
{ symbol: 'SOL', mint: 'So11111111111111111111111111111111111111112', decimals: 9 },
|
|
45
|
-
{ symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6 },
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const JUPITER_API = 'https://api.jup.ag/swap/v1';
|
|
49
41
|
<% if (styling !== 'nativewind') { %>
|
|
50
42
|
const styles = StyleSheet.create({
|
|
51
43
|
container: { flex: 1, backgroundColor: '#fff', padding: 24, paddingTop: 60 },
|
|
@@ -101,22 +93,20 @@ export function Swap() {
|
|
|
101
93
|
const [showPicker, setShowPicker] = useState<'from' | 'to' | null>(null);
|
|
102
94
|
const redirectUrl = Linking.createURL('/');
|
|
103
95
|
|
|
104
|
-
const switchTokens = () => {
|
|
105
|
-
setFromToken(toToken);
|
|
106
|
-
setToToken(fromToken);
|
|
107
|
-
setAmount('');
|
|
108
|
-
setQuote(null);
|
|
109
|
-
};
|
|
96
|
+
const switchTokens = () => { setFromToken(toToken); setToToken(fromToken); setAmount(''); setQuote(null); };
|
|
110
97
|
|
|
111
98
|
useEffect(() => {
|
|
112
99
|
if (!amount || parseFloat(amount) <= 0) { setQuote(null); return; }
|
|
113
100
|
const timeout = setTimeout(async () => {
|
|
114
101
|
try {
|
|
115
102
|
const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals);
|
|
116
|
-
const
|
|
103
|
+
const headers: Record<string, string> = {};
|
|
104
|
+
if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY;
|
|
105
|
+
const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}&restrictIntermediateTokens=true`, { headers });
|
|
117
106
|
const data = await res.json();
|
|
118
107
|
if (data.outAmount) setQuote(data);
|
|
119
|
-
|
|
108
|
+
else setQuote(null);
|
|
109
|
+
} catch (e) { console.error('Quote error:', e); setQuote(null); }
|
|
120
110
|
}, 500);
|
|
121
111
|
return () => clearTimeout(timeout);
|
|
122
112
|
}, [amount, fromToken, toToken, slippage]);
|
|
@@ -125,34 +115,48 @@ export function Swap() {
|
|
|
125
115
|
|
|
126
116
|
const handleSwap = async () => {
|
|
127
117
|
if (!wallet?.smartWallet || !quote) return;
|
|
128
|
-
setLoading(true);
|
|
129
|
-
setResult(null);
|
|
118
|
+
setLoading(true); setResult(null);
|
|
130
119
|
try {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
120
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
121
|
+
if (JUP_API_KEY) headers['x-api-key'] = JUP_API_KEY;
|
|
122
|
+
const swapRes = await fetch(`${JUPITER_API}/swap-instructions`, {
|
|
123
|
+
method: 'POST', headers,
|
|
124
|
+
body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet.smartWallet, wrapAndUnwrapSol: true, dynamicComputeUnitLimit: true }),
|
|
125
|
+
});
|
|
126
|
+
if (!swapRes.ok) throw new Error(`Jupiter API error: ${await swapRes.text()}`);
|
|
127
|
+
const { setupInstructions = [], swapInstruction, cleanupInstruction, addressLookupTableAddresses = [] } = await swapRes.json();
|
|
128
|
+
if (!swapInstruction) throw new Error('No swap instruction returned');
|
|
129
|
+
|
|
130
|
+
const toInstruction = (ix: any) => ({
|
|
131
|
+
programId: new PublicKey(ix.programId),
|
|
132
|
+
keys: ix.accounts.map((acc: any) => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable })),
|
|
133
|
+
data: Buffer.from(ix.data, 'base64'),
|
|
135
134
|
});
|
|
136
|
-
const
|
|
137
|
-
|
|
135
|
+
const instructions = [...setupInstructions.map(toInstruction), toInstruction(swapInstruction), ...(cleanupInstruction ? [toInstruction(cleanupInstruction)] : [])];
|
|
136
|
+
|
|
137
|
+
let addressLookupTableAccounts: any[] = [];
|
|
138
|
+
if (addressLookupTableAddresses.length > 0) {
|
|
139
|
+
const rpc = process.env.EXPO_PUBLIC_SOLANA_RPC || 'https://api.devnet.solana.com';
|
|
140
|
+
const conn = new Connection(rpc);
|
|
141
|
+
const alts = await Promise.all(addressLookupTableAddresses.map(async (addr: string) => (await conn.getAddressLookupTable(new PublicKey(addr))).value));
|
|
142
|
+
addressLookupTableAccounts = alts.filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sig = await signAndSendTransaction({ instructions, transactionOptions: { feeToken: 'USDC', addressLookupTableAccounts } }, { redirectUrl });
|
|
138
146
|
setResult({ success: true, message: `Swapped! ${sig.slice(0, 8)}...` });
|
|
139
|
-
setAmount('');
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' });
|
|
143
|
-
} finally { setLoading(false); }
|
|
147
|
+
setAmount(''); setQuote(null);
|
|
148
|
+
} catch (e) { console.error('Swap error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' }); }
|
|
149
|
+
finally { setLoading(false); }
|
|
144
150
|
};
|
|
145
151
|
|
|
146
152
|
const handleSignMessage = async () => {
|
|
147
|
-
setSigning(true);
|
|
148
|
-
setResult(null);
|
|
153
|
+
setSigning(true); setResult(null);
|
|
149
154
|
try {
|
|
150
155
|
const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`;
|
|
151
156
|
const signature = await signMessage(message, { redirectUrl });
|
|
152
157
|
setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` });
|
|
153
|
-
} catch (e) {
|
|
154
|
-
|
|
155
|
-
} finally { setSigning(false); }
|
|
158
|
+
} catch (e) { console.error('Sign error:', e); setResult({ success: false, message: e instanceof Error ? e.message : 'Sign failed' }); }
|
|
159
|
+
finally { setSigning(false); }
|
|
156
160
|
};
|
|
157
161
|
|
|
158
162
|
const addr = wallet?.smartWallet || '';
|
|
@@ -161,152 +165,54 @@ export function Swap() {
|
|
|
161
165
|
<% if (styling === 'nativewind') { %>
|
|
162
166
|
<View className="flex-1 bg-white p-6 pt-16">
|
|
163
167
|
<View className="bg-neutral-900 rounded-2xl p-5">
|
|
164
|
-
<View className="flex-row items-center justify-between mb-6">
|
|
165
|
-
<Text className="text-lg font-semibold text-white">Swap</Text>
|
|
166
|
-
<TouchableOpacity onPress={() => disconnect()}>
|
|
167
|
-
<Text className="text-xs text-neutral-500">{addr.slice(0, 6)}...{addr.slice(-4)}</Text>
|
|
168
|
-
</TouchableOpacity>
|
|
169
|
-
</View>
|
|
170
|
-
|
|
168
|
+
<View className="flex-row items-center justify-between mb-6"><Text className="text-lg font-semibold text-white">Swap</Text><TouchableOpacity onPress={() => disconnect()}><Text className="text-xs text-neutral-500">{addr.slice(0, 6)}...{addr.slice(-4)}</Text></TouchableOpacity></View>
|
|
171
169
|
<View className="bg-neutral-800 border border-neutral-700 rounded-xl p-4">
|
|
172
|
-
<View className="flex-row justify-between mb-2">
|
|
173
|
-
|
|
174
|
-
<Text className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text>
|
|
175
|
-
</View>
|
|
176
|
-
<View className="flex-row items-center">
|
|
177
|
-
<TextInput className="flex-1 text-2xl font-medium text-white" placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" />
|
|
178
|
-
<TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('from')}>
|
|
179
|
-
<Text className="text-sm font-medium text-white mr-1">{fromToken.symbol}</Text><Text className="text-xs text-neutral-400">▼</Text>
|
|
180
|
-
</TouchableOpacity>
|
|
181
|
-
</View>
|
|
170
|
+
<View className="flex-row justify-between mb-2"><Text className="text-xs text-neutral-500">From</Text><Text className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text></View>
|
|
171
|
+
<View className="flex-row items-center"><TextInput className="flex-1 text-2xl font-medium text-white" placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" /><TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('from')}><Text className="text-sm font-medium text-white mr-1">{fromToken.symbol}</Text><Text className="text-xs text-neutral-400">▼</Text></TouchableOpacity></View>
|
|
182
172
|
<TouchableOpacity onPress={() => setAmount(String(balances[fromToken.symbol] ?? 0))}><Text className="text-xs text-neutral-500 mt-1">Max</Text></TouchableOpacity>
|
|
183
173
|
</View>
|
|
184
|
-
|
|
185
|
-
<View className="items-center my-2">
|
|
186
|
-
<TouchableOpacity onPress={switchTokens} className="w-8 h-8 bg-neutral-800 border border-neutral-700 rounded-lg items-center justify-center"><Text className="text-neutral-500">↓</Text></TouchableOpacity>
|
|
187
|
-
</View>
|
|
188
|
-
|
|
174
|
+
<View className="items-center my-2"><TouchableOpacity onPress={switchTokens} className="w-8 h-8 bg-neutral-800 border border-neutral-700 rounded-lg items-center justify-center"><Text className="text-neutral-500">↓</Text></TouchableOpacity></View>
|
|
189
175
|
<View className="bg-neutral-800 border border-neutral-700 rounded-xl p-4">
|
|
190
|
-
<View className="flex-row justify-between mb-2">
|
|
191
|
-
|
|
192
|
-
<Text className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text>
|
|
193
|
-
</View>
|
|
194
|
-
<View className="flex-row items-center">
|
|
195
|
-
<Text className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</Text>
|
|
196
|
-
<TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('to')}>
|
|
197
|
-
<Text className="text-sm font-medium text-white mr-1">{toToken.symbol}</Text><Text className="text-xs text-neutral-400">▼</Text>
|
|
198
|
-
</TouchableOpacity>
|
|
199
|
-
</View>
|
|
176
|
+
<View className="flex-row justify-between mb-2"><Text className="text-xs text-neutral-500">To</Text><Text className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text></View>
|
|
177
|
+
<View className="flex-row items-center"><Text className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</Text><TouchableOpacity className="flex-row items-center bg-neutral-700 px-3 py-2 rounded-lg" onPress={() => setShowPicker('to')}><Text className="text-sm font-medium text-white mr-1">{toToken.symbol}</Text><Text className="text-xs text-neutral-400">▼</Text></TouchableOpacity></View>
|
|
200
178
|
{quote && <Text className="text-xs text-neutral-500 mt-2">via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</Text>}
|
|
201
179
|
</View>
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
<View className="flex-row gap-1">
|
|
206
|
-
{SLIPPAGE_OPTIONS.map((s) => (
|
|
207
|
-
<TouchableOpacity key={s} onPress={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white' : 'bg-neutral-700'}`}>
|
|
208
|
-
<Text className={`text-xs ${slippage === s ? 'text-black' : 'text-neutral-400'}`}>{s / 100}%</Text>
|
|
209
|
-
</TouchableOpacity>
|
|
210
|
-
))}
|
|
211
|
-
</View>
|
|
212
|
-
</View>
|
|
213
|
-
|
|
214
|
-
<TouchableOpacity className={`mt-4 bg-white py-3.5 rounded-xl items-center ${loading || !quote ? 'opacity-50' : ''}`} onPress={handleSwap} disabled={loading || !quote}>
|
|
215
|
-
{loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text className="text-black text-sm font-medium">Swap</Text>}
|
|
216
|
-
</TouchableOpacity>
|
|
217
|
-
|
|
218
|
-
<TouchableOpacity className={`mt-2 border border-neutral-700 py-3 rounded-xl items-center ${signing ? 'opacity-50' : ''}`} onPress={handleSignMessage} disabled={signing}>
|
|
219
|
-
{signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text className="text-white text-sm font-medium">Sign Message</Text>}
|
|
220
|
-
</TouchableOpacity>
|
|
221
|
-
|
|
180
|
+
<View className="flex-row items-center justify-between mt-3"><Text className="text-xs text-neutral-500">Slippage</Text><View className="flex-row gap-1">{SLIPPAGE_OPTIONS.map((s) => (<TouchableOpacity key={s} onPress={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-white' : 'bg-neutral-700'}`}><Text className={`text-xs ${slippage === s ? 'text-black' : 'text-neutral-400'}`}>{s / 100}%</Text></TouchableOpacity>))}</View></View>
|
|
181
|
+
<TouchableOpacity className={`mt-4 bg-white py-3.5 rounded-xl items-center ${loading || !quote ? 'opacity-50' : ''}`} onPress={handleSwap} disabled={loading || !quote}>{loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text className="text-black text-sm font-medium">Swap</Text>}</TouchableOpacity>
|
|
182
|
+
<TouchableOpacity className={`mt-2 border border-neutral-700 py-3 rounded-xl items-center ${signing ? 'opacity-50' : ''}`} onPress={handleSignMessage} disabled={signing}>{signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text className="text-white text-sm font-medium">Sign Message</Text>}</TouchableOpacity>
|
|
222
183
|
{result && <Text className={`mt-3 text-sm text-center ${result.success ? 'text-neutral-400' : 'text-red-400'}`}>{result.message}</Text>}
|
|
223
184
|
<Text className="mt-4 text-xs text-center text-neutral-500">Gas sponsored · Powered by LazorKit + Jupiter</Text>
|
|
224
185
|
</View>
|
|
225
|
-
|
|
226
186
|
<Modal visible={!!showPicker} transparent animationType="slide" onRequestClose={() => setShowPicker(null)}>
|
|
227
187
|
<TouchableOpacity className="flex-1 justify-end bg-black/50" activeOpacity={1} onPress={() => setShowPicker(null)}>
|
|
228
|
-
<View className="bg-neutral-800 rounded-t-2xl p-4 pb-8">
|
|
229
|
-
<Text className="text-base font-semibold text-center mb-4 text-white">Select Token</Text>
|
|
230
|
-
{TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (
|
|
231
|
-
<TouchableOpacity key={t.symbol} className="py-3.5 border-b border-neutral-700" onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}>
|
|
232
|
-
<Text className="text-base text-white">{t.symbol}</Text>
|
|
233
|
-
</TouchableOpacity>
|
|
234
|
-
))}
|
|
235
|
-
</View>
|
|
188
|
+
<View className="bg-neutral-800 rounded-t-2xl p-4 pb-8"><Text className="text-base font-semibold text-center mb-4 text-white">Select Token</Text>{TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (<TouchableOpacity key={t.symbol} className="py-3.5 border-b border-neutral-700" onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}><Text className="text-base text-white">{t.symbol}</Text></TouchableOpacity>))}</View>
|
|
236
189
|
</TouchableOpacity>
|
|
237
190
|
</Modal>
|
|
238
191
|
</View>
|
|
239
192
|
<% } else { %>
|
|
240
193
|
<View style={styles.container}>
|
|
241
194
|
<View style={styles.card}>
|
|
242
|
-
<View style={styles.header}>
|
|
243
|
-
<Text style={styles.title}>Swap</Text>
|
|
244
|
-
<TouchableOpacity onPress={() => disconnect()}><Text style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</Text></TouchableOpacity>
|
|
245
|
-
</View>
|
|
246
|
-
|
|
195
|
+
<View style={styles.header}><Text style={styles.title}>Swap</Text><TouchableOpacity onPress={() => disconnect()}><Text style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</Text></TouchableOpacity></View>
|
|
247
196
|
<View style={styles.inputCard}>
|
|
248
|
-
<View style={styles.labelRow}>
|
|
249
|
-
|
|
250
|
-
<Text style={styles.label}>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text>
|
|
251
|
-
</View>
|
|
252
|
-
<View style={styles.inputRow}>
|
|
253
|
-
<TextInput style={styles.input} placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" />
|
|
254
|
-
<TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('from')}>
|
|
255
|
-
<Text style={styles.tokenText}>{fromToken.symbol}</Text><Text style={styles.tokenArrow}>▼</Text>
|
|
256
|
-
</TouchableOpacity>
|
|
257
|
-
</View>
|
|
197
|
+
<View style={styles.labelRow}><Text style={styles.label}>From</Text><Text style={styles.label}>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</Text></View>
|
|
198
|
+
<View style={styles.inputRow}><TextInput style={styles.input} placeholder="0.00" placeholderTextColor="#737373" value={amount} onChangeText={setAmount} keyboardType="numeric" /><TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('from')}><Text style={styles.tokenText}>{fromToken.symbol}</Text><Text style={styles.tokenArrow}>▼</Text></TouchableOpacity></View>
|
|
258
199
|
<TouchableOpacity onPress={() => setAmount(String(balances[fromToken.symbol] ?? 0))}><Text style={styles.switchText}>Max</Text></TouchableOpacity>
|
|
259
200
|
</View>
|
|
260
|
-
|
|
261
201
|
<View style={styles.arrow}><TouchableOpacity onPress={switchTokens} style={styles.arrowBox}><Text style={styles.arrowText}>↓</Text></TouchableOpacity></View>
|
|
262
|
-
|
|
263
202
|
<View style={styles.inputCard}>
|
|
264
|
-
<View style={styles.labelRow}>
|
|
265
|
-
|
|
266
|
-
<Text style={styles.label}>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text>
|
|
267
|
-
</View>
|
|
268
|
-
<View style={styles.inputRow}>
|
|
269
|
-
<Text style={styles.outputText}>{outputAmount}</Text>
|
|
270
|
-
<TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('to')}>
|
|
271
|
-
<Text style={styles.tokenText}>{toToken.symbol}</Text><Text style={styles.tokenArrow}>▼</Text>
|
|
272
|
-
</TouchableOpacity>
|
|
273
|
-
</View>
|
|
203
|
+
<View style={styles.labelRow}><Text style={styles.label}>To</Text><Text style={styles.label}>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</Text></View>
|
|
204
|
+
<View style={styles.inputRow}><Text style={styles.outputText}>{outputAmount}</Text><TouchableOpacity style={styles.tokenBtn} onPress={() => setShowPicker('to')}><Text style={styles.tokenText}>{toToken.symbol}</Text><Text style={styles.tokenArrow}>▼</Text></TouchableOpacity></View>
|
|
274
205
|
{quote && <Text style={styles.quote}>via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</Text>}
|
|
275
206
|
</View>
|
|
276
|
-
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
<View style={{ flexDirection: 'row', gap: 4 }}>
|
|
280
|
-
{SLIPPAGE_OPTIONS.map((s) => (
|
|
281
|
-
<TouchableOpacity key={s} onPress={() => setSlippage(s)} style={[styles.slippageBtn, slippage === s && styles.slippageBtnActive]}>
|
|
282
|
-
<Text style={[styles.slippageBtnText, slippage === s && styles.slippageBtnTextActive]}>{s / 100}%</Text>
|
|
283
|
-
</TouchableOpacity>
|
|
284
|
-
))}
|
|
285
|
-
</View>
|
|
286
|
-
</View>
|
|
287
|
-
|
|
288
|
-
<TouchableOpacity style={[styles.button, (loading || !quote) && styles.buttonDisabled]} onPress={handleSwap} disabled={loading || !quote}>
|
|
289
|
-
{loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text style={styles.buttonText}>Swap</Text>}
|
|
290
|
-
</TouchableOpacity>
|
|
291
|
-
|
|
292
|
-
<TouchableOpacity style={[styles.buttonSecondary, signing && styles.buttonDisabled]} onPress={handleSignMessage} disabled={signing}>
|
|
293
|
-
{signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text style={styles.buttonTextSecondary}>Sign Message</Text>}
|
|
294
|
-
</TouchableOpacity>
|
|
295
|
-
|
|
207
|
+
<View style={styles.slippageRow}><Text style={styles.slippageLabel}>Slippage</Text><View style={{ flexDirection: 'row', gap: 4 }}>{SLIPPAGE_OPTIONS.map((s) => (<TouchableOpacity key={s} onPress={() => setSlippage(s)} style={[styles.slippageBtn, slippage === s && styles.slippageBtnActive]}><Text style={[styles.slippageBtnText, slippage === s && styles.slippageBtnTextActive]}>{s / 100}%</Text></TouchableOpacity>))}</View></View>
|
|
208
|
+
<TouchableOpacity style={[styles.button, (loading || !quote) && styles.buttonDisabled]} onPress={handleSwap} disabled={loading || !quote}>{loading ? <ActivityIndicator color="#0a0a0a" size="small" /> : <Text style={styles.buttonText}>Swap</Text>}</TouchableOpacity>
|
|
209
|
+
<TouchableOpacity style={[styles.buttonSecondary, signing && styles.buttonDisabled]} onPress={handleSignMessage} disabled={signing}>{signing ? <ActivityIndicator color="#fafafa" size="small" /> : <Text style={styles.buttonTextSecondary}>Sign Message</Text>}</TouchableOpacity>
|
|
296
210
|
{result && <Text style={[styles.result, { color: result.success ? '#737373' : '#ef4444' }]}>{result.message}</Text>}
|
|
297
211
|
<Text style={styles.footer}>Gas sponsored · Powered by LazorKit + Jupiter</Text>
|
|
298
212
|
</View>
|
|
299
|
-
|
|
300
213
|
<Modal visible={!!showPicker} transparent animationType="slide" onRequestClose={() => setShowPicker(null)}>
|
|
301
214
|
<TouchableOpacity style={styles.modal} activeOpacity={1} onPress={() => setShowPicker(null)}>
|
|
302
|
-
<View style={styles.modalContent}>
|
|
303
|
-
<Text style={styles.modalTitle}>Select Token</Text>
|
|
304
|
-
{TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (
|
|
305
|
-
<TouchableOpacity key={t.symbol} style={styles.modalItem} onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}>
|
|
306
|
-
<Text style={styles.modalItemText}>{t.symbol}</Text>
|
|
307
|
-
</TouchableOpacity>
|
|
308
|
-
))}
|
|
309
|
-
</View>
|
|
215
|
+
<View style={styles.modalContent}><Text style={styles.modalTitle}>Select Token</Text>{TOKENS.filter(t => t.symbol !== (showPicker === 'from' ? toToken.symbol : fromToken.symbol)).map(t => (<TouchableOpacity key={t.symbol} style={styles.modalItem} onPress={() => { showPicker === 'from' ? setFromToken(t) : setToToken(t); setShowPicker(null); }}><Text style={styles.modalItemText}>{t.symbol}</Text></TouchableOpacity>))}</View>
|
|
310
216
|
</TouchableOpacity>
|
|
311
217
|
</Modal>
|
|
312
218
|
</View>
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
2
2
|
import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux";
|
|
3
|
-
import type { LazorWallet } from "../lazorkit";
|
|
4
3
|
|
|
5
4
|
interface AppState {
|
|
6
|
-
wallet: LazorWallet | null;
|
|
7
5
|
isLoading: boolean;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
const appSlice = createSlice({
|
|
11
9
|
name: "app",
|
|
12
|
-
initialState: {
|
|
10
|
+
initialState: { isLoading: false } as AppState,
|
|
13
11
|
reducers: {
|
|
14
|
-
setWallet: (state, action: PayloadAction<LazorWallet | null>) => {
|
|
15
|
-
state.wallet = action.payload;
|
|
16
|
-
},
|
|
17
12
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
|
18
13
|
state.isLoading = action.payload;
|
|
19
14
|
},
|
|
20
15
|
},
|
|
21
16
|
});
|
|
22
17
|
|
|
23
|
-
export const {
|
|
18
|
+
export const { setLoading } = appSlice.actions;
|
|
24
19
|
|
|
25
20
|
export const store = configureStore({ reducer: { app: appSlice.reducer } });
|
|
26
21
|
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
-
import type { LazorWallet } from "../lazorkit";
|
|
3
2
|
|
|
4
3
|
interface AppState {
|
|
5
|
-
wallet: LazorWallet | null;
|
|
6
|
-
setWallet: (wallet: LazorWallet | null) => void;
|
|
7
4
|
isLoading: boolean;
|
|
8
5
|
setLoading: (loading: boolean) => void;
|
|
9
6
|
}
|
|
10
7
|
|
|
11
8
|
export const useStore = create<AppState>((set) => ({
|
|
12
|
-
wallet: null,
|
|
13
|
-
setWallet: (wallet) => set({ wallet }),
|
|
14
9
|
isLoading: false,
|
|
15
10
|
setLoading: (isLoading) => set({ isLoading }),
|
|
16
11
|
}));
|
|
@@ -3,18 +3,18 @@ import { useWallet } from "@lazorkit/wallet";
|
|
|
3
3
|
import { Connection, PublicKey } from "@solana/web3.js";
|
|
4
4
|
<% if (styling !== 'tailwind') { %>
|
|
5
5
|
const styles: Record<string, React.CSSProperties> = {
|
|
6
|
-
container: { width: '100%', maxWidth: 360 },
|
|
6
|
+
container: { width: '100%', maxWidth: 360, background: '#0a0a0a', borderRadius: 16, padding: 24, color: '#fafafa' },
|
|
7
7
|
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
|
|
8
|
-
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
8
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0, color: '#fafafa' },
|
|
9
9
|
list: { display: 'flex', flexDirection: 'column', gap: 8 },
|
|
10
|
-
item: { border: '1px solid #
|
|
10
|
+
item: { border: '1px solid #262626', borderRadius: 12, padding: 12, textDecoration: 'none', color: '#fafafa', background: '#171717' },
|
|
11
11
|
row: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
|
|
12
|
-
sig: { fontFamily: 'monospace', fontSize: 13, color: '#
|
|
13
|
-
time: { fontSize: 12, color: '#
|
|
12
|
+
sig: { fontFamily: 'monospace', fontSize: 13, color: '#fafafa' },
|
|
13
|
+
time: { fontSize: 12, color: '#737373', marginTop: 4 },
|
|
14
14
|
status: { fontSize: 11, padding: '2px 6px', borderRadius: 4 },
|
|
15
|
-
success: { backgroundColor: '#
|
|
16
|
-
failed: { backgroundColor: '#
|
|
17
|
-
empty: { textAlign: 'center', padding: 32, color: '#
|
|
15
|
+
success: { backgroundColor: '#14532d', color: '#4ade80' },
|
|
16
|
+
failed: { backgroundColor: '#7f1d1d', color: '#f87171' },
|
|
17
|
+
empty: { textAlign: 'center', padding: 32, color: '#737373', fontSize: 14 },
|
|
18
18
|
back: { fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
19
19
|
};
|
|
20
20
|
<% } %>
|
|
@@ -25,7 +25,7 @@ interface Props {
|
|
|
25
25
|
interface TxInfo {
|
|
26
26
|
signature: string;
|
|
27
27
|
slot: number;
|
|
28
|
-
blockTime: number | null;
|
|
28
|
+
blockTime: number | null | undefined;
|
|
29
29
|
err: any;
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -56,26 +56,26 @@ export function History({ onBack }: Props) {
|
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
58
|
<% if (styling === 'tailwind') { %>
|
|
59
|
-
<div className="w-full max-w-sm">
|
|
59
|
+
<div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
|
|
60
60
|
<div className="flex items-center justify-between mb-4">
|
|
61
61
|
<h1 className="text-lg font-semibold">History</h1>
|
|
62
|
-
<button onClick={onBack} className="text-sm text-neutral-500 hover:text-
|
|
62
|
+
<button onClick={onBack} className="text-sm text-neutral-500 hover:text-white">← Back</button>
|
|
63
63
|
</div>
|
|
64
64
|
{loading ? (
|
|
65
|
-
<p className="text-center py-8 text-neutral-
|
|
65
|
+
<p className="text-center py-8 text-neutral-500">Loading...</p>
|
|
66
66
|
) : txs.length === 0 ? (
|
|
67
|
-
<p className="text-center py-8 text-neutral-
|
|
67
|
+
<p className="text-center py-8 text-neutral-500">No transactions yet</p>
|
|
68
68
|
) : (
|
|
69
69
|
<div className="space-y-2">
|
|
70
70
|
{txs.map((tx) => (
|
|
71
|
-
<a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" className="block border border-neutral-
|
|
71
|
+
<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">
|
|
72
72
|
<div className="flex justify-between items-center">
|
|
73
73
|
<span className="font-mono text-sm">{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</span>
|
|
74
|
-
<span className={`text-xs px-2 py-0.5 rounded ${tx.err ? "bg-red-
|
|
74
|
+
<span className={`text-xs px-2 py-0.5 rounded ${tx.err ? "bg-red-900 text-red-400" : "bg-green-900 text-green-400"}`}>
|
|
75
75
|
{tx.err ? "Failed" : "Success"}
|
|
76
76
|
</span>
|
|
77
77
|
</div>
|
|
78
|
-
<p className="text-xs text-neutral-
|
|
78
|
+
<p className="text-xs text-neutral-500 mt-1">{formatTime(tx.blockTime)}</p>
|
|
79
79
|
</a>
|
|
80
80
|
))}
|
|
81
81
|
</div>
|
|
@@ -2,15 +2,15 @@ import { useState } from "react";
|
|
|
2
2
|
import { useWallet } from "@lazorkit/wallet";
|
|
3
3
|
<% if (styling !== 'tailwind') { %>
|
|
4
4
|
const styles: Record<string, React.CSSProperties> = {
|
|
5
|
-
container: { width: '100%', maxWidth: 360 },
|
|
6
|
-
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
5
|
+
container: { width: '100%', maxWidth: 360, background: '#0a0a0a', borderRadius: 16, padding: 24, color: '#fafafa' },
|
|
6
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0, color: '#fafafa' },
|
|
7
7
|
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', lineHeight: 1.5 },
|
|
8
|
-
card: { marginTop: 16, border: '1px solid #
|
|
9
|
-
cardTitle: { fontSize: 14, fontWeight: 500, margin: 0 },
|
|
8
|
+
card: { marginTop: 16, border: '1px solid #262626', borderRadius: 12, padding: 16, background: '#171717' },
|
|
9
|
+
cardTitle: { fontSize: 14, fontWeight: 500, margin: 0, color: '#fafafa' },
|
|
10
10
|
cardDesc: { marginTop: 4, fontSize: 13, color: '#737373' },
|
|
11
|
-
button: { marginTop: 12, width: '100%', backgroundColor: '#
|
|
12
|
-
buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#
|
|
13
|
-
info: { marginTop: 16, padding: 12, backgroundColor: '#
|
|
11
|
+
button: { marginTop: 12, width: '100%', backgroundColor: '#fafafa', color: '#0a0a0a', padding: '12px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
12
|
+
buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#fafafa', padding: '12px 16px', borderRadius: 10, border: '1px solid #262626', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
13
|
+
info: { marginTop: 16, padding: 12, backgroundColor: '#171717', borderRadius: 8, fontSize: 12, color: '#a3a3a3' },
|
|
14
14
|
back: { marginTop: 16, fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
15
15
|
};
|
|
16
16
|
<% } %>
|
|
@@ -24,11 +24,17 @@ export function Recovery({ onBack }: Props) {
|
|
|
24
24
|
const portalUrl = import.meta.env.VITE_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
|
|
25
25
|
|
|
26
26
|
const handleAddDevice = () => {
|
|
27
|
-
|
|
27
|
+
const url = wallet?.smartWallet
|
|
28
|
+
? `${portalUrl}?wallet=${wallet.smartWallet}&action=add-device`
|
|
29
|
+
: portalUrl;
|
|
30
|
+
window.open(url, "_blank");
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
const handleManageDevices = () => {
|
|
31
|
-
|
|
34
|
+
const url = wallet?.smartWallet
|
|
35
|
+
? `${portalUrl}?wallet=${wallet.smartWallet}&action=manage-devices`
|
|
36
|
+
: portalUrl;
|
|
37
|
+
window.open(url, "_blank");
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
const copyAddress = () => {
|
|
@@ -41,36 +47,36 @@ export function Recovery({ onBack }: Props) {
|
|
|
41
47
|
|
|
42
48
|
return (
|
|
43
49
|
<% if (styling === 'tailwind') { %>
|
|
44
|
-
<div className="w-full max-w-sm">
|
|
50
|
+
<div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
|
|
45
51
|
<h1 className="text-lg font-semibold">Recovery & Backup</h1>
|
|
46
52
|
<p className="mt-2 text-sm text-neutral-500">
|
|
47
53
|
Add backup passkeys from other devices to ensure you never lose access.
|
|
48
54
|
</p>
|
|
49
|
-
<div className="mt-4 border border-neutral-
|
|
55
|
+
<div className="mt-4 border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
50
56
|
<h3 className="text-sm font-medium">Add Backup Device</h3>
|
|
51
57
|
<p className="mt-1 text-xs text-neutral-500">
|
|
52
58
|
Register a passkey from another phone, tablet, or computer.
|
|
53
59
|
</p>
|
|
54
|
-
<button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-
|
|
60
|
+
<button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-white text-black text-sm font-medium rounded-lg hover:opacity-90">
|
|
55
61
|
Add Device
|
|
56
62
|
</button>
|
|
57
63
|
</div>
|
|
58
|
-
<div className="mt-3 border border-neutral-
|
|
64
|
+
<div className="mt-3 border border-neutral-700 rounded-xl p-4 bg-neutral-800">
|
|
59
65
|
<h3 className="text-sm font-medium">Manage Devices</h3>
|
|
60
66
|
<p className="mt-1 text-xs text-neutral-500">
|
|
61
67
|
View and remove registered passkeys.
|
|
62
68
|
</p>
|
|
63
|
-
<button onClick={handleManageDevices} className="mt-3 w-full py-3 bg-transparent text-
|
|
69
|
+
<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">
|
|
64
70
|
View Devices
|
|
65
71
|
</button>
|
|
66
72
|
</div>
|
|
67
|
-
<div className="mt-4 p-3 bg-neutral-
|
|
68
|
-
<p className="text-xs text-neutral-
|
|
73
|
+
<div className="mt-4 p-3 bg-neutral-800 rounded-lg">
|
|
74
|
+
<p className="text-xs text-neutral-400">
|
|
69
75
|
<strong>Wallet:</strong>{" "}
|
|
70
|
-
<button onClick={copyAddress} className="font-mono hover:text-
|
|
76
|
+
<button onClick={copyAddress} className="font-mono hover:text-white">
|
|
71
77
|
{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}
|
|
72
78
|
</button>
|
|
73
|
-
{copied && <span className="ml-2 text-green-
|
|
79
|
+
{copied && <span className="ml-2 text-green-400">Copied!</span>}
|
|
74
80
|
</p>
|
|
75
81
|
</div>
|
|
76
82
|
<button onClick={onBack} className="mt-4 text-sm text-neutral-500 hover:text-black">
|