create-lightning-scaffold 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +335 -0
  5. package/package.json +55 -0
  6. package/templates/backend/firebase/index.ts +25 -0
  7. package/templates/backend/firebase/package.json.ejs +8 -0
  8. package/templates/backend/supabase/index.ts +16 -0
  9. package/templates/backend/supabase/package.json.ejs +8 -0
  10. package/templates/base/.env.example.ejs +18 -0
  11. package/templates/components/nativewind-ui/button.tsx +25 -0
  12. package/templates/components/nativewind-ui/card.tsx +17 -0
  13. package/templates/components/nativewind-ui/index.ts +2 -0
  14. package/templates/components/shadcn/button.tsx +15 -0
  15. package/templates/components/shadcn/card.tsx +19 -0
  16. package/templates/components/shadcn/index.ts +2 -0
  17. package/templates/examples/mobile/biometric-onboard.tsx +104 -0
  18. package/templates/examples/mobile/gasless-transfer.tsx +72 -0
  19. package/templates/examples/mobile/index.tsx +30 -0
  20. package/templates/examples/mobile/passkey-login.tsx +55 -0
  21. package/templates/examples/web/biometric-onboard/page.tsx +70 -0
  22. package/templates/examples/web/gasless-transfer/page.tsx +85 -0
  23. package/templates/examples/web/page.tsx +27 -0
  24. package/templates/examples/web/passkey-login/page.tsx +50 -0
  25. package/templates/lib/lazorkit/mobile/index.ts +53 -0
  26. package/templates/lib/lazorkit/web/index.ts +67 -0
  27. package/templates/mobile/.vscode/extensions.json +1 -0
  28. package/templates/mobile/.vscode/settings.json +7 -0
  29. package/templates/mobile/app/(tabs)/_layout.tsx +59 -0
  30. package/templates/mobile/app/(tabs)/index.tsx +31 -0
  31. package/templates/mobile/app/(tabs)/two.tsx +31 -0
  32. package/templates/mobile/app/+html.tsx +38 -0
  33. package/templates/mobile/app/+not-found.tsx +40 -0
  34. package/templates/mobile/app/_layout.tsx +59 -0
  35. package/templates/mobile/app/modal.tsx +35 -0
  36. package/templates/mobile/app.json.ejs +38 -0
  37. package/templates/mobile/assets/fonts/SpaceMono-Regular.ttf +0 -0
  38. package/templates/mobile/assets/images/adaptive-icon.png +0 -0
  39. package/templates/mobile/assets/images/favicon.png +0 -0
  40. package/templates/mobile/assets/images/icon.png +0 -0
  41. package/templates/mobile/assets/images/splash-icon.png +0 -0
  42. package/templates/mobile/components/EditScreenInfo.tsx +77 -0
  43. package/templates/mobile/components/ExternalLink.tsx +25 -0
  44. package/templates/mobile/components/StyledText.tsx +5 -0
  45. package/templates/mobile/components/Themed.tsx +45 -0
  46. package/templates/mobile/components/__tests__/StyledText-test.js +10 -0
  47. package/templates/mobile/components/useClientOnlyValue.ts +4 -0
  48. package/templates/mobile/components/useClientOnlyValue.web.ts +12 -0
  49. package/templates/mobile/components/useColorScheme.ts +1 -0
  50. package/templates/mobile/components/useColorScheme.web.ts +8 -0
  51. package/templates/mobile/constants/Colors.ts +19 -0
  52. package/templates/mobile/lib/lazorkit/index.ts +53 -0
  53. package/templates/mobile/package.json.ejs +40 -0
  54. package/templates/mobile/tsconfig.json +17 -0
  55. package/templates/state/redux/index.ts +30 -0
  56. package/templates/state/zustand/index.ts +16 -0
  57. package/templates/styling/nativewind/global.css +3 -0
  58. package/templates/styling/nativewind/tailwind.config.js +7 -0
  59. package/templates/web/README.md +36 -0
  60. package/templates/web/app/favicon.ico +0 -0
  61. package/templates/web/app/globals.css +26 -0
  62. package/templates/web/app/layout.tsx.ejs +32 -0
  63. package/templates/web/app/page.tsx +65 -0
  64. package/templates/web/eslint.config.mjs +18 -0
  65. package/templates/web/lib/lazorkit/index.ts +67 -0
  66. package/templates/web/next.config.ts +7 -0
  67. package/templates/web/package.json.ejs +28 -0
  68. package/templates/web/postcss.config.mjs +7 -0
  69. package/templates/web/public/file.svg +1 -0
  70. package/templates/web/public/globe.svg +1 -0
  71. package/templates/web/public/next.svg +1 -0
  72. package/templates/web/public/vercel.svg +1 -0
  73. package/templates/web/public/window.svg +1 -0
  74. package/templates/web/tailwind.config.ts +14 -0
  75. package/templates/web/tsconfig.json +34 -0
