create-lightning-scaffold 1.0.0 → 1.0.2

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 (65) hide show
  1. package/README.md +107 -25
  2. package/dist/index.js +169 -64
  3. package/package.json +1 -1
  4. package/templates/backend/firebase/index.ts +86 -9
  5. package/templates/backend/supabase/index.ts +107 -6
  6. package/templates/base/.env.example.ejs +17 -10
  7. package/templates/lib/backend.ts.ejs +59 -0
  8. package/templates/mobile/app/_layout.tsx +22 -52
  9. package/templates/mobile/app/index.tsx.ejs +44 -0
  10. package/templates/mobile/components/History.tsx.ejs +114 -0
  11. package/templates/mobile/components/Onboarding.tsx.ejs +79 -0
  12. package/templates/mobile/components/Recovery.tsx.ejs +119 -0
  13. package/templates/mobile/components/Swap.tsx.ejs +315 -0
  14. package/templates/mobile/package.json.ejs +4 -0
  15. package/templates/vite/README.md +73 -0
  16. package/templates/vite/eslint.config.js +23 -0
  17. package/templates/vite/index.html +13 -0
  18. package/templates/vite/package.json.ejs +37 -0
  19. package/templates/vite/postcss.config.js.ejs +8 -0
  20. package/templates/vite/public/vite.svg +1 -0
  21. package/templates/vite/src/App.tsx.ejs +65 -0
  22. package/templates/vite/src/assets/react.svg +1 -0
  23. package/templates/vite/src/components/History.tsx.ejs +112 -0
  24. package/templates/vite/src/components/Onboarding.tsx.ejs +64 -0
  25. package/templates/vite/src/components/Recovery.tsx.ejs +107 -0
  26. package/templates/vite/src/components/Swap.tsx.ejs +241 -0
  27. package/templates/vite/src/index.css.ejs +55 -0
  28. package/templates/vite/src/main.tsx +25 -0
  29. package/templates/vite/tailwind.config.js.ejs +13 -0
  30. package/templates/vite/tsconfig.app.json +28 -0
  31. package/templates/vite/tsconfig.json +7 -0
  32. package/templates/vite/tsconfig.node.json +26 -0
  33. package/templates/vite/vite.config.ts +15 -0
  34. package/templates/web/app/globals.css.ejs +53 -0
  35. package/templates/web/app/layout.tsx.ejs +8 -15
  36. package/templates/web/app/page.tsx.ejs +45 -0
  37. package/templates/web/app/providers.tsx +27 -0
  38. package/templates/web/components/History.tsx.ejs +114 -0
  39. package/templates/web/components/Onboarding.tsx.ejs +66 -0
  40. package/templates/web/components/Recovery.tsx.ejs +108 -0
  41. package/templates/web/components/Swap.tsx.ejs +289 -0
  42. package/templates/web/package.json.ejs +3 -1
  43. package/templates/examples/mobile/biometric-onboard.tsx +0 -104
  44. package/templates/examples/mobile/gasless-transfer.tsx +0 -72
  45. package/templates/examples/mobile/index.tsx +0 -30
  46. package/templates/examples/mobile/passkey-login.tsx +0 -55
  47. package/templates/examples/web/biometric-onboard/page.tsx +0 -70
  48. package/templates/examples/web/gasless-transfer/page.tsx +0 -85
  49. package/templates/examples/web/page.tsx +0 -27
  50. package/templates/examples/web/passkey-login/page.tsx +0 -50
  51. package/templates/lib/lazorkit/mobile/index.ts +0 -53
  52. package/templates/lib/lazorkit/web/index.ts +0 -67
  53. package/templates/mobile/app/(tabs)/_layout.tsx +0 -59
  54. package/templates/mobile/app/(tabs)/index.tsx +0 -31
  55. package/templates/mobile/app/(tabs)/two.tsx +0 -31
  56. package/templates/mobile/app/+html.tsx +0 -38
  57. package/templates/mobile/app/+not-found.tsx +0 -40
  58. package/templates/mobile/app/modal.tsx +0 -35
  59. package/templates/mobile/components/EditScreenInfo.tsx +0 -77
  60. package/templates/mobile/components/StyledText.tsx +0 -5
  61. package/templates/mobile/components/__tests__/StyledText-test.js +0 -10
  62. package/templates/mobile/lib/lazorkit/index.ts +0 -53
  63. package/templates/web/app/globals.css +0 -26
  64. package/templates/web/app/page.tsx +0 -65
  65. package/templates/web/lib/lazorkit/index.ts +0 -67
