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,112 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useWallet } from "@lazorkit/wallet";
|
|
3
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
4
|
+
<% if (styling !== 'tailwind') { %>
|
|
5
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
6
|
+
container: { width: '100%', maxWidth: 360 },
|
|
7
|
+
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
|
|
8
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
9
|
+
list: { display: 'flex', flexDirection: 'column', gap: 8 },
|
|
10
|
+
item: { border: '1px solid #e5e5e5', borderRadius: 12, padding: 12, textDecoration: 'none', color: 'inherit' },
|
|
11
|
+
row: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
|
|
12
|
+
sig: { fontFamily: 'monospace', fontSize: 13, color: '#0a0a0a' },
|
|
13
|
+
time: { fontSize: 12, color: '#a3a3a3' },
|
|
14
|
+
status: { fontSize: 11, padding: '2px 6px', borderRadius: 4 },
|
|
15
|
+
success: { backgroundColor: '#dcfce7', color: '#166534' },
|
|
16
|
+
failed: { backgroundColor: '#fee2e2', color: '#991b1b' },
|
|
17
|
+
empty: { textAlign: 'center', padding: 32, color: '#a3a3a3', fontSize: 14 },
|
|
18
|
+
back: { fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
19
|
+
};
|
|
20
|
+
<% } %>
|
|
21
|
+
interface Props {
|
|
22
|
+
onBack: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TxInfo {
|
|
26
|
+
signature: string;
|
|
27
|
+
slot: number;
|
|
28
|
+
blockTime: number | null;
|
|
29
|
+
err: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function History({ onBack }: Props) {
|
|
33
|
+
const { wallet } = useWallet();
|
|
34
|
+
const [txs, setTxs] = useState<TxInfo[]>([]);
|
|
35
|
+
const [loading, setLoading] = useState(true);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!wallet?.smartWallet) return;
|
|
39
|
+
const rpc = import.meta.env.VITE_SOLANA_RPC || "https://api.devnet.solana.com";
|
|
40
|
+
const conn = new Connection(rpc);
|
|
41
|
+
conn.getSignaturesForAddress(new PublicKey(wallet.smartWallet), { limit: 10 })
|
|
42
|
+
.then((sigs) => setTxs(sigs.map((s) => ({ signature: s.signature, slot: s.slot, blockTime: s.blockTime, err: s.err }))))
|
|
43
|
+
.catch(console.error)
|
|
44
|
+
.finally(() => setLoading(false));
|
|
45
|
+
}, [wallet?.smartWallet]);
|
|
46
|
+
|
|
47
|
+
const formatTime = (ts: number | null) => {
|
|
48
|
+
if (!ts) return "—";
|
|
49
|
+
return new Date(ts * 1000).toLocaleDateString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const explorerUrl = (sig: string) => {
|
|
53
|
+
const cluster = (import.meta.env.VITE_SOLANA_RPC || "").includes("mainnet") ? "" : "?cluster=devnet";
|
|
54
|
+
return `https://solscan.io/tx/${sig}${cluster}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<% if (styling === 'tailwind') { %>
|
|
59
|
+
<div className="w-full max-w-sm">
|
|
60
|
+
<div className="flex items-center justify-between mb-4">
|
|
61
|
+
<h1 className="text-lg font-semibold">History</h1>
|
|
62
|
+
<button onClick={onBack} className="text-sm text-neutral-500 hover:text-black">← Back</button>
|
|
63
|
+
</div>
|
|
64
|
+
{loading ? (
|
|
65
|
+
<p className="text-center py-8 text-neutral-400">Loading...</p>
|
|
66
|
+
) : txs.length === 0 ? (
|
|
67
|
+
<p className="text-center py-8 text-neutral-400">No transactions yet</p>
|
|
68
|
+
) : (
|
|
69
|
+
<div className="space-y-2">
|
|
70
|
+
{txs.map((tx) => (
|
|
71
|
+
<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">
|
|
72
|
+
<div className="flex justify-between items-center">
|
|
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-100 text-red-700" : "bg-green-100 text-green-700"}`}>
|
|
75
|
+
{tx.err ? "Failed" : "Success"}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
<p className="text-xs text-neutral-400 mt-1">{formatTime(tx.blockTime)}</p>
|
|
79
|
+
</a>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
<% } else { %>
|
|
85
|
+
<div style={styles.container}>
|
|
86
|
+
<div style={styles.header}>
|
|
87
|
+
<h1 style={styles.title}>History</h1>
|
|
88
|
+
<button onClick={onBack} style={styles.back}>← Back</button>
|
|
89
|
+
</div>
|
|
90
|
+
{loading ? (
|
|
91
|
+
<p style={styles.empty}>Loading...</p>
|
|
92
|
+
) : txs.length === 0 ? (
|
|
93
|
+
<p style={styles.empty}>No transactions yet</p>
|
|
94
|
+
) : (
|
|
95
|
+
<div style={styles.list}>
|
|
96
|
+
{txs.map((tx) => (
|
|
97
|
+
<a key={tx.signature} href={explorerUrl(tx.signature)} target="_blank" rel="noopener noreferrer" style={styles.item}>
|
|
98
|
+
<div style={styles.row}>
|
|
99
|
+
<span style={styles.sig}>{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</span>
|
|
100
|
+
<span style={{ ...styles.status, ...(tx.err ? styles.failed : styles.success) }}>
|
|
101
|
+
{tx.err ? "Failed" : "Success"}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
<p style={styles.time}>{formatTime(tx.blockTime)}</p>
|
|
105
|
+
</a>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
<% } %>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useWallet } from '@lazorkit/wallet'
|
|
3
|
+
<% if (styling !== 'tailwind') { %>
|
|
4
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
5
|
+
container: { width: '100%', maxWidth: 360 },
|
|
6
|
+
title: { fontSize: 24, fontWeight: 600, letterSpacing: -0.5, color: '#0a0a0a', textAlign: 'center', margin: 0 },
|
|
7
|
+
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', textAlign: 'center', lineHeight: 1.5 },
|
|
8
|
+
button: { marginTop: 32, width: '100%', backgroundColor: '#0a0a0a', color: '#fff', padding: '14px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
9
|
+
buttonDisabled: { opacity: 0.5, cursor: 'not-allowed' },
|
|
10
|
+
hint: { marginTop: 16, fontSize: 12, color: '#a3a3a3', textAlign: 'center' },
|
|
11
|
+
error: { marginTop: 16, fontSize: 13, color: '#ef4444', textAlign: 'center' },
|
|
12
|
+
}
|
|
13
|
+
<% } %>
|
|
14
|
+
export function Onboarding() {
|
|
15
|
+
const { connect, isConnecting } = useWallet()
|
|
16
|
+
const [error, setError] = useState<string | null>(null)
|
|
17
|
+
|
|
18
|
+
const handleStart = async () => {
|
|
19
|
+
setError(null)
|
|
20
|
+
try {
|
|
21
|
+
await connect()
|
|
22
|
+
} catch (e) {
|
|
23
|
+
setError(e instanceof Error ? e.message : 'Something went wrong')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<% if (styling === 'tailwind') { %>
|
|
29
|
+
<div className="w-full max-w-sm">
|
|
30
|
+
<div className="text-center">
|
|
31
|
+
<h1 className="text-2xl font-semibold tracking-tight">Welcome</h1>
|
|
32
|
+
<p className="mt-2 text-sm text-neutral-500">
|
|
33
|
+
Create a wallet with your fingerprint or face. No seed phrases.
|
|
34
|
+
</p>
|
|
35
|
+
<button
|
|
36
|
+
onClick={handleStart}
|
|
37
|
+
disabled={isConnecting}
|
|
38
|
+
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"
|
|
39
|
+
>
|
|
40
|
+
{isConnecting ? 'Creating...' : 'Create Wallet'}
|
|
41
|
+
</button>
|
|
42
|
+
<p className="mt-4 text-xs text-neutral-400">Secured by passkeys</p>
|
|
43
|
+
{error && <p className="mt-4 text-sm text-red-500">{error}</p>}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<% } else { %>
|
|
47
|
+
<div style={styles.container}>
|
|
48
|
+
<h1 style={styles.title}>Welcome</h1>
|
|
49
|
+
<p style={styles.subtitle}>
|
|
50
|
+
Create a wallet with your fingerprint or face. No seed phrases.
|
|
51
|
+
</p>
|
|
52
|
+
<button
|
|
53
|
+
onClick={handleStart}
|
|
54
|
+
disabled={isConnecting}
|
|
55
|
+
style={{ ...styles.button, ...(isConnecting ? styles.buttonDisabled : {}) }}
|
|
56
|
+
>
|
|
57
|
+
{isConnecting ? 'Creating...' : 'Create Wallet'}
|
|
58
|
+
</button>
|
|
59
|
+
<p style={styles.hint}>Secured by passkeys</p>
|
|
60
|
+
{error && <p style={styles.error}>{error}</p>}
|
|
61
|
+
</div>
|
|
62
|
+
<% } %>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useWallet } from "@lazorkit/wallet";
|
|
3
|
+
<% if (styling !== 'tailwind') { %>
|
|
4
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
5
|
+
container: { width: '100%', maxWidth: 360 },
|
|
6
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
7
|
+
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', lineHeight: 1.5 },
|
|
8
|
+
card: { marginTop: 16, border: '1px solid #e5e5e5', borderRadius: 12, padding: 16 },
|
|
9
|
+
cardTitle: { fontSize: 14, fontWeight: 500, margin: 0 },
|
|
10
|
+
cardDesc: { marginTop: 4, fontSize: 13, color: '#737373' },
|
|
11
|
+
button: { marginTop: 12, width: '100%', backgroundColor: '#0a0a0a', color: '#fff', padding: '12px 16px', borderRadius: 10, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
12
|
+
buttonSecondary: { marginTop: 12, width: '100%', backgroundColor: 'transparent', color: '#0a0a0a', padding: '12px 16px', borderRadius: 10, border: '1px solid #e5e5e5', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
13
|
+
info: { marginTop: 16, padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8, fontSize: 12, color: '#525252' },
|
|
14
|
+
back: { marginTop: 16, fontSize: 13, color: '#737373', background: 'none', border: 'none', cursor: 'pointer' },
|
|
15
|
+
};
|
|
16
|
+
<% } %>
|
|
17
|
+
interface Props {
|
|
18
|
+
onBack: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Recovery({ onBack }: Props) {
|
|
22
|
+
const { wallet } = useWallet();
|
|
23
|
+
const [copied, setCopied] = useState(false);
|
|
24
|
+
const portalUrl = import.meta.env.VITE_LAZORKIT_PORTAL_URL || "https://portal.lazor.sh";
|
|
25
|
+
|
|
26
|
+
const handleAddDevice = () => {
|
|
27
|
+
window.open(`${portalUrl}/recovery/add-device`, "_blank");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleManageDevices = () => {
|
|
31
|
+
window.open(`${portalUrl}/recovery/devices`, "_blank");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const copyAddress = () => {
|
|
35
|
+
if (wallet?.smartWallet) {
|
|
36
|
+
navigator.clipboard.writeText(wallet.smartWallet);
|
|
37
|
+
setCopied(true);
|
|
38
|
+
setTimeout(() => setCopied(false), 2000);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<% if (styling === 'tailwind') { %>
|
|
44
|
+
<div className="w-full max-w-sm">
|
|
45
|
+
<h1 className="text-lg font-semibold">Recovery & Backup</h1>
|
|
46
|
+
<p className="mt-2 text-sm text-neutral-500">
|
|
47
|
+
Add backup passkeys from other devices to ensure you never lose access.
|
|
48
|
+
</p>
|
|
49
|
+
<div className="mt-4 border border-neutral-200 rounded-xl p-4">
|
|
50
|
+
<h3 className="text-sm font-medium">Add Backup Device</h3>
|
|
51
|
+
<p className="mt-1 text-xs text-neutral-500">
|
|
52
|
+
Register a passkey from another phone, tablet, or computer.
|
|
53
|
+
</p>
|
|
54
|
+
<button onClick={handleAddDevice} className="mt-3 w-full py-3 bg-black text-white text-sm font-medium rounded-lg hover:opacity-90">
|
|
55
|
+
Add Device
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="mt-3 border border-neutral-200 rounded-xl p-4">
|
|
59
|
+
<h3 className="text-sm font-medium">Manage Devices</h3>
|
|
60
|
+
<p className="mt-1 text-xs text-neutral-500">
|
|
61
|
+
View and remove registered passkeys.
|
|
62
|
+
</p>
|
|
63
|
+
<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">
|
|
64
|
+
View Devices
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="mt-4 p-3 bg-neutral-100 rounded-lg">
|
|
68
|
+
<p className="text-xs text-neutral-600">
|
|
69
|
+
<strong>Wallet:</strong>{" "}
|
|
70
|
+
<button onClick={copyAddress} className="font-mono hover:text-black">
|
|
71
|
+
{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}
|
|
72
|
+
</button>
|
|
73
|
+
{copied && <span className="ml-2 text-green-600">Copied!</span>}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
<button onClick={onBack} className="mt-4 text-sm text-neutral-500 hover:text-black">
|
|
77
|
+
← Back
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<% } else { %>
|
|
81
|
+
<div style={styles.container}>
|
|
82
|
+
<h1 style={styles.title}>Recovery & Backup</h1>
|
|
83
|
+
<p style={styles.subtitle}>
|
|
84
|
+
Add backup passkeys from other devices to ensure you never lose access.
|
|
85
|
+
</p>
|
|
86
|
+
<div style={styles.card}>
|
|
87
|
+
<h3 style={styles.cardTitle}>Add Backup Device</h3>
|
|
88
|
+
<p style={styles.cardDesc}>Register a passkey from another phone, tablet, or computer.</p>
|
|
89
|
+
<button onClick={handleAddDevice} style={styles.button}>Add Device</button>
|
|
90
|
+
</div>
|
|
91
|
+
<div style={styles.card}>
|
|
92
|
+
<h3 style={styles.cardTitle}>Manage Devices</h3>
|
|
93
|
+
<p style={styles.cardDesc}>View and remove registered passkeys.</p>
|
|
94
|
+
<button onClick={handleManageDevices} style={styles.buttonSecondary}>View Devices</button>
|
|
95
|
+
</div>
|
|
96
|
+
<div style={styles.info}>
|
|
97
|
+
<strong>Wallet:</strong>{" "}
|
|
98
|
+
<button onClick={copyAddress} style={{ fontFamily: 'monospace', background: 'none', border: 'none', cursor: 'pointer' }}>
|
|
99
|
+
{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}
|
|
100
|
+
</button>
|
|
101
|
+
{copied && <span style={{ marginLeft: 8, color: '#16a34a' }}>Copied!</span>}
|
|
102
|
+
</div>
|
|
103
|
+
<button onClick={onBack} style={styles.back}>← Back</button>
|
|
104
|
+
</div>
|
|
105
|
+
<% } %>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useWallet } from '@lazorkit/wallet'
|
|
3
|
+
import { Connection, PublicKey, VersionedTransaction } from '@solana/web3.js'
|
|
4
|
+
|
|
5
|
+
const TOKENS = [
|
|
6
|
+
{ symbol: 'SOL', mint: 'So11111111111111111111111111111111111111112', decimals: 9 },
|
|
7
|
+
{ symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6 },
|
|
8
|
+
{ symbol: 'USDT', mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', decimals: 6 },
|
|
9
|
+
{ symbol: 'BONK', mint: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', decimals: 5 },
|
|
10
|
+
{ symbol: 'JUP', mint: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', decimals: 6 },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const SLIPPAGE_OPTIONS = [50, 100, 300]
|
|
14
|
+
|
|
15
|
+
const JUPITER_API = 'https://api.jup.ag/swap/v1'
|
|
16
|
+
|
|
17
|
+
function useBalances(walletAddress: string | undefined) {
|
|
18
|
+
const [balances, setBalances] = useState<Record<string, number>>({})
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!walletAddress) return
|
|
21
|
+
const rpc = import.meta.env.VITE_SOLANA_RPC || 'https://api.devnet.solana.com'
|
|
22
|
+
const conn = new Connection(rpc)
|
|
23
|
+
const pk = new PublicKey(walletAddress)
|
|
24
|
+
|
|
25
|
+
conn.getBalance(pk).then((bal) => setBalances((b) => ({ ...b, SOL: bal / 1e9 }))).catch(() => {})
|
|
26
|
+
|
|
27
|
+
conn.getParsedTokenAccountsByOwner(pk, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') })
|
|
28
|
+
.then((res) => {
|
|
29
|
+
const tokenBals: Record<string, number> = {}
|
|
30
|
+
res.value.forEach((acc) => {
|
|
31
|
+
const info = acc.account.data.parsed.info
|
|
32
|
+
const token = TOKENS.find((t) => t.mint === info.mint)
|
|
33
|
+
if (token) tokenBals[token.symbol] = info.tokenAmount.uiAmount || 0
|
|
34
|
+
})
|
|
35
|
+
setBalances((b) => ({ ...b, ...tokenBals }))
|
|
36
|
+
}).catch(() => {})
|
|
37
|
+
}, [walletAddress])
|
|
38
|
+
return balances
|
|
39
|
+
}
|
|
40
|
+
<% if (styling !== 'tailwind') { %>
|
|
41
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
42
|
+
container: { width: '100%', maxWidth: 360 },
|
|
43
|
+
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 },
|
|
44
|
+
title: { fontSize: 18, fontWeight: 600, margin: 0 },
|
|
45
|
+
address: { fontSize: 12, color: '#a3a3a3', background: 'none', border: 'none', cursor: 'pointer' },
|
|
46
|
+
card: { border: '1px solid #e5e5e5', borderRadius: 12, padding: 16 },
|
|
47
|
+
label: { fontSize: 12, color: '#a3a3a3', marginBottom: 8, display: 'flex', justifyContent: 'space-between' },
|
|
48
|
+
inputRow: { display: 'flex', alignItems: 'center', gap: 12 },
|
|
49
|
+
input: { flex: 1, fontSize: 24, fontWeight: 500, border: 'none', background: 'transparent', width: '100%' },
|
|
50
|
+
arrow: { display: 'flex', justifyContent: 'center', margin: '8px 0' },
|
|
51
|
+
arrowBox: { width: 32, height: 32, border: '1px solid #e5e5e5', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#a3a3a3' },
|
|
52
|
+
button: { marginTop: 16, width: '100%', backgroundColor: '#0a0a0a', color: '#fff', padding: '14px 16px', borderRadius: 12, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
53
|
+
buttonDisabled: { opacity: 0.5, cursor: 'not-allowed' },
|
|
54
|
+
buttonSecondary: { marginTop: 8, width: '100%', backgroundColor: 'transparent', color: '#0a0a0a', padding: '12px 16px', borderRadius: 12, border: '1px solid #e5e5e5', fontSize: 13, fontWeight: 500, cursor: 'pointer' },
|
|
55
|
+
result: { marginTop: 12, fontSize: 14, textAlign: 'center' },
|
|
56
|
+
footer: { marginTop: 16, fontSize: 12, color: '#a3a3a3', textAlign: 'center' },
|
|
57
|
+
tokenBtn: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: '#f5f5f5', borderRadius: 8, border: 'none', fontSize: 14, fontWeight: 500, cursor: 'pointer' },
|
|
58
|
+
dropdown: { position: 'absolute', right: 0, marginTop: 4, background: '#fff', border: '1px solid #e5e5e5', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.1)', zIndex: 10 },
|
|
59
|
+
dropdownItem: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', border: 'none', background: 'none', fontSize: 14, cursor: 'pointer' },
|
|
60
|
+
quote: { fontSize: 12, color: '#737373', marginTop: 4 },
|
|
61
|
+
}
|
|
62
|
+
<% } %>
|
|
63
|
+
export function Swap() {
|
|
64
|
+
const { disconnect, wallet, smartWalletPubkey, signAndSendTransaction, signMessage } = useWallet()
|
|
65
|
+
const balances = useBalances(wallet?.smartWallet)
|
|
66
|
+
const [fromToken, setFromToken] = useState(TOKENS[0])
|
|
67
|
+
const [toToken, setToToken] = useState(TOKENS[1])
|
|
68
|
+
const [amount, setAmount] = useState('')
|
|
69
|
+
const [slippage, setSlippage] = useState(SLIPPAGE_OPTIONS[0])
|
|
70
|
+
const [quote, setQuote] = useState<any>(null)
|
|
71
|
+
const [loading, setLoading] = useState(false)
|
|
72
|
+
const [signing, setSigning] = useState(false)
|
|
73
|
+
const [result, setResult] = useState<{ success: boolean; message: string } | null>(null)
|
|
74
|
+
|
|
75
|
+
const switchTokens = () => {
|
|
76
|
+
setFromToken(toToken)
|
|
77
|
+
setToToken(fromToken)
|
|
78
|
+
setAmount('')
|
|
79
|
+
setQuote(null)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!amount || parseFloat(amount) <= 0) { setQuote(null); return }
|
|
84
|
+
const timeout = setTimeout(async () => {
|
|
85
|
+
try {
|
|
86
|
+
const inputAmount = Math.floor(parseFloat(amount) * 10 ** fromToken.decimals)
|
|
87
|
+
const res = await fetch(`${JUPITER_API}/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${inputAmount}&slippageBps=${slippage}`)
|
|
88
|
+
const data = await res.json()
|
|
89
|
+
if (data.outAmount) setQuote(data)
|
|
90
|
+
} catch (e) { console.error('Quote error:', e) }
|
|
91
|
+
}, 500)
|
|
92
|
+
return () => clearTimeout(timeout)
|
|
93
|
+
}, [amount, fromToken, toToken, slippage])
|
|
94
|
+
|
|
95
|
+
const outputAmount = quote ? (parseInt(quote.outAmount) / 10 ** toToken.decimals).toFixed(toToken.decimals === 6 ? 2 : 4) : '0.00'
|
|
96
|
+
|
|
97
|
+
const handleSwap = async () => {
|
|
98
|
+
if (!smartWalletPubkey || !quote) return
|
|
99
|
+
setLoading(true)
|
|
100
|
+
setResult(null)
|
|
101
|
+
try {
|
|
102
|
+
const swapRes = await fetch(`${JUPITER_API}/swap`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify({ quoteResponse: quote, userPublicKey: smartWalletPubkey.toBase58(), dynamicComputeUnitLimit: true, dynamicSlippage: true }),
|
|
106
|
+
})
|
|
107
|
+
const { swapTransaction } = await swapRes.json()
|
|
108
|
+
const signature = await signAndSendTransaction({ instructions: [], transactionOptions: { feeToken: 'USDC' } })
|
|
109
|
+
setResult({ success: true, message: `Swapped! ${signature.slice(0, 8)}...` })
|
|
110
|
+
setAmount('')
|
|
111
|
+
setQuote(null)
|
|
112
|
+
} catch (e) {
|
|
113
|
+
setResult({ success: false, message: e instanceof Error ? e.message : 'Swap failed' })
|
|
114
|
+
} finally { setLoading(false) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleSignMessage = async () => {
|
|
118
|
+
setSigning(true)
|
|
119
|
+
setResult(null)
|
|
120
|
+
try {
|
|
121
|
+
const message = `Verify wallet ownership\nTimestamp: ${Date.now()}`
|
|
122
|
+
const { signature } = await signMessage(message)
|
|
123
|
+
setResult({ success: true, message: `Signed: ${signature.slice(0, 16)}...` })
|
|
124
|
+
} catch (e) {
|
|
125
|
+
setResult({ success: false, message: e instanceof Error ? e.message : 'Sign failed' })
|
|
126
|
+
} finally { setSigning(false) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const addr = wallet?.smartWallet || ''
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<% if (styling === 'tailwind') { %>
|
|
133
|
+
<div className="w-full max-w-sm">
|
|
134
|
+
<div className="flex items-center justify-between mb-6">
|
|
135
|
+
<h1 className="text-lg font-semibold">Swap</h1>
|
|
136
|
+
<button onClick={() => disconnect()} className="text-xs text-neutral-400 hover:text-neutral-600">{addr.slice(0, 6)}...{addr.slice(-4)}</button>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
<div className="border border-neutral-200 rounded-xl p-4">
|
|
140
|
+
<div className="flex items-center justify-between mb-2">
|
|
141
|
+
<span className="text-xs text-neutral-400">From</span>
|
|
142
|
+
<span className="text-xs text-neutral-400">Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex items-center gap-3">
|
|
145
|
+
<input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} className="flex-1 text-2xl font-medium bg-transparent w-full focus:outline-none" />
|
|
146
|
+
<TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
|
|
147
|
+
</div>
|
|
148
|
+
<button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} className="text-xs text-neutral-400 hover:text-black mt-1">Max</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex justify-center"><button onClick={switchTokens} className="w-8 h-8 border border-neutral-200 rounded-lg flex items-center justify-center text-neutral-400 hover:bg-neutral-50">↓</button></div>
|
|
151
|
+
<div className="border border-neutral-200 rounded-xl p-4">
|
|
152
|
+
<div className="flex items-center justify-between mb-2">
|
|
153
|
+
<span className="text-xs text-neutral-400">To</span>
|
|
154
|
+
<span className="text-xs text-neutral-400">Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="flex items-center gap-3">
|
|
157
|
+
<span className="flex-1 text-2xl font-medium text-neutral-300">{outputAmount}</span>
|
|
158
|
+
<TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
|
|
159
|
+
</div>
|
|
160
|
+
{quote && <p className="text-xs text-neutral-400 mt-2">via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</p>}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div className="flex items-center justify-between mt-3 text-xs">
|
|
164
|
+
<span className="text-neutral-400">Slippage</span>
|
|
165
|
+
<div className="flex gap-1">
|
|
166
|
+
{SLIPPAGE_OPTIONS.map((s) => (
|
|
167
|
+
<button key={s} onClick={() => setSlippage(s)} className={`px-2 py-1 rounded ${slippage === s ? 'bg-black text-white' : 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'}`}>{s / 100}%</button>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<button onClick={handleSwap} disabled={loading || !quote} className="mt-4 w-full py-3 bg-black text-white text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50">{loading ? 'Swapping...' : 'Swap'}</button>
|
|
172
|
+
<button onClick={handleSignMessage} disabled={signing} className="mt-2 w-full py-3 bg-transparent text-black text-sm font-medium rounded-xl border border-neutral-200 hover:bg-neutral-50 disabled:opacity-50">{signing ? 'Signing...' : 'Sign Message'}</button>
|
|
173
|
+
{result && <p className={`mt-3 text-sm text-center ${result.success ? 'text-neutral-500' : 'text-red-500'}`}>{result.message}</p>}
|
|
174
|
+
<p className="mt-4 text-xs text-center text-neutral-400">Gas sponsored · Powered by LazorKit + Jupiter</p>
|
|
175
|
+
</div>
|
|
176
|
+
<% } else { %>
|
|
177
|
+
<div style={styles.container}>
|
|
178
|
+
<div style={styles.header}>
|
|
179
|
+
<h1 style={styles.title}>Swap</h1>
|
|
180
|
+
<button onClick={() => disconnect()} style={styles.address}>{addr.slice(0, 6)}...{addr.slice(-4)}</button>
|
|
181
|
+
</div>
|
|
182
|
+
<div style={styles.card}>
|
|
183
|
+
<div style={styles.label}><span>From</span><span>Balance: {(balances[fromToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
184
|
+
<div style={styles.inputRow}>
|
|
185
|
+
<input type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} style={styles.input} />
|
|
186
|
+
<TokenSelect token={fromToken} tokens={TOKENS} onChange={setFromToken} exclude={toToken.symbol} />
|
|
187
|
+
</div>
|
|
188
|
+
<button onClick={() => setAmount(String(balances[fromToken.symbol] ?? 0))} style={{ ...styles.address, fontSize: 12, marginTop: 4 }}>Max</button>
|
|
189
|
+
</div>
|
|
190
|
+
<div style={styles.arrow}><button onClick={switchTokens} style={{ ...styles.arrowBox, cursor: 'pointer', border: '1px solid #e5e5e5' }}>↓</button></div>
|
|
191
|
+
<div style={styles.card}>
|
|
192
|
+
<div style={styles.label}><span>To</span><span>Balance: {(balances[toToken.symbol] ?? 0).toFixed(4)}</span></div>
|
|
193
|
+
<div style={styles.inputRow}>
|
|
194
|
+
<span style={{ ...styles.input, color: '#d4d4d4' }}>{outputAmount}</span>
|
|
195
|
+
<TokenSelect token={toToken} tokens={TOKENS} onChange={setToToken} exclude={fromToken.symbol} />
|
|
196
|
+
</div>
|
|
197
|
+
{quote && <p style={styles.quote}>via Jupiter · {quote.routePlan?.[0]?.swapInfo?.label || 'Best route'}</p>}
|
|
198
|
+
</div>
|
|
199
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, fontSize: 12 }}>
|
|
200
|
+
<span style={{ color: '#a3a3a3' }}>Slippage</span>
|
|
201
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
202
|
+
{SLIPPAGE_OPTIONS.map((s) => (
|
|
203
|
+
<button key={s} onClick={() => setSlippage(s)} style={{ padding: '4px 8px', borderRadius: 4, border: 'none', cursor: 'pointer', background: slippage === s ? '#0a0a0a' : '#f5f5f5', color: slippage === s ? '#fff' : '#525252' }}>{s / 100}%</button>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<button onClick={handleSwap} disabled={loading || !quote} style={{ ...styles.button, ...(loading || !quote ? styles.buttonDisabled : {}) }}>{loading ? 'Swapping...' : 'Swap'}</button>
|
|
208
|
+
<button onClick={handleSignMessage} disabled={signing} style={{ ...styles.buttonSecondary, ...(signing ? styles.buttonDisabled : {}) }}>{signing ? 'Signing...' : 'Sign Message'}</button>
|
|
209
|
+
{result && <p style={{ ...styles.result, color: result.success ? '#737373' : '#ef4444' }}>{result.message}</p>}
|
|
210
|
+
<p style={styles.footer}>Gas sponsored · Powered by LazorKit + Jupiter</p>
|
|
211
|
+
</div>
|
|
212
|
+
<% } %>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function TokenSelect({ token, tokens, onChange, exclude }: { token: typeof TOKENS[0]; tokens: typeof TOKENS; onChange: (t: typeof TOKENS[0]) => void; exclude: string }) {
|
|
217
|
+
const [open, setOpen] = useState(false)
|
|
218
|
+
return (
|
|
219
|
+
<div style={{ position: 'relative' }}>
|
|
220
|
+
<% if (styling === 'tailwind') { %>
|
|
221
|
+
<button onClick={() => setOpen(!open)} className="flex items-center gap-2 px-3 py-2 bg-neutral-100 rounded-lg text-sm font-medium">{token.symbol}<span className="text-neutral-400">▼</span></button>
|
|
222
|
+
{open && (
|
|
223
|
+
<div className="absolute right-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-10">
|
|
224
|
+
{tokens.filter((t) => t.symbol !== exclude).map((t) => (
|
|
225
|
+
<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} className="block w-full px-4 py-2 text-left text-sm hover:bg-neutral-100">{t.symbol}</button>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
<% } else { %>
|
|
230
|
+
<button onClick={() => setOpen(!open)} style={styles.tokenBtn}>{token.symbol} <span style={{ color: '#a3a3a3' }}>▼</span></button>
|
|
231
|
+
{open && (
|
|
232
|
+
<div style={styles.dropdown as React.CSSProperties}>
|
|
233
|
+
{tokens.filter((t) => t.symbol !== exclude).map((t) => (
|
|
234
|
+
<button key={t.symbol} onClick={() => { onChange(t); setOpen(false) }} style={styles.dropdownItem}>{t.symbol}</button>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
<% } %>
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<% if (styling === 'tailwind') { %>
|
|
2
|
+
@tailwind base;
|
|
3
|
+
@tailwind components;
|
|
4
|
+
@tailwind utilities;
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
--background: #ffffff;
|
|
8
|
+
--foreground: #0a0a0a;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@media (prefers-color-scheme: dark) {
|
|
12
|
+
:root {
|
|
13
|
+
--background: #0a0a0a;
|
|
14
|
+
--foreground: #fafafa;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
background: var(--background);
|
|
20
|
+
color: var(--foreground);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
input:focus {
|
|
24
|
+
outline: none;
|
|
25
|
+
}
|
|
26
|
+
<% } else { %>
|
|
27
|
+
* {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:root {
|
|
34
|
+
--background: #ffffff;
|
|
35
|
+
--foreground: #0a0a0a;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@media (prefers-color-scheme: dark) {
|
|
39
|
+
:root {
|
|
40
|
+
--background: #0a0a0a;
|
|
41
|
+
--foreground: #fafafa;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
body {
|
|
46
|
+
background: var(--background);
|
|
47
|
+
color: var(--foreground);
|
|
48
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
49
|
+
-webkit-font-smoothing: antialiased;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
input:focus {
|
|
53
|
+
outline: none;
|
|
54
|
+
}
|
|
55
|
+
<% } %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { LazorkitProvider } from '@lazorkit/wallet'
|
|
4
|
+
import App from './App'
|
|
5
|
+
import './index.css'
|
|
6
|
+
|
|
7
|
+
const CONFIG = {
|
|
8
|
+
rpcUrl: import.meta.env.VITE_SOLANA_RPC || 'https://api.devnet.solana.com',
|
|
9
|
+
portalUrl: import.meta.env.VITE_LAZORKIT_PORTAL_URL || 'https://portal.lazor.sh',
|
|
10
|
+
paymasterConfig: {
|
|
11
|
+
paymasterUrl: import.meta.env.VITE_LAZORKIT_PAYMASTER_URL || 'https://kora.devnet.lazorkit.com',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createRoot(document.getElementById('root')!).render(
|
|
16
|
+
<StrictMode>
|
|
17
|
+
<LazorkitProvider
|
|
18
|
+
rpcUrl={CONFIG.rpcUrl}
|
|
19
|
+
portalUrl={CONFIG.portalUrl}
|
|
20
|
+
paymasterConfig={CONFIG.paymasterConfig}
|
|
21
|
+
>
|
|
22
|
+
<App />
|
|
23
|
+
</LazorkitProvider>
|
|
24
|
+
</StrictMode>,
|
|
25
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|