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
|
@@ -5,12 +5,113 @@ const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || process.env.NEX
|
|
|
5
5
|
|
|
6
6
|
export const supabase = createClient(supabaseUrl, supabaseKey);
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
export
|
|
10
|
-
|
|
8
|
+
// Types
|
|
9
|
+
export interface User {
|
|
10
|
+
id: string;
|
|
11
|
+
wallet_address: string;
|
|
12
|
+
created_at: string;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
export interface SwapRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
wallet_address: string;
|
|
18
|
+
from_token: string;
|
|
19
|
+
to_token: string;
|
|
20
|
+
from_amount: string;
|
|
21
|
+
to_amount: string;
|
|
22
|
+
signature: string;
|
|
23
|
+
created_at: string;
|
|
16
24
|
}
|
|
25
|
+
|
|
26
|
+
// User Management
|
|
27
|
+
export async function createOrGetUser(walletAddress: string) {
|
|
28
|
+
const { data: existing } = await supabase
|
|
29
|
+
.from("users")
|
|
30
|
+
.select("*")
|
|
31
|
+
.eq("wallet_address", walletAddress)
|
|
32
|
+
.single();
|
|
33
|
+
|
|
34
|
+
if (existing) return existing;
|
|
35
|
+
|
|
36
|
+
const { data, error } = await supabase
|
|
37
|
+
.from("users")
|
|
38
|
+
.insert({ wallet_address: walletAddress })
|
|
39
|
+
.select()
|
|
40
|
+
.single();
|
|
41
|
+
|
|
42
|
+
if (error) throw error;
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Swap History
|
|
47
|
+
export async function saveSwap(swap: Omit<SwapRecord, "id" | "created_at">) {
|
|
48
|
+
const { data, error } = await supabase.from("swaps").insert(swap).select().single();
|
|
49
|
+
if (error) throw error;
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getSwapHistory(walletAddress: string, limit = 20) {
|
|
54
|
+
const { data, error } = await supabase
|
|
55
|
+
.from("swaps")
|
|
56
|
+
.select("*")
|
|
57
|
+
.eq("wallet_address", walletAddress)
|
|
58
|
+
.order("created_at", { ascending: false })
|
|
59
|
+
.limit(limit);
|
|
60
|
+
|
|
61
|
+
if (error) throw error;
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Message Signatures (for verification records)
|
|
66
|
+
export async function saveSignature(walletAddress: string, message: string, signature: string) {
|
|
67
|
+
const { data, error } = await supabase
|
|
68
|
+
.from("signatures")
|
|
69
|
+
.insert({ wallet_address: walletAddress, message, signature })
|
|
70
|
+
.select()
|
|
71
|
+
.single();
|
|
72
|
+
|
|
73
|
+
if (error) throw error;
|
|
74
|
+
return data;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
SQL Schema - Run in Supabase SQL Editor:
|
|
79
|
+
|
|
80
|
+
create table users (
|
|
81
|
+
id uuid default gen_random_uuid() primary key,
|
|
82
|
+
wallet_address text unique not null,
|
|
83
|
+
created_at timestamp with time zone default now()
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
create table swaps (
|
|
87
|
+
id uuid default gen_random_uuid() primary key,
|
|
88
|
+
wallet_address text not null references users(wallet_address),
|
|
89
|
+
from_token text not null,
|
|
90
|
+
to_token text not null,
|
|
91
|
+
from_amount text not null,
|
|
92
|
+
to_amount text not null,
|
|
93
|
+
signature text not null,
|
|
94
|
+
created_at timestamp with time zone default now()
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
create table signatures (
|
|
98
|
+
id uuid default gen_random_uuid() primary key,
|
|
99
|
+
wallet_address text not null references users(wallet_address),
|
|
100
|
+
message text not null,
|
|
101
|
+
signature text not null,
|
|
102
|
+
created_at timestamp with time zone default now()
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
-- Enable RLS
|
|
106
|
+
alter table users enable row level security;
|
|
107
|
+
alter table swaps enable row level security;
|
|
108
|
+
alter table signatures enable row level security;
|
|
109
|
+
|
|
110
|
+
-- Policies (adjust based on your auth setup)
|
|
111
|
+
create policy "Users can read own data" on users for select using (true);
|
|
112
|
+
create policy "Users can insert" on users for insert with check (true);
|
|
113
|
+
create policy "Swaps readable by wallet owner" on swaps for select using (true);
|
|
114
|
+
create policy "Swaps insertable" on swaps for insert with check (true);
|
|
115
|
+
create policy "Signatures readable" on signatures for select using (true);
|
|
116
|
+
create policy "Signatures insertable" on signatures for insert with check (true);
|
|
117
|
+
*/
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
# LazorKit Configuration
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
NEXT_PUBLIC_LAZORKIT_PORTAL_URL=https://portal.lazor.sh
|
|
3
|
+
NEXT_PUBLIC_LAZORKIT_PAYMASTER_URL=https://kora.devnet.lazorkit.com
|
|
4
|
+
EXPO_PUBLIC_LAZORKIT_PORTAL_URL=https://portal.lazor.sh
|
|
5
|
+
EXPO_PUBLIC_LAZORKIT_PAYMASTER_URL=https://kora.devnet.lazorkit.com
|
|
5
6
|
|
|
6
7
|
# Solana
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
NEXT_PUBLIC_SOLANA_RPC=https://api.devnet.solana.com
|
|
9
|
+
EXPO_PUBLIC_SOLANA_RPC=https://api.devnet.solana.com
|
|
10
|
+
VITE_SOLANA_RPC=https://api.devnet.solana.com
|
|
9
11
|
<% if (backend === 'supabase') { %>
|
|
10
12
|
# Supabase
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
|
14
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
15
|
+
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
|
16
|
+
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
13
17
|
<% } %><% if (backend === 'firebase') { %>
|
|
14
18
|
# Firebase
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
NEXT_PUBLIC_FIREBASE_API_KEY=your_firebase_api_key
|
|
20
|
+
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
|
21
|
+
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
|
|
22
|
+
EXPO_PUBLIC_FIREBASE_API_KEY=your_firebase_api_key
|
|
23
|
+
EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
|
24
|
+
EXPO_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
|
|
18
25
|
<% } %>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<% if (backend === 'supabase') { %>
|
|
2
|
+
import { supabase, createOrGetUser, saveSwap, getSwapHistory, saveSignature } from "./supabase";
|
|
3
|
+
|
|
4
|
+
export { supabase, createOrGetUser, saveSwap, getSwapHistory, saveSignature };
|
|
5
|
+
|
|
6
|
+
// Hook to sync wallet on connect
|
|
7
|
+
export async function onWalletConnect(walletAddress: string) {
|
|
8
|
+
return createOrGetUser(walletAddress);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Hook to save swap after successful transaction
|
|
12
|
+
export async function onSwapComplete(
|
|
13
|
+
walletAddress: string,
|
|
14
|
+
fromToken: string,
|
|
15
|
+
toToken: string,
|
|
16
|
+
fromAmount: string,
|
|
17
|
+
toAmount: string,
|
|
18
|
+
signature: string
|
|
19
|
+
) {
|
|
20
|
+
return saveSwap({ wallet_address: walletAddress, from_token: fromToken, to_token: toToken, from_amount: fromAmount, to_amount: toAmount, signature });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Hook to save signature after signing
|
|
24
|
+
export async function onMessageSigned(walletAddress: string, message: string, signature: string) {
|
|
25
|
+
return saveSignature(walletAddress, message, signature);
|
|
26
|
+
}
|
|
27
|
+
<% } else if (backend === 'firebase') { %>
|
|
28
|
+
import { db, createOrGetUser, saveSwap, getSwapHistory, saveSignature } from "./firebase";
|
|
29
|
+
|
|
30
|
+
export { db, createOrGetUser, saveSwap, getSwapHistory, saveSignature };
|
|
31
|
+
|
|
32
|
+
// Hook to sync wallet on connect
|
|
33
|
+
export async function onWalletConnect(walletAddress: string) {
|
|
34
|
+
return createOrGetUser(walletAddress);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Hook to save swap after successful transaction
|
|
38
|
+
export async function onSwapComplete(
|
|
39
|
+
walletAddress: string,
|
|
40
|
+
fromToken: string,
|
|
41
|
+
toToken: string,
|
|
42
|
+
fromAmount: string,
|
|
43
|
+
toAmount: string,
|
|
44
|
+
signature: string
|
|
45
|
+
) {
|
|
46
|
+
return saveSwap({ walletAddress, fromToken, toToken, fromAmount, toAmount, signature });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Hook to save signature after signing
|
|
50
|
+
export async function onMessageSigned(walletAddress: string, message: string, signature: string) {
|
|
51
|
+
return saveSignature(walletAddress, message, signature);
|
|
52
|
+
}
|
|
53
|
+
<% } else { %>
|
|
54
|
+
// No backend configured - these are no-op stubs
|
|
55
|
+
export async function onWalletConnect(_walletAddress: string) {}
|
|
56
|
+
export async function onSwapComplete(..._args: any[]) {}
|
|
57
|
+
export async function onMessageSigned(..._args: any[]) {}
|
|
58
|
+
export async function getSwapHistory(_walletAddress: string) { return []; }
|
|
59
|
+
<% } %>
|
|
@@ -1,59 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import * as SplashScreen from 'expo-splash-screen';
|
|
6
|
-
import { useEffect } from 'react';
|
|
7
|
-
import 'react-native-reanimated';
|
|
8
|
-
|
|
9
|
-
import { useColorScheme } from '@/components/useColorScheme';
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
// Catch any errors thrown by the Layout component.
|
|
13
|
-
ErrorBoundary,
|
|
14
|
-
} from 'expo-router';
|
|
1
|
+
import 'react-native-get-random-values';
|
|
2
|
+
import 'react-native-url-polyfill/auto';
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
global.Buffer = global.Buffer || Buffer;
|
|
15
5
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
import { Stack } from 'expo-router';
|
|
7
|
+
import { LazorKitProvider } from '@lazorkit/wallet-mobile-adapter';
|
|
8
|
+
import { StatusBar } from 'expo-status-bar';
|
|
9
|
+
|
|
10
|
+
const CONFIG = {
|
|
11
|
+
rpcUrl: process.env.EXPO_PUBLIC_SOLANA_RPC || 'https://api.devnet.solana.com',
|
|
12
|
+
portalUrl: process.env.EXPO_PUBLIC_LAZORKIT_PORTAL_URL || 'https://portal.lazor.sh',
|
|
13
|
+
configPaymaster: {
|
|
14
|
+
paymasterUrl: process.env.EXPO_PUBLIC_LAZORKIT_PAYMASTER_URL || 'https://kora.devnet.lazorkit.com',
|
|
15
|
+
},
|
|
19
16
|
};
|
|
20
17
|
|
|
21
|
-
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
|
22
|
-
SplashScreen.preventAutoHideAsync();
|
|
23
|
-
|
|
24
18
|
export default function RootLayout() {
|
|
25
|
-
const [loaded, error] = useFonts({
|
|
26
|
-
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
|
27
|
-
...FontAwesome.font,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (error) throw error;
|
|
33
|
-
}, [error]);
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (loaded) {
|
|
37
|
-
SplashScreen.hideAsync();
|
|
38
|
-
}
|
|
39
|
-
}, [loaded]);
|
|
40
|
-
|
|
41
|
-
if (!loaded) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return <RootLayoutNav />;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function RootLayoutNav() {
|
|
49
|
-
const colorScheme = useColorScheme();
|
|
50
|
-
|
|
51
19
|
return (
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
20
|
+
<LazorKitProvider
|
|
21
|
+
rpcUrl={CONFIG.rpcUrl}
|
|
22
|
+
portalUrl={CONFIG.portalUrl}
|
|
23
|
+
configPaymaster={CONFIG.configPaymaster}
|
|
24
|
+
>
|
|
25
|
+
<StatusBar style="auto" />
|
|
26
|
+
<Stack screenOptions={{ headerShown: false }} />
|
|
27
|
+
</LazorKitProvider>
|
|
58
28
|
);
|
|
59
29
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity<% if (styling !== 'nativewind') { %>, StyleSheet<% } %> } from 'react-native';
|
|
3
|
+
import { useWallet } from '@lazorkit/wallet-mobile-adapter';
|
|
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
|
+
<% if (styling !== 'nativewind') { %>
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
nav: { flexDirection: 'row', justifyContent: 'center', gap: 16, paddingVertical: 16 },
|
|
11
|
+
navBtn: { fontSize: 12, color: '#737373' },
|
|
12
|
+
});
|
|
13
|
+
<% } %>
|
|
14
|
+
type Screen = 'swap' | 'recovery' | 'history';
|
|
15
|
+
|
|
16
|
+
export default function Index() {
|
|
17
|
+
const { isConnected } = useWallet();
|
|
18
|
+
const [screen, setScreen] = useState<Screen>('swap');
|
|
19
|
+
|
|
20
|
+
if (!isConnected) return <Onboarding />;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{screen === 'swap' && (
|
|
25
|
+
<>
|
|
26
|
+
<Swap />
|
|
27
|
+
<% if (styling === 'nativewind') { %>
|
|
28
|
+
<View className="flex-row justify-center gap-4 py-4 bg-white">
|
|
29
|
+
<TouchableOpacity onPress={() => setScreen('history')}><Text className="text-xs text-neutral-500">History</Text></TouchableOpacity>
|
|
30
|
+
<TouchableOpacity onPress={() => setScreen('recovery')}><Text className="text-xs text-neutral-500">Recovery</Text></TouchableOpacity>
|
|
31
|
+
</View>
|
|
32
|
+
<% } else { %>
|
|
33
|
+
<View style={styles.nav}>
|
|
34
|
+
<TouchableOpacity onPress={() => setScreen('history')}><Text style={styles.navBtn}>History</Text></TouchableOpacity>
|
|
35
|
+
<TouchableOpacity onPress={() => setScreen('recovery')}><Text style={styles.navBtn}>Recovery</Text></TouchableOpacity>
|
|
36
|
+
</View>
|
|
37
|
+
<% } %>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
{screen === 'recovery' && <Recovery onBack={() => setScreen('swap')} />}
|
|
41
|
+
{screen === 'history' && <History onBack={() => setScreen('swap')} />}
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, ActivityIndicator, ScrollView<% if (styling !== 'nativewind') { %>, StyleSheet<% } %> } from 'react-native';
|
|
3
|
+
import { useWallet } from '@lazorkit/wallet-mobile-adapter';
|
|
4
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
5
|
+
import * as Linking from 'expo-linking';
|
|
6
|
+
<% if (styling !== 'nativewind') { %>
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
container: { flex: 1, backgroundColor: '#fff', padding: 24, paddingTop: 60 },
|
|
9
|
+
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
|
|
10
|
+
title: { fontSize: 18, fontWeight: '600', color: '#0a0a0a' },
|
|
11
|
+
back: { fontSize: 13, color: '#737373' },
|
|
12
|
+
list: { gap: 8 },
|
|
13
|
+
item: { borderWidth: 1, borderColor: '#e5e5e5', borderRadius: 12, padding: 12 },
|
|
14
|
+
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
15
|
+
sig: { fontFamily: 'monospace', fontSize: 13, color: '#0a0a0a' },
|
|
16
|
+
time: { fontSize: 12, color: '#a3a3a3', marginTop: 4 },
|
|
17
|
+
status: { fontSize: 11, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, overflow: 'hidden' },
|
|
18
|
+
success: { backgroundColor: '#dcfce7', color: '#166534' },
|
|
19
|
+
failed: { backgroundColor: '#fee2e2', color: '#991b1b' },
|
|
20
|
+
empty: { textAlign: 'center', paddingVertical: 32, color: '#a3a3a3', fontSize: 14 },
|
|
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.EXPO_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 openTx = (sig: string) => {
|
|
55
|
+
const cluster = (process.env.EXPO_PUBLIC_SOLANA_RPC || '').includes('mainnet') ? '' : '?cluster=devnet';
|
|
56
|
+
Linking.openURL(`https://solscan.io/tx/${sig}${cluster}`);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<% if (styling === 'nativewind') { %>
|
|
61
|
+
<View className="flex-1 bg-white p-6 pt-16">
|
|
62
|
+
<View className="flex-row items-center justify-between mb-4">
|
|
63
|
+
<Text className="text-lg font-semibold text-black">History</Text>
|
|
64
|
+
<TouchableOpacity onPress={onBack}><Text className="text-sm text-neutral-500">← Back</Text></TouchableOpacity>
|
|
65
|
+
</View>
|
|
66
|
+
{loading ? (
|
|
67
|
+
<View className="py-8 items-center"><ActivityIndicator color="#a3a3a3" /></View>
|
|
68
|
+
) : txs.length === 0 ? (
|
|
69
|
+
<Text className="text-center py-8 text-neutral-400">No transactions yet</Text>
|
|
70
|
+
) : (
|
|
71
|
+
<ScrollView className="gap-2" showsVerticalScrollIndicator={false}>
|
|
72
|
+
{txs.map((tx) => (
|
|
73
|
+
<TouchableOpacity key={tx.signature} onPress={() => openTx(tx.signature)} className="border border-neutral-200 rounded-xl p-3 mb-2">
|
|
74
|
+
<View className="flex-row justify-between items-center">
|
|
75
|
+
<Text className="font-mono text-sm text-black">{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</Text>
|
|
76
|
+
<Text 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
|
+
</Text>
|
|
79
|
+
</View>
|
|
80
|
+
<Text className="text-xs text-neutral-400 mt-1">{formatTime(tx.blockTime)}</Text>
|
|
81
|
+
</TouchableOpacity>
|
|
82
|
+
))}
|
|
83
|
+
</ScrollView>
|
|
84
|
+
)}
|
|
85
|
+
</View>
|
|
86
|
+
<% } else { %>
|
|
87
|
+
<View style={styles.container}>
|
|
88
|
+
<View style={styles.header}>
|
|
89
|
+
<Text style={styles.title}>History</Text>
|
|
90
|
+
<TouchableOpacity onPress={onBack}><Text style={styles.back}>← Back</Text></TouchableOpacity>
|
|
91
|
+
</View>
|
|
92
|
+
{loading ? (
|
|
93
|
+
<View style={{ paddingVertical: 32, alignItems: 'center' }}><ActivityIndicator color="#a3a3a3" /></View>
|
|
94
|
+
) : txs.length === 0 ? (
|
|
95
|
+
<Text style={styles.empty}>No transactions yet</Text>
|
|
96
|
+
) : (
|
|
97
|
+
<ScrollView style={styles.list} showsVerticalScrollIndicator={false}>
|
|
98
|
+
{txs.map((tx) => (
|
|
99
|
+
<TouchableOpacity key={tx.signature} onPress={() => openTx(tx.signature)} style={[styles.item, { marginBottom: 8 }]}>
|
|
100
|
+
<View style={styles.row}>
|
|
101
|
+
<Text style={styles.sig}>{tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}</Text>
|
|
102
|
+
<Text style={[styles.status, tx.err ? styles.failed : styles.success]}>
|
|
103
|
+
{tx.err ? 'Failed' : 'Success'}
|
|
104
|
+
</Text>
|
|
105
|
+
</View>
|
|
106
|
+
<Text style={styles.time}>{formatTime(tx.blockTime)}</Text>
|
|
107
|
+
</TouchableOpacity>
|
|
108
|
+
))}
|
|
109
|
+
</ScrollView>
|
|
110
|
+
)}
|
|
111
|
+
</View>
|
|
112
|
+
<% } %>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, ActivityIndicator<% if (styling !== 'nativewind') { %>, StyleSheet<% } %> } from 'react-native';
|
|
3
|
+
import { useWallet } from '@lazorkit/wallet-mobile-adapter';
|
|
4
|
+
import * as Linking from 'expo-linking';
|
|
5
|
+
<% if (styling !== 'nativewind') { %>
|
|
6
|
+
const styles = StyleSheet.create({
|
|
7
|
+
container: { flex: 1, backgroundColor: '#fff', justifyContent: 'center', alignItems: 'center', padding: 24 },
|
|
8
|
+
content: { width: '100%', maxWidth: 320, alignItems: 'center' },
|
|
9
|
+
title: { fontSize: 24, fontWeight: '600', color: '#0a0a0a', letterSpacing: -0.5 },
|
|
10
|
+
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', textAlign: 'center', lineHeight: 20 },
|
|
11
|
+
button: { marginTop: 32, width: '100%', backgroundColor: '#0a0a0a', paddingVertical: 14, borderRadius: 10, alignItems: 'center' },
|
|
12
|
+
buttonDisabled: { opacity: 0.5 },
|
|
13
|
+
buttonText: { color: '#fff', fontSize: 14, fontWeight: '500' },
|
|
14
|
+
hint: { marginTop: 16, fontSize: 12, color: '#a3a3a3' },
|
|
15
|
+
error: { marginTop: 16, fontSize: 13, color: '#ef4444', textAlign: 'center' },
|
|
16
|
+
});
|
|
17
|
+
<% } %>
|
|
18
|
+
export function Onboarding() {
|
|
19
|
+
const { connect, isConnecting } = useWallet();
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const redirectUrl = Linking.createURL('/');
|
|
22
|
+
|
|
23
|
+
const handleStart = async () => {
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
await connect({ redirectUrl });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
setError(e instanceof Error ? e.message : 'Something went wrong');
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<% if (styling === 'nativewind') { %>
|
|
34
|
+
<View className="flex-1 bg-white justify-center items-center p-6">
|
|
35
|
+
<View className="w-full max-w-xs items-center">
|
|
36
|
+
<Text className="text-2xl font-semibold text-black tracking-tight">Welcome</Text>
|
|
37
|
+
<Text className="mt-2 text-sm text-neutral-500 text-center leading-5">
|
|
38
|
+
Create a wallet with your fingerprint or face. No seed phrases.
|
|
39
|
+
</Text>
|
|
40
|
+
<TouchableOpacity
|
|
41
|
+
className={`mt-8 w-full bg-black py-3.5 rounded-xl items-center ${isConnecting ? 'opacity-50' : ''}`}
|
|
42
|
+
onPress={handleStart}
|
|
43
|
+
disabled={isConnecting}
|
|
44
|
+
>
|
|
45
|
+
{isConnecting ? (
|
|
46
|
+
<ActivityIndicator color="#fff" size="small" />
|
|
47
|
+
) : (
|
|
48
|
+
<Text className="text-white text-sm font-medium">Create Wallet</Text>
|
|
49
|
+
)}
|
|
50
|
+
</TouchableOpacity>
|
|
51
|
+
<Text className="mt-4 text-xs text-neutral-400">Secured by passkeys</Text>
|
|
52
|
+
{error && <Text className="mt-4 text-sm text-red-500 text-center">{error}</Text>}
|
|
53
|
+
</View>
|
|
54
|
+
</View>
|
|
55
|
+
<% } else { %>
|
|
56
|
+
<View style={styles.container}>
|
|
57
|
+
<View style={styles.content}>
|
|
58
|
+
<Text style={styles.title}>Welcome</Text>
|
|
59
|
+
<Text style={styles.subtitle}>
|
|
60
|
+
Create a wallet with your fingerprint or face. No seed phrases.
|
|
61
|
+
</Text>
|
|
62
|
+
<TouchableOpacity
|
|
63
|
+
style={[styles.button, isConnecting && styles.buttonDisabled]}
|
|
64
|
+
onPress={handleStart}
|
|
65
|
+
disabled={isConnecting}
|
|
66
|
+
>
|
|
67
|
+
{isConnecting ? (
|
|
68
|
+
<ActivityIndicator color="#fff" size="small" />
|
|
69
|
+
) : (
|
|
70
|
+
<Text style={styles.buttonText}>Create Wallet</Text>
|
|
71
|
+
)}
|
|
72
|
+
</TouchableOpacity>
|
|
73
|
+
<Text style={styles.hint}>Secured by passkeys</Text>
|
|
74
|
+
{error && <Text style={styles.error}>{error}</Text>}
|
|
75
|
+
</View>
|
|
76
|
+
</View>
|
|
77
|
+
<% } %>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity<% if (styling !== 'nativewind') { %>, StyleSheet<% } %> } from 'react-native';
|
|
3
|
+
import { useWallet } from '@lazorkit/wallet-mobile-adapter';
|
|
4
|
+
import * as Linking from 'expo-linking';
|
|
5
|
+
import * as Clipboard from 'expo-clipboard';
|
|
6
|
+
<% if (styling !== 'nativewind') { %>
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
container: { flex: 1, backgroundColor: '#fff', padding: 24, paddingTop: 60 },
|
|
9
|
+
title: { fontSize: 18, fontWeight: '600', color: '#0a0a0a' },
|
|
10
|
+
subtitle: { marginTop: 8, fontSize: 14, color: '#737373', lineHeight: 20 },
|
|
11
|
+
card: { marginTop: 16, borderWidth: 1, borderColor: '#e5e5e5', borderRadius: 12, padding: 16 },
|
|
12
|
+
cardTitle: { fontSize: 14, fontWeight: '500', color: '#0a0a0a' },
|
|
13
|
+
cardDesc: { marginTop: 4, fontSize: 13, color: '#737373' },
|
|
14
|
+
button: { marginTop: 12, backgroundColor: '#0a0a0a', paddingVertical: 12, borderRadius: 10, alignItems: 'center' },
|
|
15
|
+
buttonText: { color: '#fff', fontSize: 14, fontWeight: '500' },
|
|
16
|
+
buttonSecondary: { marginTop: 12, backgroundColor: 'transparent', borderWidth: 1, borderColor: '#e5e5e5', paddingVertical: 12, borderRadius: 10, alignItems: 'center' },
|
|
17
|
+
buttonTextSecondary: { color: '#0a0a0a', fontSize: 14, fontWeight: '500' },
|
|
18
|
+
info: { marginTop: 16, padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8 },
|
|
19
|
+
infoText: { fontSize: 12, color: '#525252' },
|
|
20
|
+
back: { marginTop: 16 },
|
|
21
|
+
backText: { fontSize: 13, color: '#737373' },
|
|
22
|
+
copied: { marginLeft: 8, color: '#16a34a', fontSize: 12 },
|
|
23
|
+
});
|
|
24
|
+
<% } %>
|
|
25
|
+
interface Props {
|
|
26
|
+
onBack: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function Recovery({ onBack }: Props) {
|
|
30
|
+
const { wallet } = useWallet();
|
|
31
|
+
const [copied, setCopied] = useState(false);
|
|
32
|
+
const portalUrl = process.env.EXPO_PUBLIC_LAZORKIT_PORTAL_URL || 'https://portal.lazor.sh';
|
|
33
|
+
|
|
34
|
+
const handleAddDevice = () => {
|
|
35
|
+
Linking.openURL(`${portalUrl}/recovery/add-device`);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleManageDevices = () => {
|
|
39
|
+
Linking.openURL(`${portalUrl}/recovery/devices`);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const copyAddress = async () => {
|
|
43
|
+
if (wallet?.smartWallet) {
|
|
44
|
+
await Clipboard.setStringAsync(wallet.smartWallet);
|
|
45
|
+
setCopied(true);
|
|
46
|
+
setTimeout(() => setCopied(false), 2000);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<% if (styling === 'nativewind') { %>
|
|
52
|
+
<View className="flex-1 bg-white p-6 pt-16">
|
|
53
|
+
<Text className="text-lg font-semibold text-black">Recovery & Backup</Text>
|
|
54
|
+
<Text className="mt-2 text-sm text-neutral-500 leading-5">
|
|
55
|
+
Add backup passkeys from other devices to ensure you never lose access.
|
|
56
|
+
</Text>
|
|
57
|
+
<View className="mt-4 border border-neutral-200 rounded-xl p-4">
|
|
58
|
+
<Text className="text-sm font-medium text-black">Add Backup Device</Text>
|
|
59
|
+
<Text className="mt-1 text-xs text-neutral-500">Register a passkey from another phone, tablet, or computer.</Text>
|
|
60
|
+
<TouchableOpacity onPress={handleAddDevice} className="mt-3 bg-black py-3 rounded-lg items-center">
|
|
61
|
+
<Text className="text-white text-sm font-medium">Add Device</Text>
|
|
62
|
+
</TouchableOpacity>
|
|
63
|
+
</View>
|
|
64
|
+
<View className="mt-3 border border-neutral-200 rounded-xl p-4">
|
|
65
|
+
<Text className="text-sm font-medium text-black">Manage Devices</Text>
|
|
66
|
+
<Text className="mt-1 text-xs text-neutral-500">View and remove registered passkeys.</Text>
|
|
67
|
+
<TouchableOpacity onPress={handleManageDevices} className="mt-3 border border-neutral-200 py-3 rounded-lg items-center">
|
|
68
|
+
<Text className="text-black text-sm font-medium">View Devices</Text>
|
|
69
|
+
</TouchableOpacity>
|
|
70
|
+
</View>
|
|
71
|
+
<View className="mt-4 p-3 bg-neutral-100 rounded-lg">
|
|
72
|
+
<TouchableOpacity onPress={copyAddress} className="flex-row items-center">
|
|
73
|
+
<Text className="text-xs text-neutral-600">
|
|
74
|
+
<Text className="font-semibold">Wallet: </Text>
|
|
75
|
+
<Text className="font-mono">{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}</Text>
|
|
76
|
+
</Text>
|
|
77
|
+
{copied && <Text className="ml-2 text-xs text-green-600">Copied!</Text>}
|
|
78
|
+
</TouchableOpacity>
|
|
79
|
+
</View>
|
|
80
|
+
<TouchableOpacity onPress={onBack} className="mt-4">
|
|
81
|
+
<Text className="text-sm text-neutral-500">← Back</Text>
|
|
82
|
+
</TouchableOpacity>
|
|
83
|
+
</View>
|
|
84
|
+
<% } else { %>
|
|
85
|
+
<View style={styles.container}>
|
|
86
|
+
<Text style={styles.title}>Recovery & Backup</Text>
|
|
87
|
+
<Text style={styles.subtitle}>
|
|
88
|
+
Add backup passkeys from other devices to ensure you never lose access.
|
|
89
|
+
</Text>
|
|
90
|
+
<View style={styles.card}>
|
|
91
|
+
<Text style={styles.cardTitle}>Add Backup Device</Text>
|
|
92
|
+
<Text style={styles.cardDesc}>Register a passkey from another phone, tablet, or computer.</Text>
|
|
93
|
+
<TouchableOpacity onPress={handleAddDevice} style={styles.button}>
|
|
94
|
+
<Text style={styles.buttonText}>Add Device</Text>
|
|
95
|
+
</TouchableOpacity>
|
|
96
|
+
</View>
|
|
97
|
+
<View style={styles.card}>
|
|
98
|
+
<Text style={styles.cardTitle}>Manage Devices</Text>
|
|
99
|
+
<Text style={styles.cardDesc}>View and remove registered passkeys.</Text>
|
|
100
|
+
<TouchableOpacity onPress={handleManageDevices} style={styles.buttonSecondary}>
|
|
101
|
+
<Text style={styles.buttonTextSecondary}>View Devices</Text>
|
|
102
|
+
</TouchableOpacity>
|
|
103
|
+
</View>
|
|
104
|
+
<View style={styles.info}>
|
|
105
|
+
<TouchableOpacity onPress={copyAddress}>
|
|
106
|
+
<Text style={styles.infoText}>
|
|
107
|
+
<Text style={{ fontWeight: '600' }}>Wallet: </Text>
|
|
108
|
+
<Text style={{ fontFamily: 'monospace' }}>{wallet?.smartWallet?.slice(0, 8)}...{wallet?.smartWallet?.slice(-8)}</Text>
|
|
109
|
+
{copied && <Text style={styles.copied}> Copied!</Text>}
|
|
110
|
+
</Text>
|
|
111
|
+
</TouchableOpacity>
|
|
112
|
+
</View>
|
|
113
|
+
<TouchableOpacity onPress={onBack} style={styles.back}>
|
|
114
|
+
<Text style={styles.backText}>← Back</Text>
|
|
115
|
+
</TouchableOpacity>
|
|
116
|
+
</View>
|
|
117
|
+
<% } %>
|
|
118
|
+
);
|
|
119
|
+
}
|