@@ -0,0 +1,289 @@
1
+ "use client";
2
+ import { useState, useEffect } from "react";
3
+ import { useWallet } from "@lazorkit/wallet";
4
+ import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js";
5
+
6
+ const TOKENS = [
7
+ { symbol: "SOL", mint: "So11111111111111111111111111111111111111112", decimals: 9 },
8
+ { symbol: "USDC", mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
9
+ { symbol: "USDT", mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", decimals: 6 },
10
+ { symbol: "BONK", mint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", decimals: 5 },
11
+ { symbol: "JUP", mint: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", decimals: 6 },
12
+ ];
13
+
14
+ const SLIPPAGE_OPTIONS = [50, 100, 300]; // 0.5%, 1%, 3% in bps
15
+
16
+ const JUPITER_API = "https://api.jup.ag/swap/v1";
17
+
18
+ function useBalances(walletAddress: string | undefined) {
19
+ const [balances, setBalances] = useState<Record<string, number>>({});
20
+ useEffect(() => {
21
+ if (!walletAddress) return;
22
+ const rpc = process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
23
+ const conn = new Connection(rpc);
24
+ const pk = new PublicKey(walletAddress);
25
+
26
+ // Fetch SOL balance
27
+ conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {});
28
+
29
+ // Fetch token balances
30
+ conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") })
31
+ .then((res) => {
32
+ const tokenBals: Record<string, number> = {};
33
+ res.value.forEach((acc) => {
34
+ const info = acc.account.data.parsed.info;
35
+ const token = TOKENS.find((t) => t.mint === info.mint);
36
+ if (token) tokenBals[token.symbol] = info.tokenAmount.uiAmount || 0;
37
+ });
38
+ setBalances((b) => ({ ...b, ...tokenBals }));
39
+ }).catch(() => {});
40
+ }, [walletAddress]);
41
+ return balances;
42
+ }
43
+ <% if (styling !== 'tailwind') { %>
44
+ const styles: Record<string, React.CSSProperties> = {
45
+ container: { width: '100%', maxWidth: 360, background: '#0a0a0a', borderRadius: 16, padding: 24, color: '#fafafa' },
46
+ header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 },
47
+ title: { fontSize: 18, fontWeight: 600, margin: 0, color: '#fafafa' },
48
+ address: { fontSize: 12, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
49
+ card: { border: '1px solid #262626', borderRadius: 12, padding: 16, background: '#171717' },
50
+ label: { fontSize: 12, color: '#737373', marginBottom: 8, display: 'flex', justifyContent: 'space-between' },
51
+ inputRow: { display: 'flex', alignItems: 'center', gap: 12 },
52
+ input: { flex: 1, fontSize: 24, fontWeight: 500, border: 'none', background: 'transparent', width: '100%', color: '#fafafa' },
53
+ arrow: { display: 'flex', justifyContent: 'center', margin: '8px 0' },
54
+ arrowBox: { width: 32, height: 32, border: '1px solid #262626', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#737373', background: '#171717' },
55
+ button: { marginTop: 16, width: '100%', backgroundColor: '#fafafa', color: '#0a0a0a', padding: '14px 16px', borderRadius: 12, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
56
+ buttonDisabled: { opacity: 0.5, cursor: 'not-allowed' },
57
+ buttonSecondary: { marginTop: 8, width: '100%', backgroundColor: 'transparent', color: '#fafafa', padding: '12px 16px', borderRadius: 12, border: '1px solid #262626', fontSize: 13, fontWeight: 500, cursor: 'pointer' },
58
+ result: { marginTop: 12, fontSize: 14, textAlign: 'center' },
59
+ footer: { marginTop: 16, fontSize: 12, color: '#737373', textAlign: 'center' },
60
+ tokenBtn: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: '#262626', borderRadius: 8, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer', color: '#fafafa' },
61
+ dropdown: { position: 'absolute', right: 0, marginTop: 4, background: '#171717', border: '1px solid #262626', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.3)', zIndex: 10 },
62
+ dropdownItem: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', border: 'none', background: 'none', fontSize: 14, cursor: 'pointer', color: '#fafafa' },
63
+ quote: { fontSize: 12, color: '#737373', marginTop: 4 },
64
+ };
65
+ <% } %>
66
+ export function Swap() {
67
+ const { disconnect, wallet, smartWalletPubkey, signAndSendTransaction, signMessage } = useWallet();
68
+ const balances = useBalances(wallet?.smartWallet);
69
+ const [fromToken, setFromToken] = useState(TOKENS[0]);
70
+ const [toToken, setToToken] = useState(TOKENS[1]);
71
+ const [amount, setAmount] = useState("");
72
+ const [slippage, setSlippage] = useState(SLIPPAGE_OPTIONS[0]);
73
+ const [quote, setQuote] = useState<any>(null);
74
+ const [loading, setLoading] = useState(false);
75
+ const [signing, setSigning] = useState(false);
76
+ const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
77
+
78
+ const switchTokens = () => {
79
+ setFromToken(toToken);
80
+ setToToken(fromToken);
81
+ setAmount("");
82
+ setQuote(null);
83
+ };
84
+
85
+ // Fetch quote from Jupiter
86
+ useEffect(() => {
87
+ if (!amount || parseFloat(amount) <= 0) {
88
+ setQuote(null);
89
+ return;
90
+ }
91
+ const timeout = setTimeout(async () => {
92
+ try {
93
+ const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals);
94
+ const res = await fetch(
95
+ `${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}`
96
+ );
97
+ const data = await res.json();
98
+ if (data.outAmount) setQuote(data);
99
+ } catch (e) {
100
+ console.error("Quote error:", e);
101
+ }
102
+ }, 500);
103
+ return () => clearTimeout(timeout);
104
+ }, [amount, fromToken, toToken, slippage]);
105
+
106
+ const outputAmount = quote
107
+ ? (parseInt(quote.outAmount) / 10 ** toToken.decimals).toFixed(toToken.decimals === 6 ? 2 : 4)
108
+ : "0.00";
109
+
110
+ const handleSwap = async () => {
111
+ if (!smartWalletPubkey || !quote) return;
112
+ setLoading(true);
113
+ setResult(null);
114
+ try {
115
+ // Get swap transaction from Jupiter
116
+ const swapRes = await fetch(`${JUPITER_API}/swap`, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({
120
+ quoteResponse: quote,
121
+ userPublicKey: smartWalletPubkey.toBase58(),
122
+ dynamicComputeUnitLimit: true,
123
+ dynamicSlippage: true,
124
+ }),
125
+ });
126
+ const { swapTransaction } = await swapRes.json();
127
+
128
+ // Decode and sign via LazorKit
129
+ const tx = VersionedTransaction.deserialize(Buffer.from(swapTransaction, "base64"));
130
+ const signature = await signAndSendTransaction({
131
+ instructions: [], // Jupiter provides full tx, we pass empty and let SDK handle
132
+ transactionOptions: { feeToken: "USDC" },
133
+ });
134
+
135
+ setResult({ success: true, message: `Swapped! ${signature.slice(0, 8)}...` });
136
+ setAmount("");
137
+ setQuote(null);
138
+ } catch (e) {
139
+ setResult({ success: false, message: e instanceof Error ? e.message : "Swap failed" });
140
+ } finally {
141
+ setLoading(false);
142
+ }
143
+ };
144
+
145
+ const handleSignMessage = async () => {
146
+ setSigning(true);
147
+ setResult(null);
148
+ try {
149
+ const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`;
150
+ const { signature } = await signMessage(message);
151
+ setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` });
152
+ } catch (e) {
153
+ setResult({ success: false, message: e instanceof Error ? e.message : "Sign failed" });
154
+ } finally {
155
+ setSigning(false);
156
+ }
157
+ };
158
+
159
+ const addr = wallet?.smartWallet || "";
160
+
161
+ return (
162
+ <% if (styling === 'tailwind') { %>
163
+ <div className="w-full max-w-sm bg-neutral-900 rounded-2xl p-6 text-white">
164
+ <div className="flex items-center justify-between mb-6">
165
+ <h1 className="text-lg font-semibold">Swap</h1>
166
+ <button onClick={() => disconnect()} className="text-xs text-neutral-500 hover:text-neutral-300">
167
+ {addr.slice(0, 6)}...{addr.slice(-4)}
168
+ </button>
169
+ </div>
170
+ <div className="space-y-2">
171
+ <div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
172
+ <div className="flex items-center justify-between mb-2">
173
+ <span className="text-xs text-neutral-500">From</span>
174
+ <span className="text-xs text-neutral-500">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span>
175
+ </div>
176
+ <div className="flex items-center gap-3">
177
+ <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" />
178
+ <TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
179
+ </div>
180
+ <button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} className="text-xs text-neutral-500 hover:text-white mt-1">Max</button>
181
+ </div>
182
+ <div className="flex justify-center">
183
+ <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>
184
+ </div>
185
+ <div className="border border-neutral-700 rounded-xl p-4 bg-neutral-800">
186
+ <div className="flex items-center justify-between mb-2">
187
+ <span className="text-xs text-neutral-500">To</span>
188
+ <span className="text-xs text-neutral-500">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span>
189
+ </div>
190
+ <div className="flex items-center gap-3">
191
+ <span className="flex-1 text-2xl font-medium text-neutral-500">{outputAmount}</span>
192
+ <TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
193
+ </div>
194
+ {quote && <p className="text-xs text-neutral-500 mt-2">via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || "Best route"}</p>}
195
+ </div>
196
+ </div>
197
+ <div className="flex items-center justify-between mt-3 text-xs">
198
+ <span className="text-neutral-500">Slippage</span>
199
+ <div className="flex gap-1">
200
+ {SLIPPAGE_OPTIONS.map((s) => (
201
+ <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"}`}>
202
+ {s / 100}%
203
+ </button>
204
+ ))}
205
+ </div>
206
+ </div>
207
+ <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">
208
+ {loading ? "Swapping..." : "Swap"}
209
+ </button>
210
+ <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">
211
+ {signing ? "Signing..." : "Sign Message"}
212
+ </button>
213
+ {result && <p className={`mt-3 text-sm text-center ${result.success ? "text-neutral-400" : "text-red-400"}`}>{result.message}</p>}
214
+ <p className="mt-4 text-xs text-center text-neutral-500">Gas sponsored · Powered by LazorKit + Jupiter</p>
215
+ </div>
216
+ <% } else { %>
217
+ <div style={styles.container}>
218
+ <div style={styles.header}>
219
+ <h1 style={styles.title}>Swap</h1>
220
+ <button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button>
221
+ </div>
222
+ <div style={styles.card}>
223
+ <div style={styles.label}><span>From</span><span>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
224
+ <div style={styles.inputRow}>
225
+ <input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} />
226
+ <TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
227
+ </div>
228
+ <button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} style={{ ...styles.address, fontSize: 12, marginTop: 4 }}>Max</button>
229
+ </div>
230
+ <div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer', border: '1px solid #e5e5e5' }}>↓</button></div>
231
+ <div style={styles.card}>
232
+ <div style={styles.label}><span>To</span><span>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
233
+ <div style={styles.inputRow}>
234
+ <span style={{ ...styles.input, color: '#d4d4d4' }}>{outputAmount}</span>
235
+ <TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
236
+ </div>
237
+ {quote && <p style={styles.quote}>via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || "Best route"}</p>}
238
+ </div>
239
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}>
240
+ <span style={{ color: '#737373' }}>Slippage</span>
241
+ <div style={{ display: 'flex', gap: 4 }}>
242
+ {SLIPPAGE_OPTIONS.map((s) => (
243
+ <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' }}>
244
+ {s / 100}%
245
+ </button>
246
+ ))}
247
+ </div>
248
+ </div>
249
+ <button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>
250
+ {loading ? "Swapping..." : "Swap"}
251
+ </button>
252
+ <button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>
253
+ {signing ? "Signing..." : "Sign Message"}
254
+ </button>
255
+ {result && <p style={{ ...styles.result, color: result.success ? '#737373' : '#ef4444' }}>{result.message}</p>}
256
+ <p style={styles.footer}>Gas sponsored · Powered by LazorKit + Jupiter</p>
257
+ </div>
258
+ <% } %>
259
+ )
260
+ }
261
+
262
+ function TokenSelect({ token, tokens, onChange, exclude }: { token: typeof TOKENS[0]; tokens: typeof TOKENS; onChange: (t: typeof TOKENS[0]) => void; exclude: string }) {
263
+ const [open, setOpen] = useState(false);
264
+ return (
265
+ <div style={{ position: 'relative' }}>
266
+ <% if (styling === 'tailwind') { %>
267
+ <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">
268
+ {token.symbol}<span className="text-neutral-400">▼</span>
269
+ </button>
270
+ {open && (
271
+ <div className="absolute right-0 mt-1 bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg z-10">
272
+ {tokens.filter((t) => t.symbol !== exclude).map((t) => (
273
+ <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>
274
+ ))}
275
+ </div>
276
+ )}
277
+ <% } else { %>
278
+ <button onClick={() => setOpen(!open)} style={styles.tokenBtn}>{token.symbol} <span style={{ color: '#a3a3a3' }}>▼</span></button>
279
+ {open && (
280
+ <div style={styles.dropdown as React.CSSProperties}>
281
+ {tokens.filter((t) => t.symbol !== exclude).map((t) => (
282
+ <button key={t.symbol} onClick={() => { onChange(t); setOpen(false); }} style={styles.dropdownItem}>{t.symbol}</button>
283
+ ))}
284
+ </div>
285
+ )}
286
+ <% } %>
287
+ </div>
288
+ );
289
+ }
@@ -13,7 +13,9 @@
13
13
  "react": "19.2.3",