@@ -0,0 +1,104 @@
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
+ }
@@ -0,0 +1,72 @@
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
+ }
@@ -0,0 +1,30 @@
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
+ }
@@ -0,0 +1,55 @@
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
+ }
@@ -0,0 +1,70 @@
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
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import Link from "next/link";
4
+ import { PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js";
5
+ import { signAndSendTransaction, sponsorTransaction } from "@/lib/lazorkit";
6
+ import { useStore } from "@/lib/store";
7
+
8
+ export default function GaslessTransfer() {
9
+ const { wallet } = useStore();
10
+ const [recipient, setRecipient] = useState("");
11
+ const [amount, setAmount] = useState("");
12
+ const [loading, setLoading] = useState(false);
13
+ const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
14
+
15
+ const handleTransfer = async () => {
16
+ if (!wallet || !recipient || !amount) return;
17
+ setLoading(true);
18
+ setResult(null);
19
+
20
+ try {
21
+ const tx = new Transaction().add(
22
+ SystemProgram.transfer({
23
+ fromPubkey: wallet.publicKey,
24
+ toPubkey: new PublicKey(recipient),
25
+ lamports: parseFloat(amount) * LAMPORTS_PER_SOL,
26
+ })
27
+ );
28
+
29
+ const sponsoredTx = await sponsorTransaction(tx);
30
+ const signature = await signAndSendTransaction(sponsoredTx, wallet);
31
+
32
+ setResult(signature ? { success: true, message: `Sent! ${signature.slice(0, 20)}...` } : { success: false, message: "Cancelled" });
33
+ } catch (e) {
34
+ setResult({ success: false, message: e instanceof Error ? e.message : "Failed" });
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ if (!wallet) {
41
+ return (
42
+ <div className="min-h-screen flex flex-col items-center justify-center p-6">
43
+ <h1 className="text-xl font-bold mb-4">Connect wallet first</h1>
44
+ <Link href="/examples/passkey-login" className="bg-blue-600 text-white px-6 py-3 rounded-xl">Go to Login</Link>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <div className="min-h-screen flex flex-col items-center justify-center p-6">
51
+ <h1 className="text-3xl font-bold">Gasless Transfer</h1>
52
+ <p className="text-gray-500 mt-2 mb-8">Send SOL without paying gas</p>
53
+
54
+ <div className="w-full max-w-sm space-y-4">
55
+ <input
56
+ type="text"
57
+ placeholder="Recipient address"
58
+ value={recipient}
59
+ onChange={(e) => setRecipient(e.target.value)}
60
+ className="w-full border border-gray-300 rounded-lg p-3"
61
+ />
62
+ <input
63
+ type="text"
64
+ placeholder="Amount (SOL)"
65
+ value={amount}
66
+ onChange={(e) => setAmount(e.target.value)}
67
+ className="w-full border border-gray-300 rounded-lg p-3"
68
+ />
69
+ <button
70
+ onClick={handleTransfer}
71
+ disabled={loading || !recipient || !amount}
72
+ className="w-full bg-blue-600 text-white py-4 rounded-xl font-semibold hover:bg-blue-700 disabled:opacity-50"
73
+ >
74
+ {loading ? "Sending..." : "Send (Gasless)"}
75
+ </button>
76
+ </div>
77
+
78
+ {result && (
79
+ <p className={`mt-4 ${result.success ? "text-green-600" : "text-red-500"}`}>{result.message}</p>
80
+ )}
81
+
82
+ <Link href="/examples" className="text-blue-600 mt-8">← Back to Examples</Link>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,27 @@
1
+ import Link from "next/link";
2
+
3
+ export default function Examples() {
4
+ const examples = [
5
+ { href: "/examples/passkey-login", title: "Passkey Login", desc: "WebAuthn-based authentication" },
6
+ { href: "/examples/gasless-transfer", title: "Gasless Transfer", desc: "Send SOL without gas fees" },
7
+ { href: "/examples/biometric-onboard", title: "Biometric Onboarding", desc: "Secure wallet setup flow" },
8
+ ];
9
+
10
+ return (
11
+ <div className="min-h-screen flex flex-col items-center justify-center p-6">
12
+ <h1 className="text-3xl font-bold mb-2">LazorKit Examples</h1>
13
+ <p className="text-gray-500 mb-8">Explore the SDK integration demos</p>
14
+
15
+ <div className="grid gap-4 w-full max-w-md">
16
+ {examples.map((ex) => (
17
+ <Link key={ex.href} href={ex.href} className="block p-4 border border-gray-200 rounded-xl hover:border-blue-500 hover:bg-blue-50 transition">
18
+ <h2 className="font-semibold">{ex.title}</h2>
19
+ <p className="text-sm text-gray-500">{ex.desc}</p>
20
+ </Link>
21
+ ))}
22
+ </div>
23
+
24
+ <Link href="/" className="text-blue-600 mt-8">← Back to Home</Link>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,50 @@
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 PasskeyLogin() {
8
+ const { wallet, setWallet } = useStore();
9
+ const [loading, setLoading] = useState(false);
10
+ const [error, setError] = useState<string | null>(null);
11
+
12
+ const handleConnect = async () => {
13
+ setLoading(true);
14
+ setError(null);
15
+ try {
16
+ const result = await connectPasskey();
17
+ if (result) setWallet(result);
18
+ else setError("Connection cancelled");
19
+ } catch (e) {
20
+ setError(e instanceof Error ? e.message : "Connection failed");
21
+ } finally {
22
+ setLoading(false);
23
+ }
24
+ };
25
+
26
+ return (
27
+ <div className="min-h-screen flex flex-col items-center justify-center p-6">
28
+ <h1 className="text-3xl font-bold">Passkey Login</h1>
29
+ <p className="text-gray-500 mt-2 mb-8">Connect with biometric authentication</p>
30
+
31
+ {wallet ? (
32
+ <div className="bg-gray-100 p-6 rounded-xl w-full max-w-sm">
33
+ <p className="text-sm text-gray-500">Connected Wallet</p>
34
+ <p className="font-mono font-semibold">{wallet.address.slice(0, 8)}...{wallet.address.slice(-8)}</p>
35
+ <button onClick={() => setWallet(null)} className="w-full mt-4 border border-gray-300 p-3 rounded-lg text-gray-600 hover:bg-gray-50">
36
+ Disconnect
37
+ </button>
38
+ </div>
39
+ ) : (
40
+ <button onClick={handleConnect} disabled={loading} className="bg-blue-600 text-white px-8 py-4 rounded-xl font-semibold hover:bg-blue-700 disabled:opacity-50">
41
+ {loading ? "Connecting..." : "Connect with Passkey"}
42
+ </button>
43
+ )}
44
+
45
+ {error && <p className="text-red-500 mt-4">{error}</p>}
46
+
47
+ <Link href="/examples" className="text-blue-600 mt-8">← Back to Examples</Link>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,53 @@
1
+ import * as WebBrowser from "expo-web-browser";
2
+ import * as Linking from "expo-linking";
3
+ import { Connection, PublicKey, Transaction } from "@solana/web3.js";
4
+
5
+ const PORTAL_URL = process.env.EXPO_PUBLIC_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
6
+ const PAYMASTER_URL = process.env.EXPO_PUBLIC_LAZORKIT_PAYMASTER_URL || "https://kora.devnet.lazorkit.com";
7
+ const RPC_URL = process.env.EXPO_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
8
+
9
+ export interface LazorWallet {
10
+ address: string;
11
+ publicKey: PublicKey;
12
+ }
13
+
14
+ export async function connectPasskey(redirectUrl: string): Promise<LazorWallet | null> {
15
+ const authUrl = `${PORTAL_URL}/auth?redirect=${encodeURIComponent(redirectUrl)}`;
16
+ const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUrl);
17
+
18
+ if (result.type === "success" && result.url) {
19
+ const params = Linking.parse(result.url);
20
+ const address = params.queryParams?.address as string;
21
+ if (address) return { address, publicKey: new PublicKey(address) };
22
+ }
23
+ return null;
24
+ }
25
+
26
+ export async function signAndSendTransaction(tx: Transaction, wallet: LazorWallet, redirectUrl: string): Promise<string | null> {
27
+ const connection = new Connection(RPC_URL);
28
+ const { blockhash } = await connection.getLatestBlockhash();
29
+ tx.recentBlockhash = blockhash;
30
+ tx.feePayer = wallet.publicKey;
31
+
32
+ const serialized = tx.serialize({ requireAllSignatures: false }).toString("base64");
33
+ const signUrl = `${PORTAL_URL}/sign?tx=${encodeURIComponent(serialized)}&redirect=${encodeURIComponent(redirectUrl)}`;
34
+ const result = await WebBrowser.openAuthSessionAsync(signUrl, redirectUrl);
35
+
36
+ if (result.type === "success" && result.url) {
37
+ const params = Linking.parse(result.url);
38
+ return (params.queryParams?.signature as string) || null;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export async function sponsorTransaction(tx: Transaction): Promise<Transaction> {
44
+ const res = await fetch(`${PAYMASTER_URL}/sponsor`, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({ transaction: tx.serialize({ requireAllSignatures: false }).toString("base64") }),
48
+ });
49
+ const { sponsoredTx } = await res.json();
50
+ return Transaction.from(Buffer.from(sponsoredTx, "base64"));
51
+ }
52
+
53
+ export const connection = new Connection(RPC_URL);
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { Connection, PublicKey, Transaction } from "@solana/web3.js";
3
+
4
+ const PORTAL_URL = process.env.NEXT_PUBLIC_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
5
+ const PAYMASTER_URL = process.env.NEXT_PUBLIC_LAZORKIT_PAYMASTER_URL || "https://kora.devnet.lazorkit.com";
6
+ const RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
7
+
8
+ export interface LazorWallet {
9
+ address: string;
10
+ publicKey: PublicKey;
11
+ }
12
+
13
+ export async function connectPasskey(): Promise<LazorWallet | null> {
14
+ const redirectUrl = `${window.location.origin}/auth/callback`;
15
+ const popup = window.open(`${PORTAL_URL}/auth?redirect=${encodeURIComponent(redirectUrl)}`, "lazorkit", "width=500,height=600");
16
+
17
+ return new Promise((resolve) => {
18
+ const handler = (e: MessageEvent) => {
19
+ if (e.origin === window.location.origin && e.data?.address) {
20
+ window.removeEventListener("message", handler);
21
+ popup?.close();
22
+ resolve({ address: e.data.address, publicKey: new PublicKey(e.data.address) });
23
+ }
24
+ };
25
+ window.addEventListener("message", handler);
26
+ const check = setInterval(() => {
27
+ if (popup?.closed) { clearInterval(check); window.removeEventListener("message", handler); resolve(null); }
28
+ }, 500);
29
+ });
30
+ }
31
+
32
+ export async function signAndSendTransaction(tx: Transaction, wallet: LazorWallet): Promise<string | null> {
33
+ const connection = new Connection(RPC_URL);
34
+ const { blockhash } = await connection.getLatestBlockhash();
35
+ tx.recentBlockhash = blockhash;
36
+ tx.feePayer = wallet.publicKey;
37
+
38
+ const serialized = tx.serialize({ requireAllSignatures: false }).toString("base64");
39
+ const redirectUrl = `${window.location.origin}/auth/callback`;
40
+ const popup = window.open(`${PORTAL_URL}/sign?tx=${encodeURIComponent(serialized)}&redirect=${encodeURIComponent(redirectUrl)}`, "lazorkit", "width=500,height=600");
41
+
42
+ return new Promise((resolve) => {
43
+ const handler = (e: MessageEvent) => {
44
+ if (e.origin === window.location.origin && e.data?.signature) {
45
+ window.removeEventListener("message", handler);
46
+ popup?.close();
47
+ resolve(e.data.signature);
48
+ }
49
+ };
50
+ window.addEventListener("message", handler);
51
+ const check = setInterval(() => {
52
+ if (popup?.closed) { clearInterval(check); window.removeEventListener("message", handler); resolve(null); }
53
+ }, 500);
54
+ });
55
+ }
56
+
57
+ export async function sponsorTransaction(tx: Transaction): Promise<Transaction> {
58
+ const res = await fetch(`${PAYMASTER_URL}/sponsor`, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({ transaction: tx.serialize({ requireAllSignatures: false }).toString("base64") }),
62
+ });
63
+ const { sponsoredTx } = await res.json();
64
+ return Transaction.from(Buffer.from(sponsoredTx, "base64"));
65
+ }
66
+
67
+ export const connection = new Connection(RPC_URL);
@@ -0,0 +1 @@
1
+ { "recommendations": ["expo.vscode-expo-tools"] }
@@ -0,0 +1,7 @@
1
+ {
2
+ "editor.codeActionsOnSave": {
3
+ "source.fixAll": "explicit",
4
+ "source.organizeImports": "explicit",
5
+ "source.sortMembers": "explicit"
6
+ }
7
+ }