create-lightning-scaffold 1.0.0 → 1.0.1
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 +107 -25
- package/dist/index.js +169 -64
- package/package.json +1 -1
- package/templates/backend/firebase/index.ts +86 -9
- package/templates/backend/supabase/index.ts +107 -6
- package/templates/base/.env.example.ejs +17 -10
- package/templates/lib/backend.ts.ejs +59 -0
- package/templates/mobile/app/_layout.tsx +22 -52
- package/templates/mobile/app/index.tsx.ejs +44 -0
- package/templates/mobile/components/History.tsx.ejs +114 -0
- package/templates/mobile/components/Onboarding.tsx.ejs +79 -0
- package/templates/mobile/components/Recovery.tsx.ejs +119 -0
- package/templates/mobile/components/Swap.tsx.ejs +304 -0
- package/templates/mobile/package.json.ejs +4 -0
- package/templates/vite/README.md +73 -0
- package/templates/vite/eslint.config.js +23 -0
- package/templates/vite/index.html +13 -0
- package/templates/vite/package.json.ejs +37 -0
- package/templates/vite/postcss.config.js.ejs +8 -0
- package/templates/vite/public/vite.svg +1 -0
- package/templates/vite/src/App.tsx.ejs +65 -0
- package/templates/vite/src/assets/react.svg +1 -0
- package/templates/vite/src/components/History.tsx.ejs +112 -0
- package/templates/vite/src/components/Onboarding.tsx.ejs +64 -0
- package/templates/vite/src/components/Recovery.tsx.ejs +107 -0
- package/templates/vite/src/components/Swap.tsx.ejs +241 -0
- package/templates/vite/src/index.css.ejs +55 -0
- package/templates/vite/src/main.tsx +25 -0
- package/templates/vite/tailwind.config.js.ejs +13 -0
- package/templates/vite/tsconfig.app.json +28 -0
- package/templates/vite/tsconfig.json +7 -0
- package/templates/vite/tsconfig.node.json +26 -0
- package/templates/vite/vite.config.ts +15 -0
- package/templates/web/app/globals.css.ejs +53 -0
- package/templates/web/app/layout.tsx.ejs +8 -15
- package/templates/web/app/page.tsx.ejs +45 -0
- package/templates/web/app/providers.tsx +27 -0
- package/templates/web/components/History.tsx.ejs +114 -0
- package/templates/web/components/Onboarding.tsx.ejs +66 -0
- package/templates/web/components/Recovery.tsx.ejs +108 -0
- package/templates/web/components/Swap.tsx.ejs +289 -0
- package/templates/web/package.json.ejs +3 -1
- package/templates/examples/mobile/biometric-onboard.tsx +0 -104
- package/templates/examples/mobile/gasless-transfer.tsx +0 -72
- package/templates/examples/mobile/index.tsx +0 -30
- package/templates/examples/mobile/passkey-login.tsx +0 -55
- package/templates/examples/web/biometric-onboard/page.tsx +0 -70
- package/templates/examples/web/gasless-transfer/page.tsx +0 -85
- package/templates/examples/web/page.tsx +0 -27
- package/templates/examples/web/passkey-login/page.tsx +0 -50
- package/templates/lib/lazorkit/mobile/index.ts +0 -53
- package/templates/lib/lazorkit/web/index.ts +0 -67
- package/templates/mobile/app/(tabs)/_layout.tsx +0 -59
- package/templates/mobile/app/(tabs)/index.tsx +0 -31
- package/templates/mobile/app/(tabs)/two.tsx +0 -31
- package/templates/mobile/app/+html.tsx +0 -38
- package/templates/mobile/app/+not-found.tsx +0 -40
- package/templates/mobile/app/modal.tsx +0 -35
- package/templates/mobile/components/EditScreenInfo.tsx +0 -77
- package/templates/mobile/components/StyledText.tsx +0 -5
- package/templates/mobile/components/__tests__/StyledText-test.js +0 -10
- package/templates/mobile/lib/lazorkit/index.ts +0 -53
- package/templates/web/app/globals.css +0 -26
- package/templates/web/app/page.tsx +0 -65
- package/templates/web/lib/lazorkit/index.ts +0 -67
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [
|
|
7
|
+
react(),
|
|
8
|
+
nodePolyfills(),
|
|
9
|
+
],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'@': '/src',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<% if (styling === 'tailwind') { %>
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--background: #ffffff;
|
|
6
|
+
--foreground: #0a0a0a;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@media (prefers-color-scheme: dark) {
|
|
10
|
+
:root {
|
|
11
|
+
--background: #0a0a0a;
|
|
12
|
+
--foreground: #fafafa;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
background: var(--background);
|
|
18
|
+
color: var(--foreground);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
input:focus {
|
|
22
|
+
outline: none;
|
|
23
|
+
}
|
|
24
|
+
<% } else { %>
|
|
25
|
+
* {
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:root {
|
|
32
|
+
--background: #ffffff;
|
|
33
|
+
--foreground: #0a0a0a;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root {
|
|
38
|
+
--background: #0a0a0a;
|
|
39
|
+
--foreground: #fafafa;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
body {
|
|
44
|
+
background: var(--background);
|
|
45
|
+
color: var(--foreground);
|
|
46
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
47
|
+
-webkit-font-smoothing: antialiased;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
input:focus {
|
|
51
|
+
outline: none;
|
|
52
|
+
}
|
|
53
|
+
<% } %>
|
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import {
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
3
|
import "./globals.css";
|
|
4
|
+
import { Providers } from "./providers";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
variable: "--font-geist-sans",
|
|
7
|
-
subsets: ["latin"],
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const geistMono = Geist_Mono({
|
|
11
|
-
variable: "--font-geist-mono",
|
|
12
|
-
subsets: ["latin"],
|
|
13
|
-
});
|
|
6
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
14
7
|
|
|
15
8
|
export const metadata: Metadata = {
|
|
16
9
|
title: "<%= name %>",
|
|
17
|
-
description: "
|
|
10
|
+
description: "Passkey wallet with gasless swaps",
|
|
18
11
|
};
|
|
19
12
|
|
|
20
13
|
export default function RootLayout({
|
|
21
14
|
children,
|
|
22
|
-
}:
|
|
15
|
+
}: {
|
|
23
16
|
children: React.ReactNode;
|
|
24
|
-
}
|
|
17
|
+
}) {
|
|
25
18
|
return (
|
|
26
19
|
<html lang="en">
|
|
27
|
-
<body className={
|
|
28
|
-
{children}
|
|
20
|
+
<body className={inter.className}>
|
|
21
|
+
<Providers>{children}</Providers>
|
|
29
22
|
</body>
|
|
30
23
|
</html>
|
|
31
24
|
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useWallet } from "@lazorkit/wallet";
|
|
4
|
+
import { Onboarding } from "@/components/Onboarding";
|
|
5
|
+
import { Swap } from "@/components/Swap";
|
|
6
|
+
import { Recovery } from "@/components/Recovery";
|
|
7
|
+
import { History } from "@/components/History";
|
|
8
|
+
|
|
9
|
+
type View = "swap" | "recovery" | "history";
|
|
10
|
+
|
|
11
|
+
export default function Home() {
|
|
12
|
+
const { isConnected } = useWallet();
|
|
13
|
+
const [view, setView] = useState<View>("swap");
|
|
14
|
+
|
|
15
|
+
if (!isConnected) {
|
|
16
|
+
return (
|
|
17
|
+
<main<% if (styling === 'tailwind') { %> className="min-h-screen flex items-center justify-center p-4"<% } else { %> style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}<% } %>>
|
|
18
|
+
<Onboarding />
|
|
19
|
+
</main>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<main<% if (styling === 'tailwind') { %> className="min-h-screen flex flex-col items-center justify-center p-4"<% } else { %> style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 16 }}<% } %>>
|
|
25
|
+
{view === "swap" && (
|
|
26
|
+
<>
|
|
27
|
+
<Swap />
|
|
28
|
+
<% if (styling === 'tailwind') { %>
|
|
29
|
+
<div className="flex gap-4 mt-4">
|
|
30
|
+
<button onClick={() => setView("history")} className="text-xs text-neutral-500 hover:text-black">History</button>
|
|
31
|
+
<button onClick={() => setView("recovery")} className="text-xs text-neutral-500 hover:text-black">Recovery</button>
|
|
32
|
+
</div>
|
|
33
|
+
<% } else { %>
|
|
34
|
+
<div style={{ display: 'flex', gap: 16, marginTop: 16 }}>
|
|
35
|
+
<button onClick={() => setView("history")} style={{ fontSize: 12, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' }}>History</button>
|
|
36
|
+
<button onClick={() => setView("recovery")} style={{ fontSize: 12, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' }}>Recovery</button>
|
|
37
|
+
</div>
|
|
38
|
+
<% } %>
|
|
39
|
+
</>
|
|
40
|
+
)}
|
|
41
|
+
{view === "recovery" && <Recovery onBack={() => setView("swap")} />}
|
|
42
|
+
{view === "history" && <History onBack={() => setView("swap")} />}
|
|
43
|
+
</main>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { LazorkitProvider } from "@lazorkit/wallet";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
const CONFIG = {
|
|
6
|
+
rpcUrl: process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com",
|
|
7
|
+
portalUrl: process.env.NEXT_PUBLIC_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh",
|
|
8
|
+
paymasterConfig: {
|
|
9
|
+
paymasterUrl: process.env.NEXT_PUBLIC_LAZORKIT_PAYMASTER_URL || "https://kora.devnet.lazorkit.com",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (typeof window !== "undefined") {
|
|
14
|
+
window.Buffer = window.Buffer || require("buffer").Buffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
18
|
+
return (
|
|
19
|
+
<LazorkitProvider
|
|
20
|
+
rpcUrl={CONFIG.rpcUrl}
|
|
21
|
+
portalUrl={CONFIG.portalUrl}
|
|
22
|
+
paymasterConfig={CONFIG.paymasterConfig}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</LazorkitProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { useWallet } from "@lazorkit/wallet";
|
|
4
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
5
|
+
<% if (styling !== 'tailwind') { %>
|
|
6
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
7
|
+
container: { width: '100%', maxWidth: 360 },
|
|
8
|
+
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
|
|
9
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
10
|
+
list: { display: 'flex', flexDirection: 'column', gap: 8 },
|
|
11
|
+
item: { border: '1px solid #e5e5e5', borderRadius: 12, padding: 12 },
|
|
12
|
+
row: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
|
|
13
|
+
sig: { fontFamily: 'monospace', fontSize: 13, color: '#0a0a0a' },
|
|
14
|
+
time: { fontSize: 12, color: '#a3a3a3' },
|
|
15
|
+
status: { fontSize: 11, padding: '2px 6px', borderRadius: 4 },
|
|
16
|
+
success: { backgroundColor: '#dcfce7', color: '#166534' },
|
|
17
|
+
failed: { backgroundColor: '#fee2e2', color: '#991b1b' },
|
|
18
|
+
empty: { textAlign: 'center', padding: 32, color: '#a3a3a3', fontSize: 14 },
|
|
19
|
+
back: { fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
20
|
+
link: { color: '#0a0a0a', textDecoration: 'none' },
|
|
21
|
+
};
|
|
22
|
+
<% } %>
|
|
23
|
+
interface Props {
|
|
24
|
+
onBack: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TxInfo {
|
|
28
|
+
signature: string;
|
|
29
|
+
slot: number;
|
|
30
|
+
blockTime: number | null;
|
|
31
|
+
err: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function History({ onBack }: Props) {
|
|
35
|
+
const { wallet } = useWallet();
|
|
36
|
+
const [txs, setTxs] = useState<TxInfo[]>([]);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!wallet?.smartWallet) return;
|
|
41
|
+
const rpc = process.env.NEXT_PUBLIC_SOLANA_RPC || "https://api.devnet.solana.com";
|
|
42
|
+
const conn = new Connection(rpc);
|
|
43
|
+
conn.getSignaturesForAddress(new PublicKey(wallet.smartWallet), { limit: 10 })
|
|
44
|
+
.then((sigs) => setTxs(sigs.map((s) => ({ signature: s.signature, slot: s.slot, blockTime: s.blockTime, err: s.err }))))
|
|
45
|
+
.catch(console.error)
|
|
46
|
+
.finally(() => setLoading(false));
|
|
47
|
+
}, [wallet?.smartWallet]);
|
|
48
|
+
|
|
49
|
+
const formatTime = (ts: number | null) => {
|
|
50
|
+
if (!ts) return "—";
|
|
51
|
+
return new Date(ts * 1000).toLocaleDateString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const explorerUrl = (sig: string) => {
|
|
55
|
+
const cluster = (process.env.NEXT_PUBLIC_SOLANA_RPC || "").includes("mainnet") ? "" : "?cluster=devnet";
|
|
56
|
+
return `https://solscan.io/tx/${sig}${cluster}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<% if (styling === 'tailwind') { %>
|
|
61
|
+
<div className="w-full max-w-sm">
|
|
62
|
+
<div className="flex items-center justify-between mb-4">
|
|
63
|
+
<h1 className="text-lg font-semibold">History</h1>
|
|
64
|
+
<button onClick={onBack} className="text-sm text-neutral-500 hover:text-black">← Back</button>
|
|
65
|
+
</div>
|
|
66
|
+
{loading ? (
|
|
67
|
+
<p className="text-center py-8 text-neutral-400">Loading...</p>
|
|
68
|
+
) : txs.length === 0 ? (
|
|
69
|
+
<p className="text-center py-8 text-neutral-400">No transactions yet</p>
|
|
70
|
+
) : (
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
{txs.map((tx) => (
|
|
73
|
+
<a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" className="block border border-neutral-200 rounded-xl p-3 hover:bg-neutral-50">
|
|
74
|
+
<div className="flex justify-between items-center">
|
|
75
|
+
<span className="font-mono text-sm">{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</span>
|
|
76
|
+
<span className={`text-xs px-2 py-0.5 rounded ${tx.err ? "bg-red-100 text-red-700" : "bg-green-100 text-green-700"}`}>
|
|
77
|
+
{tx.err ? "Failed" : "Success"}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<p className="text-xs text-neutral-400 mt-1">{formatTime(tx.blockTime)}</p>
|
|
81
|
+
</a>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
<% } else { %>
|
|
87
|
+
<div style={styles.container}>
|
|
88
|
+
<div style={styles.header}>
|
|
89
|
+
<h1 style={styles.title}>History</h1>
|
|
90
|
+
<button onClick={onBack} style={styles.back}>← Back</button>
|
|
91
|
+
</div>
|
|
92
|
+
{loading ? (
|
|
93
|
+
<p style={styles.empty}>Loading...</p>
|
|
94
|
+
) : txs.length === 0 ? (
|
|
95
|
+
<p style={styles.empty}>No transactions yet</p>
|
|
96
|
+
) : (
|
|
97
|
+
<div style={styles.list}>
|
|
98
|
+
{txs.map((tx) => (
|
|
99
|
+
<a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" style={{ ...styles.item, ...styles.link }}>
|
|
100
|
+
<div style={styles.row}>
|
|
101
|
+
<span style={styles.sig}>{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</span>
|
|
102
|
+
<span style={{ ...styles.status, ...(tx.err ? styles.failed : styles.success) }}>
|
|
103
|
+
{tx.err ? "Failed" : "Success"}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
<p style={styles.time}>{formatTime(tx.blockTime)}</p>
|
|
107
|
+
</a>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
<% } %>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useWallet } from "@lazorkit/wallet";
|
|
4
|
+
<% if (styling !== 'tailwind') { %>
|
|
5
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
6
|
+
container: { width: '100%', maxWidth: 360 },
|
|
7
|
+
title: { fontSize: 24, fontWeight: 600, letterSpacing: -0.5, color: '#0a0a0a', textAlign: 'center', margin: 0 },
|
|
8
|
+
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', textAlign: 'center', lineHeight: 1.5 },
|
|
9
|
+
button: { marginTop: 32, width: '100%', backgroundColor: '#0a0a0a', color: '#fff', padding: '14px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
10
|
+
buttonDisabled: { opacity: 0.5, cursor: 'not-allowed' },
|
|
11
|
+
hint: { marginTop: 16, fontSize: 12, color: '#a3a3a3', textAlign: 'center' },
|
|
12
|
+
error: { marginTop: 16, fontSize: 13, color: '#ef4444', textAlign: 'center' },
|
|
13
|
+
spinner: { width: 20, height: 20, border: '2px solid #fff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 1s linear infinite', margin: '0 auto' },
|
|
14
|
+
};
|
|
15
|
+
<% } %>
|
|
16
|
+
export function Onboarding() {
|
|
17
|
+
const { connect, isConnecting } = useWallet();
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
const handleStart = async () => {
|
|
21
|
+
setError(null);
|
|
22
|
+
try {
|
|
23
|
+
await connect();
|
|
24
|
+
} catch (e) {
|
|
25
|
+
setError(e instanceof Error ? e.message : "Something went wrong");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<% if (styling === 'tailwind') { %>
|
|
31
|
+
<div className="w-full max-w-sm">
|
|
32
|
+
<div className="text-center">
|
|
33
|
+
<h1 className="text-2xl font-semibold tracking-tight">Welcome</h1>
|
|
34
|
+
<p className="mt-2 text-sm text-neutral-500">
|
|
35
|
+
Create a wallet with your fingerprint or face. No seed phrases.
|
|
36
|
+
</p>
|
|
37
|
+
<button
|
|
38
|
+
onClick={handleStart}
|
|
39
|
+
disabled={isConnecting}
|
|
40
|
+
className="mt-8 w-full py-3 px-4 bg-black text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
|
|
41
|
+
>
|
|
42
|
+
{isConnecting ? "Creating..." : "Create Wallet"}
|
|
43
|
+
</button>
|
|
44
|
+
<p className="mt-4 text-xs text-neutral-400">Secured by passkeys</p>
|
|
45
|
+
{error && <p className="mt-4 text-sm text-red-500">{error}</p>}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<% } else { %>
|
|
49
|
+
<div style={styles.container}>
|
|
50
|
+
<h1 style={styles.title}>Welcome</h1>
|
|
51
|
+
<p style={styles.subtitle}>
|
|
52
|
+
Create a wallet with your fingerprint or face. No seed phrases.
|
|
53
|
+
</p>
|
|
54
|
+
<button
|
|
55
|
+
onClick={handleStart}
|
|
56
|
+
disabled={isConnecting}
|
|
57
|
+
style={{ ...styles.button, ...(isConnecting ? styles.buttonDisabled : {}) }}
|
|
58
|
+
>
|
|
59
|
+
{isConnecting ? "Creating..." : "Create Wallet"}
|
|
60
|
+
</button>
|
|
61
|
+
<p style={styles.hint}>Secured by passkeys</p>
|
|
62
|
+
{error && <p style={styles.error}>{error}</p>}
|
|
63
|
+
</div>
|
|
64
|
+
<% } %>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useWallet } from "@lazorkit/wallet";
|
|
4
|
+
<% if (styling !== 'tailwind') { %>
|
|
5
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
6
|
+
container: { width: '100%', maxWidth: 360 },
|
|
7
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
8
|
+
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', lineHeight: 1.5 },
|
|
9
|
+
card: { marginTop: 16, border: '1px solid #e5e5e5', borderRadius: 12, padding: 16 },
|
|
10
|
+
cardTitle: { fontSize: 14, fontWeight: 500, margin: 0 },
|
|
11
|
+
cardDesc: { marginTop: 4, fontSize: 13, color: '#737373' },
|
|
12
|
+
button: { marginTop: 12, width: '100%', backgroundColor: '#0a0a0a', color: '#fff', padding: '12px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
13
|
+
buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#0a0a0a', padding: '12px 16px', borderRadius: 10, border: '1px solid #e5e5e5', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
14
|
+
info: { marginTop: 16, padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8, fontSize: 12, color: '#525252' },
|
|
15
|
+
back: { marginTop: 16, fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
16
|
+
};
|
|
17
|
+
<% } %>
|
|
18
|
+
interface Props {
|
|
19
|
+
onBack: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Recovery({ onBack }: Props) {
|
|
23
|
+
const { wallet } = useWallet();
|
|
24
|
+
const [copied, setCopied] = useState(false);
|
|
25
|
+
const portalUrl = process.env.NEXT_PUBLIC_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
|
|
26
|
+
|
|
27
|
+
const handleAddDevice = () => {
|
|
28
|
+
window.open(`${portalUrl}/recovery/add-device`, "_blank");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleManageDevices = () => {
|
|
32
|
+
window.open(`${portalUrl}/recovery/devices`, "_blank");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const copyAddress = () => {
|
|
36
|
+
if (wallet?.smartWallet) {
|
|
37
|
+
navigator.clipboard.writeText(wallet.smartWallet);
|
|
38
|
+
setCopied(true);
|
|
39
|
+
setTimeout(() => setCopied(false), 2000);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<% if (styling === 'tailwind') { %>
|
|
45
|
+
<div className="w-full max-w-sm">
|
|
46
|
+
<h1 className="text-lg font-semibold">Recovery & Backup</h1>
|
|
47
|
+
<p className="mt-2 text-sm text-neutral-500">
|
|
48
|
+
Add backup passkeys from other devices to ensure you never lose access.
|
|
49
|
+
</p>
|
|
50
|
+
<div className="mt-4 border border-neutral-200 rounded-xl p-4">
|
|
51
|
+
<h3 className="text-sm font-medium">Add Backup Device</h3>
|
|
52
|
+
<p className="mt-1 text-xs text-neutral-500">
|
|
53
|
+
Register a passkey from another phone, tablet, or computer.
|
|
54
|
+
</p>
|
|
55
|
+
<button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-black text-white text-sm font-medium rounded-lg hover:opacity-90">
|
|
56
|
+
Add Device
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="mt-3 border border-neutral-200 rounded-xl p-4">
|
|
60
|
+
<h3 className="text-sm font-medium">Manage Devices</h3>
|
|
61
|
+
<p className="mt-1 text-xs text-neutral-500">
|
|
62
|
+
View and remove registered passkeys.
|
|
63
|
+
</p>
|
|
64
|
+
<button onClick={handleManageDevices} className="mt-3 w-full py-3 bg-transparent text-black text-sm font-medium rounded-lg border border-neutral-200 hover:bg-neutral-50">
|
|
65
|
+
View Devices
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="mt-4 p-3 bg-neutral-100 rounded-lg">
|
|
69
|
+
<p className="text-xs text-neutral-600">
|
|
70
|
+
<strong>Wallet:</strong>{" "}
|
|
71
|
+
<button onClick={copyAddress} className="font-mono hover:text-black">
|
|
72
|
+
{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}
|
|
73
|
+
</button>
|
|
74
|
+
{copied && <span className="ml-2 text-green-600">Copied!</span>}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
<button onClick={onBack} className="mt-4 text-sm text-neutral-500 hover:text-black">
|
|
78
|
+
← Back
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<% } else { %>
|
|
82
|
+
<div style={styles.container}>
|
|
83
|
+
<h1 style={styles.title}>Recovery & Backup</h1>
|
|
84
|
+
<p style={styles.subtitle}>
|
|
85
|
+
Add backup passkeys from other devices to ensure you never lose access.
|
|
86
|
+
</p>
|
|
87
|
+
<div style={styles.card}>
|
|
88
|
+
<h3 style={styles.cardTitle}>Add Backup Device</h3>
|
|
89
|
+
<p style={styles.cardDesc}>Register a passkey from another phone, tablet, or computer.</p>
|
|
90
|
+
<button onClick={handleAddDevice} style={styles.button}>Add Device</button>
|
|
91
|
+
</div>
|
|
92
|
+
<div style={styles.card}>
|
|
93
|
+
<h3 style={styles.cardTitle}>Manage Devices</h3>
|
|
94
|
+
<p style={styles.cardDesc}>View and remove registered passkeys.</p>
|
|
95
|
+
<button onClick={handleManageDevices} style={styles.buttonSecondary}>View Devices</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div style={styles.info}>
|
|
98
|
+
<strong>Wallet:</strong>{" "}
|
|
99
|
+
<button onClick={copyAddress} style={{ fontFamily: 'monospace', background: 'none', border: 'none', cursor: 'pointer' }}>
|
|
100
|
+
{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}
|
|
101
|
+
</button>
|
|
102
|
+
{copied && <span style={{ marginLeft: 8, color: '#16a34a' }}>Copied!</span>}
|
|
103
|
+
</div>
|
|
104
|
+
<button onClick={onBack} style={styles.back}>← Back</button>
|
|
105
|
+
</div>
|
|
106
|
+
<% } %>
|
|
107
|
+
);
|
|
108
|
+
}
|