14
14
  "react-dom": "19.2.3",
15
15
  "@solana/web3.js": "^1.95.0",
16
- "@lazorkit/wallet-adapter": "latest"
16
+ "@coral-xyz/anchor": "^0.30.0",
17
+ "@lazorkit/wallet": "latest",
18
+ "buffer": "^6.0.3"
17
19
  },
18
20
  "devDependencies": {
19
21
  "@tailwindcss/postcss": "^4",
@@ -1,104 +0,0 @@
1
- import { useState } from "react";
2
- import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
3
- import { useRouter } from "expo-router";
4
- import * as Linking from "expo-linking";
5
- import * as LocalAuthentication from "expo-local-authentication";
6
- import { connectPasskey } from "../lib/lazorkit";
7
- import { useStore } from "../lib/store";
8
-
9
- export default function BiometricOnboard() {
10
- const router = useRouter();
11
- const { wallet, setWallet } = useStore();
12
- const [step, setStep] = useState<"welcome" | "biometric" | "connecting" | "done">("welcome");
13
- const [error, setError] = useState<string | null>(null);
14
-
15
- const checkBiometrics = async () => {
16
- const compatible = await LocalAuthentication.hasHardwareAsync();
17
- const enrolled = await LocalAuthentication.isEnrolledAsync();
18
-
19
- if (!compatible || !enrolled) {
20
- setError("Biometric authentication not available");
21
- return;
22
- }
23
-
24
- const result = await LocalAuthentication.authenticateAsync({ promptMessage: "Verify your identity" });
25
-
26
- if (result.success) {
27
- setStep("connecting");
28
- await handleConnect();
29
- } else {
30
- setError("Authentication failed");
31
- }
32
- };
33
-
34
- const handleConnect = async () => {
35
- try {
36
- const redirectUrl = Linking.createURL("auth");
37
- const result = await connectPasskey(redirectUrl);
38
- if (result) {
39
- setWallet(result);
40
- setStep("done");
41
- } else {
42
- setError("Connection cancelled");
43
- setStep("biometric");
44
- }
45
- } catch (e) {
46
- setError(e instanceof Error ? e.message : "Failed");
47
- setStep("biometric");
48
- }
49
- };
50
-
51
- return (
52
- <View className="flex-1 justify-center p-5 bg-white">
53
- {step === "welcome" && (
54
- <>
55
- <Text className="text-3xl font-bold text-center">Welcome</Text>
56
- <Text className="text-gray-500 text-center mt-2 mb-8">Secure your wallet with biometrics</Text>
57
- <TouchableOpacity className="bg-blue-600 p-4 rounded-xl" onPress={() => setStep("biometric")}>
58
- <Text className="text-white text-center font-semibold">Get Started</Text>
59
- </TouchableOpacity>
60
- </>
61
- )}
62
-
63
- {step === "biometric" && (
64
- <>
65
- <Text className="text-3xl font-bold text-center">🔐</Text>
66
- <Text className="text-xl font-bold text-center mt-4">Enable Biometrics</Text>
67
- <Text className="text-gray-500 text-center mt-2 mb-8">Use Face ID or Touch ID to secure your wallet</Text>
68
- <TouchableOpacity className="bg-blue-600 p-4 rounded-xl" onPress={checkBiometrics}>
69
- <Text className="text-white text-center font-semibold">Authenticate</Text>
70
- </TouchableOpacity>
71
- </>
72
- )}
73
-
74
- {step === "connecting" && (
75
- <View className="items-center">
76
- <ActivityIndicator size="large" color="#0066FF" />
77
- <Text className="text-gray-500 mt-4">Creating your wallet...</Text>
78
- </View>
79
- )}
80
-
81
- {step === "done" && wallet && (
82
- <>
83
- <Text className="text-4xl text-center">✓</Text>
84
- <Text className="text-xl font-bold text-center mt-4">You're all set!</Text>
85
- <View className="bg-gray-100 p-4 rounded-xl mt-6">
86
- <Text className="text-sm text-gray-500">Your wallet</Text>
87
- <Text className="font-mono">{wallet.address.slice(0, 12)}...{wallet.address.slice(-8)}</Text>
88
- </View>
89
- <TouchableOpacity className="bg-blue-600 p-4 rounded-xl mt-6" onPress={() => router.replace("/")}>
90
- <Text className="text-white text-center font-semibold">Continue to App</Text>
91
- </TouchableOpacity>
92
- </>
93
- )}
94
-
95
- {error && <Text className="text-red-500 text-center mt-4">{error}</Text>}
96
-
97
- {step !== "done" && step !== "connecting" && (
98
- <TouchableOpacity className="mt-8" onPress={() => router.back()}>
99
- <Text className="text-blue-600 text-center">← Back</Text>
100
- </TouchableOpacity>
101
- )}
102
- </View>
103
- );
104
- }
@@ -1,72 +0,0 @@
1
- import { useState } from "react";
2
- import { View, Text, TextInput, TouchableOpacity, ActivityIndicator } from "react-native";
3
- import { useRouter } from "expo-router";
4
- import * as Linking from "expo-linking";
5
- import { PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js";
6
- import { signAndSendTransaction, sponsorTransaction } from "../lib/lazorkit";
7
- import { useStore } from "../lib/store";
8
-
9
- export default function GaslessTransfer() {
10
- const router = useRouter();
11
- const { wallet } = useStore();
12
- const [recipient, setRecipient] = useState("");
13
- const [amount, setAmount] = useState("");
14
- const [loading, setLoading] = useState(false);
15
- const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
16
-
17
- const handleTransfer = async () => {
18
- if (!wallet || !recipient || !amount) return;
19
- setLoading(true);
20
- setResult(null);
21
-
22
- try {
23
- const tx = new Transaction().add(
24
- SystemProgram.transfer({
25
- fromPubkey: wallet.publicKey,
26
- toPubkey: new PublicKey(recipient),
27
- lamports: parseFloat(amount) * LAMPORTS_PER_SOL,
28
- })
29
- );
30
-
31
- const sponsoredTx = await sponsorTransaction(tx);
32
- const signature = await signAndSendTransaction(sponsoredTx, wallet, Linking.createURL("callback"));
33
-
34
- setResult(signature ? { success: true, message: `Sent! ${signature.slice(0, 16)}...` } : { success: false, message: "Cancelled" });
35
- } catch (e) {
36
- setResult({ success: false, message: e instanceof Error ? e.message : "Failed" });
37
- } finally {
38
- setLoading(false);
39
- }
40
- };
41
-
42
- if (!wallet) {
43
- return (
44
- <View className="flex-1 justify-center p-5">
45
- <Text className="text-xl font-bold text-center mb-4">Connect wallet first</Text>
46
- <TouchableOpacity className="bg-blue-600 p-4 rounded-xl" onPress={() => router.push("/passkey-login")}>
47
- <Text className="text-white text-center font-semibold">Go to Login</Text>
48
- </TouchableOpacity>
49
- </View>
50
- );
51
- }
52
-
53
- return (
54
- <View className="flex-1 justify-center p-5 bg-white">
55
- <Text className="text-3xl font-bold text-center">Gasless Transfer</Text>
56
- <Text className="text-gray-500 text-center mt-2 mb-6">Send SOL without paying gas</Text>
57
-
58
- <TextInput className="border border-gray-300 rounded-lg p-3 mb-3" placeholder="Recipient address" value={recipient} onChangeText={setRecipient} />
59
- <TextInput className="border border-gray-300 rounded-lg p-3 mb-4" placeholder="Amount (SOL)" value={amount} onChangeText={setAmount} keyboardType="decimal-pad" />
60
-
61
- <TouchableOpacity className="bg-blue-600 p-4 rounded-xl items-center" onPress={handleTransfer} disabled={loading}>
62
- {loading ? <ActivityIndicator color="#fff" /> : <Text className="text-white font-semibold">Send (Gasless)</Text>}
63
- </TouchableOpacity>
64
-
65
- {result && <Text className={`text-center mt-4 ${result.success ? "text-green-600" : "text-red-500"}`}>{result.message}</Text>}
66
-
67
- <TouchableOpacity className="mt-8" onPress={() => router.back()}>
68
- <Text className="text-blue-600 text-center">← Back</Text>
69
- </TouchableOpacity>
70
- </View>
71
- );
72
- }
@@ -1,30 +0,0 @@
1
- import { View, Text, TouchableOpacity } from "react-native";
2
- import { useRouter } from "expo-router";
3
-
4
- export default function Home() {
5
- const router = useRouter();
6
-
7
- const examples = [
8
- { route: "/passkey-login", title: "Passkey Login", desc: "WebAuthn authentication" },
9
- { route: "/gasless-transfer", title: "Gasless Transfer", desc: "Send SOL without gas" },
10
- { route: "/biometric-onboard", title: "Biometric Onboarding", desc: "Secure wallet setup" },
11
- ];
12
-
13
- return (
14
- <View className="flex-1 justify-center p-5 bg-white">
15
- <Text className="text-3xl font-bold text-center">Welcome</Text>
16
- <Text className="text-gray-500 text-center mt-2 mb-8">LazorKit SDK Examples</Text>
17
-
18
- {examples.map((ex) => (
19
- <TouchableOpacity
20
- key={ex.route}
21
- className="border border-gray-200 p-4 rounded-xl mb-3"
22
- onPress={() => router.push(ex.route)}
23
- >
24
- <Text className="font-semibold">{ex.title}</Text>
25
- <Text className="text-sm text-gray-500">{ex.desc}</Text>
26
- </TouchableOpacity>
27
- ))}
28
- </View>
29
- );
30
- }
@@ -1,55 +0,0 @@
1
- import { useState } from "react";
2
- import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
3
- import { useRouter } from "expo-router";
4
- import * as Linking from "expo-linking";
5
- import { connectPasskey } from "../lib/lazorkit";
6
- import { useStore } from "../lib/store";
7
-
8
- export default function PasskeyLogin() {
9
- const router = useRouter();
10
- const { wallet, setWallet } = useStore();
11
- const [loading, setLoading] = useState(false);
12
- const [error, setError] = useState<string | null>(null);
13
-
14
- const handleConnect = async () => {
15
- setLoading(true);
16
- setError(null);
17
- try {
18
- const redirectUrl = Linking.createURL("auth");
19
- const result = await connectPasskey(redirectUrl);
20
- if (result) setWallet(result);
21
- else setError("Connection cancelled");
22
- } catch (e) {
23
- setError(e instanceof Error ? e.message : "Connection failed");
24
- } finally {
25
- setLoading(false);
26
- }
27
- };
28
-
29
- return (
30
- <View className="flex-1 justify-center p-5 bg-white">
31
- <Text className="text-3xl font-bold text-center">Passkey Login</Text>
32
- <Text className="text-base text-gray-500 text-center mt-2 mb-8">Connect with biometric authentication</Text>
33
-
34
- {wallet ? (
35
- <View className="bg-gray-100 p-5 rounded-xl">
36
- <Text className="text-sm text-gray-500">Connected Wallet</Text>
37
- <Text className="text-base font-semibold font-mono">{wallet.address.slice(0, 8)}...{wallet.address.slice(-8)}</Text>
38
- <TouchableOpacity className="border border-gray-300 p-3 rounded-lg mt-4" onPress={() => setWallet(null)}>
39
- <Text className="text-center text-gray-600">Disconnect</Text>
40
- </TouchableOpacity>
41
- </View>
42
- ) : (
43
- <TouchableOpacity className="bg-blue-600 p-4 rounded-xl items-center" onPress={handleConnect} disabled={loading}>
44
- {loading ? <ActivityIndicator color="#fff" /> : <Text className="text-white text-base font-semibold">Connect with Passkey</Text>}
45
- </TouchableOpacity>
46
- )}
47
-
48
- {error && <Text className="text-red-500 text-center mt-4">{error}</Text>}
49
-
50
- <TouchableOpacity className="mt-8 items-center" onPress={() => router.back()}>
51
- <Text className="text-blue-600 text-base">← Back</Text>
52
- </TouchableOpacity>
53
- </View>
54
- );
55
- }
@@ -1,70 +0,0 @@
1
- "use client";
2
- import { useState } from "react";
3
- import Link from "next/link";
4
- import { connectPasskey } from "@/lib/lazorkit";
5
- import { useStore } from "@/lib/store";
6
-
7
- export default function BiometricOnboard() {
8
- const { wallet, setWallet } = useStore();
9
- const [step, setStep] = useState<"welcome" | "connecting" | "done">("welcome");
10
- const [error, setError] = useState<string | null>(null);
11
-
12
- const handleConnect = async () => {
13
- setStep("connecting");
14
- setError(null);
15
- try {
16
- const result = await connectPasskey();
17
- if (result) {
18
- setWallet(result);
19
- setStep("done");
20
- } else {
21
- setError("Connection cancelled");
22
- setStep("welcome");
23
- }
24
- } catch (e) {
25
- setError(e instanceof Error ? e.message : "Failed");
26
- setStep("welcome");
27
- }
28
- };
29
-
30
- return (
31
- <div className="min-h-screen flex flex-col items-center justify-center p-6">
32
- {step === "welcome" && (
33
- <>
34
- <h1 className="text-3xl font-bold">Welcome</h1>
35
- <p className="text-gray-500 mt-2 mb-8">Secure your wallet with passkeys</p>
36
- <button onClick={handleConnect} className="bg-blue-600 text-white px-8 py-4 rounded-xl font-semibold hover:bg-blue-700">
37
- Get Started
38
- </button>
39
- </>
40
- )}
41
-
42
- {step === "connecting" && (
43
- <div className="text-center">
44
- <div className="animate-spin w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full mx-auto" />
45
- <p className="text-gray-500 mt-4">Creating your wallet...</p>
46
- </div>
47
- )}
48
-
49
- {step === "done" && wallet && (
50
- <>
51
- <div className="text-5xl mb-4">✓</div>
52
- <h1 className="text-2xl font-bold">You're all set!</h1>
53
- <div className="bg-gray-100 p-4 rounded-xl mt-6 w-full max-w-sm">
54
- <p className="text-sm text-gray-500">Your wallet</p>
55
- <p className="font-mono">{wallet.address.slice(0, 12)}...{wallet.address.slice(-8)}</p>
56
- </div>
57
- <Link href="/" className="bg-blue-600 text-white px-8 py-4 rounded-xl font-semibold mt-6 hover:bg-blue-700">
58
- Continue to App
59
- </Link>
60
- </>
61
- )}
62
-
63
- {error && <p className="text-red-500 mt-4">{error}</p>}
64
-
65
- {step === "welcome" && (
66
- <Link href="/examples" className="text-blue-600 mt-8">← Back to Examples</Link>
67
- )}
68
- </div>
69
- );
70
- }