@getpara/create-para-app 0.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +306 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -48
- package/dist/index.js.map +1 -0
- package/dist/package-builder.d.ts +13 -0
- package/dist/package-builder.d.ts.map +1 -0
- package/dist/package-builder.js +89 -0
- package/dist/package-builder.js.map +1 -0
- package/dist/post-scaffold.d.ts +3 -0
- package/dist/post-scaffold.d.ts.map +1 -0
- package/dist/post-scaffold.js +64 -0
- package/dist/post-scaffold.js.map +1 -0
- package/dist/prompt-orchestrator.d.ts +3 -0
- package/dist/prompt-orchestrator.d.ts.map +1 -0
- package/dist/prompt-orchestrator.js +198 -0
- package/dist/prompt-orchestrator.js.map +1 -0
- package/dist/scaffolder.d.ts +3 -0
- package/dist/scaffolder.d.ts.map +1 -0
- package/dist/scaffolder.js +82 -0
- package/dist/scaffolder.js.map +1 -0
- package/dist/template-registry.d.ts +12 -0
- package/dist/template-registry.d.ts.map +1 -0
- package/dist/template-registry.js +29 -0
- package/dist/template-registry.js.map +1 -0
- package/dist/template-renderer.d.ts +11 -0
- package/dist/template-renderer.d.ts.map +1 -0
- package/dist/template-renderer.js +133 -0
- package/dist/template-renderer.js.map +1 -0
- package/dist/template-strategies/expo-template.d.ts +18 -0
- package/dist/template-strategies/expo-template.d.ts.map +1 -0
- package/dist/template-strategies/expo-template.js +173 -0
- package/dist/template-strategies/expo-template.js.map +1 -0
- package/dist/template-strategies/index.d.ts +4 -0
- package/dist/template-strategies/index.d.ts.map +1 -0
- package/dist/template-strategies/index.js +3 -0
- package/dist/template-strategies/index.js.map +1 -0
- package/dist/template-strategies/nextjs-template.d.ts +12 -0
- package/dist/template-strategies/nextjs-template.d.ts.map +1 -0
- package/dist/template-strategies/nextjs-template.js +123 -0
- package/dist/template-strategies/nextjs-template.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/fs.d.ts +11 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +38 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +25 -19
- package/dist/utils/logger.js.map +1 -0
- package/package.json +35 -30
- package/templates/expo/_env.example +3 -0
- package/templates/expo/_gitignore +48 -0
- package/templates/expo/_yarnrc.yml +1 -0
- package/templates/expo/app/(auth)/_layout.tsx +12 -0
- package/templates/expo/app/(auth)/index.tsx.template +86 -0
- package/templates/expo/app/(tabs)/_layout.tsx +16 -0
- package/templates/expo/app/(tabs)/index.tsx +112 -0
- package/templates/expo/app/(tabs)/send.tsx +111 -0
- package/templates/expo/app/_layout.tsx +17 -0
- package/templates/expo/app/index.tsx +22 -0
- package/templates/expo/app.json.template +32 -0
- package/templates/expo/assets/adaptive-icon.png +0 -0
- package/templates/expo/assets/favicon.png +0 -0
- package/templates/expo/assets/icon.png +0 -0
- package/templates/expo/assets/splash.png +0 -0
- package/templates/expo/babel.config.cjs +12 -0
- package/templates/expo/components/features/AuthForm.tsx.template +138 -0
- package/templates/expo/components/features/OAuthButtons.tsx.template +27 -0
- package/templates/expo/components/features/index.ts.template +4 -0
- package/templates/expo/components/ui/Button.tsx +58 -0
- package/templates/expo/components/ui/Card.tsx +11 -0
- package/templates/expo/components/ui/Divider.tsx +19 -0
- package/templates/expo/components/ui/Input.tsx +23 -0
- package/templates/expo/components/ui/WalletCard.tsx +44 -0
- package/templates/expo/components/ui/index.ts +5 -0
- package/templates/expo/eslint.config.cjs +15 -0
- package/templates/expo/global.css +3 -0
- package/templates/expo/hooks/useOneClickLogin.ts.template +161 -0
- package/templates/expo/hooks/useViemClient.ts +118 -0
- package/templates/expo/hooks/useWallets.ts +52 -0
- package/templates/expo/index.js +2 -0
- package/templates/expo/lib/auth.ts +54 -0
- package/templates/expo/lib/constants.ts.template +2 -0
- package/templates/expo/lib/para.ts +13 -0
- package/templates/expo/metro.config.cjs +14 -0
- package/templates/expo/nativewind-env.d.ts +2 -0
- package/templates/expo/prettier.config.cjs +10 -0
- package/templates/expo/providers/ParaProvider.tsx +140 -0
- package/templates/expo/tailwind.config.cjs +23 -0
- package/templates/expo/tsconfig.json +11 -0
- package/templates/expo/types/index.ts +28 -0
- package/templates/nextjs/README.md +69 -0
- package/templates/nextjs/_env.example +8 -0
- package/templates/nextjs/_gitignore +36 -0
- package/templates/nextjs/_yarnrc.yml +1 -0
- package/templates/nextjs/next.config.ts +5 -0
- package/templates/nextjs/postcss.config.mjs +7 -0
- package/templates/nextjs/public/para.svg +3 -0
- package/templates/nextjs/src/app/layout.tsx +30 -0
- package/templates/nextjs/src/app/page.tsx +40 -0
- package/templates/nextjs/src/components/ParaProvider.tsx +116 -0
- package/templates/nextjs/src/components/layout/Header.tsx +44 -0
- package/templates/nextjs/src/components/ui/ConnectCard.tsx +24 -0
- package/templates/nextjs/src/components/ui/SignMessage.tsx +53 -0
- package/templates/nextjs/src/components/ui/WalletInfo.tsx +22 -0
- package/templates/nextjs/src/hooks/useSignHelloWorld.ts +23 -0
- package/templates/nextjs/src/styles/globals.css +1 -0
- package/templates/nextjs/tsconfig.json +27 -0
- package/dist/bin/index.js +0 -2
- package/dist/commands/scaffold.js +0 -137
- package/dist/integrations/codeGenerators.js +0 -637
- package/dist/integrations/packageJsonHelpers.js +0 -77
- package/dist/integrations/sdkSetup.js +0 -24
- package/dist/integrations/sdkSetupNextjs.js +0 -111
- package/dist/integrations/sdkSetupVite.js +0 -82
- package/dist/package.json +0 -44
- package/dist/prompts/interactive.js +0 -98
- package/dist/src/commands/scaffold.js +0 -157
- package/dist/src/index.js +0 -48
- package/dist/src/integrations/codeGenerators.js +0 -637
- package/dist/src/integrations/packageJsonHelpers.js +0 -77
- package/dist/src/integrations/sdkSetup.js +0 -24
- package/dist/src/integrations/sdkSetupNextjs.js +0 -111
- package/dist/src/integrations/sdkSetupVite.js +0 -82
- package/dist/src/prompts/interactive.js +0 -98
- package/dist/src/templates/nextjs.js +0 -68
- package/dist/src/templates/vite-react.js +0 -52
- package/dist/src/utils/exec.js +0 -16
- package/dist/src/utils/formatting.js +0 -31
- package/dist/src/utils/logger.js +0 -19
- package/dist/templates/nextjs.js +0 -61
- package/dist/templates/vite-react.js +0 -43
- package/dist/utils/exec.js +0 -16
- package/dist/utils/formatting.js +0 -31
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View, Text, Alert } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Button, Input } from '@/components/ui';
|
|
5
|
+
// @if:hasEmail
|
|
6
|
+
import { isValidEmail } from '@/lib/auth';
|
|
7
|
+
// @endif
|
|
8
|
+
// @if:hasPhone
|
|
9
|
+
import { isValidPhone } from '@/lib/auth';
|
|
10
|
+
// @endif
|
|
11
|
+
|
|
12
|
+
// @if:hasEmail && hasPhone
|
|
13
|
+
type AuthMethod = 'email' | 'phone';
|
|
14
|
+
// @endif
|
|
15
|
+
|
|
16
|
+
interface AuthFormProps {
|
|
17
|
+
onSubmit: (value: string, method: 'email' | 'phone') => Promise<void>;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
error?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AuthForm({ onSubmit, loading = false, error }: AuthFormProps) {
|
|
23
|
+
// @if:hasEmail && hasPhone
|
|
24
|
+
const [method, setMethod] = useState<AuthMethod>('email');
|
|
25
|
+
// @endif
|
|
26
|
+
// @if:hasEmail
|
|
27
|
+
const [email, setEmail] = useState('');
|
|
28
|
+
// @endif
|
|
29
|
+
// @if:hasPhone
|
|
30
|
+
const [phone, setPhone] = useState('');
|
|
31
|
+
// @endif
|
|
32
|
+
|
|
33
|
+
const handleSubmit = async () => {
|
|
34
|
+
// @if:hasEmail && hasPhone
|
|
35
|
+
if (method === 'email') {
|
|
36
|
+
if (!isValidEmail(email)) {
|
|
37
|
+
Alert.alert('Invalid Email', 'Please enter a valid email address');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await onSubmit(email, 'email');
|
|
41
|
+
} else {
|
|
42
|
+
if (!isValidPhone(phone)) {
|
|
43
|
+
Alert.alert('Invalid Phone', 'Please enter a valid phone number');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await onSubmit(phone, 'phone');
|
|
47
|
+
}
|
|
48
|
+
// @endif
|
|
49
|
+
// @if:hasEmail && !hasPhone
|
|
50
|
+
if (!isValidEmail(email)) {
|
|
51
|
+
Alert.alert('Invalid Email', 'Please enter a valid email address');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await onSubmit(email, 'email');
|
|
55
|
+
// @endif
|
|
56
|
+
// @if:hasPhone && !hasEmail
|
|
57
|
+
if (!isValidPhone(phone)) {
|
|
58
|
+
Alert.alert('Invalid Phone', 'Please enter a valid phone number');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await onSubmit(phone, 'phone');
|
|
62
|
+
// @endif
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View>
|
|
67
|
+
// @if:hasEmail && hasPhone
|
|
68
|
+
<View className="mb-4 flex-row rounded-xl bg-gray-100 p-1">
|
|
69
|
+
<Button
|
|
70
|
+
title="Email"
|
|
71
|
+
variant={method === 'email' ? 'primary' : 'secondary'}
|
|
72
|
+
onPress={() => setMethod('email')}
|
|
73
|
+
fullWidth={false}
|
|
74
|
+
/>
|
|
75
|
+
<Button
|
|
76
|
+
title="Phone"
|
|
77
|
+
variant={method === 'phone' ? 'primary' : 'secondary'}
|
|
78
|
+
onPress={() => setMethod('phone')}
|
|
79
|
+
fullWidth={false}
|
|
80
|
+
/>
|
|
81
|
+
</View>
|
|
82
|
+
// @endif
|
|
83
|
+
|
|
84
|
+
// @if:hasEmail && hasPhone
|
|
85
|
+
{method === 'email' ? (
|
|
86
|
+
<Input
|
|
87
|
+
label="Email Address"
|
|
88
|
+
placeholder="you@example.com"
|
|
89
|
+
value={email}
|
|
90
|
+
onChangeText={setEmail}
|
|
91
|
+
keyboardType="email-address"
|
|
92
|
+
autoCapitalize="none"
|
|
93
|
+
autoCorrect={false}
|
|
94
|
+
/>
|
|
95
|
+
) : (
|
|
96
|
+
<Input
|
|
97
|
+
label="Phone Number"
|
|
98
|
+
placeholder="+1 (555) 000-0000"
|
|
99
|
+
value={phone}
|
|
100
|
+
onChangeText={setPhone}
|
|
101
|
+
keyboardType="phone-pad"
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
// @endif
|
|
105
|
+
// @if:hasEmail && !hasPhone
|
|
106
|
+
<Input
|
|
107
|
+
label="Email Address"
|
|
108
|
+
placeholder="you@example.com"
|
|
109
|
+
value={email}
|
|
110
|
+
onChangeText={setEmail}
|
|
111
|
+
keyboardType="email-address"
|
|
112
|
+
autoCapitalize="none"
|
|
113
|
+
autoCorrect={false}
|
|
114
|
+
/>
|
|
115
|
+
// @endif
|
|
116
|
+
// @if:hasPhone && !hasEmail
|
|
117
|
+
<Input
|
|
118
|
+
label="Phone Number"
|
|
119
|
+
placeholder="+1 (555) 000-0000"
|
|
120
|
+
value={phone}
|
|
121
|
+
onChangeText={setPhone}
|
|
122
|
+
keyboardType="phone-pad"
|
|
123
|
+
/>
|
|
124
|
+
// @endif
|
|
125
|
+
|
|
126
|
+
{error && (
|
|
127
|
+
<Text className="mb-4 text-center text-sm text-red-500">{error}</Text>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
<Button
|
|
131
|
+
title={loading ? 'Signing in...' : 'Continue'}
|
|
132
|
+
onPress={handleSubmit}
|
|
133
|
+
disabled={loading}
|
|
134
|
+
loading={loading}
|
|
135
|
+
/>
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @if:hasOAuth
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui';
|
|
6
|
+
|
|
7
|
+
interface OAuthButtonsProps {
|
|
8
|
+
onGooglePress?: () => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function OAuthButtons({ onGooglePress, disabled = false }: OAuthButtonsProps) {
|
|
13
|
+
return (
|
|
14
|
+
<View className="gap-3">
|
|
15
|
+
{onGooglePress && (
|
|
16
|
+
<Button
|
|
17
|
+
title="Continue with Google"
|
|
18
|
+
variant="outline"
|
|
19
|
+
onPress={onGooglePress}
|
|
20
|
+
disabled={disabled}
|
|
21
|
+
icon={<Ionicons name="logo-google" size={20} color="#111" />}
|
|
22
|
+
/>
|
|
23
|
+
)}
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
// @endif
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TouchableOpacity, Text, View, ActivityIndicator } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ButtonProps {
|
|
5
|
+
title: string;
|
|
6
|
+
onPress: () => void;
|
|
7
|
+
variant?: 'primary' | 'secondary' | 'danger' | 'outline';
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
fullWidth?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Button({
|
|
15
|
+
title,
|
|
16
|
+
onPress,
|
|
17
|
+
variant = 'primary',
|
|
18
|
+
disabled = false,
|
|
19
|
+
loading = false,
|
|
20
|
+
icon,
|
|
21
|
+
fullWidth = true,
|
|
22
|
+
}: ButtonProps) {
|
|
23
|
+
const baseClasses = 'flex-row items-center justify-center rounded-xl py-4 px-6';
|
|
24
|
+
const widthClass = fullWidth ? 'w-full' : '';
|
|
25
|
+
|
|
26
|
+
const variantClasses = {
|
|
27
|
+
primary: 'bg-gray-900',
|
|
28
|
+
secondary: 'bg-gray-100',
|
|
29
|
+
danger: 'bg-red-500',
|
|
30
|
+
outline: 'bg-transparent border border-gray-300',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const textClasses = {
|
|
34
|
+
primary: 'text-white',
|
|
35
|
+
secondary: 'text-gray-900',
|
|
36
|
+
danger: 'text-white',
|
|
37
|
+
outline: 'text-gray-900',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const disabledClasses = disabled || loading ? 'opacity-50' : '';
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<TouchableOpacity
|
|
44
|
+
onPress={onPress}
|
|
45
|
+
disabled={disabled || loading}
|
|
46
|
+
className={`${baseClasses} ${widthClass} ${variantClasses[variant]} ${disabledClasses}`}
|
|
47
|
+
activeOpacity={0.8}>
|
|
48
|
+
{loading ? (
|
|
49
|
+
<ActivityIndicator color={variant === 'outline' || variant === 'secondary' ? '#111' : '#fff'} />
|
|
50
|
+
) : (
|
|
51
|
+
<View className="flex-row items-center gap-2">
|
|
52
|
+
{icon}
|
|
53
|
+
<Text className={`text-base font-semibold ${textClasses[variant]}`}>{title}</Text>
|
|
54
|
+
</View>
|
|
55
|
+
)}
|
|
56
|
+
</TouchableOpacity>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface CardProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Card({ children, className = '' }: CardProps) {
|
|
10
|
+
return <View className={`rounded-2xl bg-white p-6 shadow-sm ${className}`}>{children}</View>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { View, Text } from 'react-native';
|
|
2
|
+
|
|
3
|
+
interface DividerProps {
|
|
4
|
+
text?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Divider({ text }: DividerProps) {
|
|
8
|
+
if (!text) {
|
|
9
|
+
return <View className="my-4 h-px bg-gray-200" />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<View className="my-6 flex-row items-center">
|
|
14
|
+
<View className="h-px flex-1 bg-gray-200" />
|
|
15
|
+
<Text className="mx-4 text-sm text-gray-400">{text}</Text>
|
|
16
|
+
<View className="h-px flex-1 bg-gray-200" />
|
|
17
|
+
</View>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TextInput, View, Text } from 'react-native';
|
|
2
|
+
import type { TextInputProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface InputProps extends Omit<TextInputProps, 'className'> {
|
|
5
|
+
label?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Input({ label, error, ...props }: InputProps) {
|
|
10
|
+
return (
|
|
11
|
+
<View className="mb-4">
|
|
12
|
+
{label && <Text className="mb-2 text-sm font-medium text-gray-700">{label}</Text>}
|
|
13
|
+
<TextInput
|
|
14
|
+
className={`rounded-xl border bg-white p-4 text-base ${
|
|
15
|
+
error ? 'border-red-500' : 'border-gray-200'
|
|
16
|
+
}`}
|
|
17
|
+
placeholderTextColor="#9CA3AF"
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
{error && <Text className="mt-1 text-sm text-red-500">{error}</Text>}
|
|
21
|
+
</View>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
2
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
3
|
+
import * as Clipboard from 'expo-clipboard';
|
|
4
|
+
|
|
5
|
+
interface WalletCardProps {
|
|
6
|
+
address: string;
|
|
7
|
+
balance: string;
|
|
8
|
+
network: string;
|
|
9
|
+
onSend?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function WalletCard({ address, balance, network, onSend }: WalletCardProps) {
|
|
13
|
+
const handleCopyAddress = async () => {
|
|
14
|
+
await Clipboard.setStringAsync(address);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const shortAddress = `${address.slice(0, 10)}...${address.slice(-8)}`;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View className="rounded-2xl border border-gray-200 bg-white p-6">
|
|
21
|
+
<View className="mb-4 flex-row items-center justify-between">
|
|
22
|
+
<View className="rounded-full bg-gray-100 px-3 py-1">
|
|
23
|
+
<Text className="text-xs font-medium text-gray-600">{network}</Text>
|
|
24
|
+
</View>
|
|
25
|
+
<TouchableOpacity onPress={handleCopyAddress} className="flex-row items-center gap-1">
|
|
26
|
+
<Text className="font-mono text-xs text-gray-500">{shortAddress}</Text>
|
|
27
|
+
<Ionicons name="copy-outline" size={14} color="#6b7280" />
|
|
28
|
+
</TouchableOpacity>
|
|
29
|
+
</View>
|
|
30
|
+
|
|
31
|
+
<Text className="mb-1 text-sm text-gray-500">Balance</Text>
|
|
32
|
+
<Text className="mb-6 text-3xl font-bold text-gray-900">{balance}</Text>
|
|
33
|
+
|
|
34
|
+
{onSend && (
|
|
35
|
+
<TouchableOpacity
|
|
36
|
+
onPress={onSend}
|
|
37
|
+
className="flex-row items-center justify-center gap-2 rounded-xl bg-gray-900 py-3">
|
|
38
|
+
<Ionicons name="send-outline" size={18} color="#fff" />
|
|
39
|
+
<Text className="font-semibold text-white">Send</Text>
|
|
40
|
+
</TouchableOpacity>
|
|
41
|
+
)}
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
const { defineConfig } = require('eslint/config');
|
|
3
|
+
const expoConfig = require('eslint-config-expo/flat');
|
|
4
|
+
|
|
5
|
+
module.exports = defineConfig([
|
|
6
|
+
expoConfig,
|
|
7
|
+
{
|
|
8
|
+
ignores: ['dist/*'],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
rules: {
|
|
12
|
+
'react/display-name': 'off',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { para } from '@/lib/para';
|
|
3
|
+
import { openAuthUrl } from '@/lib/auth';
|
|
4
|
+
import type { AuthStatus } from '@/types';
|
|
5
|
+
|
|
6
|
+
interface UseOneClickLoginResult {
|
|
7
|
+
status: AuthStatus;
|
|
8
|
+
error: string | null;
|
|
9
|
+
// @if:hasEmail
|
|
10
|
+
loginWithEmail: (email: string) => Promise<boolean>;
|
|
11
|
+
// @endif
|
|
12
|
+
// @if:hasPhone
|
|
13
|
+
loginWithPhone: (phone: string) => Promise<boolean>;
|
|
14
|
+
// @endif
|
|
15
|
+
// @if:hasOAuth
|
|
16
|
+
loginWithGoogle: () => Promise<boolean>;
|
|
17
|
+
// @endif
|
|
18
|
+
reset: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useOneClickLogin(onSuccess: () => void): UseOneClickLoginResult {
|
|
22
|
+
const [status, setStatus] = useState<AuthStatus>('idle');
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
const reset = useCallback(() => {
|
|
26
|
+
setStatus('idle');
|
|
27
|
+
setError(null);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
// @if:hasEmail
|
|
31
|
+
const loginWithEmail = useCallback(
|
|
32
|
+
async (email: string): Promise<boolean> => {
|
|
33
|
+
try {
|
|
34
|
+
reset();
|
|
35
|
+
setStatus('loading');
|
|
36
|
+
|
|
37
|
+
const authState = await para.signUpOrLogIn({ auth: { email } });
|
|
38
|
+
|
|
39
|
+
if (authState?.stage === 'verify' && 'loginUrl' in authState && authState.loginUrl) {
|
|
40
|
+
const result = await openAuthUrl(authState.loginUrl);
|
|
41
|
+
|
|
42
|
+
if (!result.success) {
|
|
43
|
+
throw new Error('Authentication was cancelled');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (authState.nextStage === 'login') {
|
|
47
|
+
await para.waitForLogin({});
|
|
48
|
+
} else {
|
|
49
|
+
await para.waitForWalletCreation({});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setStatus('success');
|
|
53
|
+
onSuccess();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error('One-click login not available for this account');
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const message = err instanceof Error ? err.message : 'Login failed';
|
|
60
|
+
setError(message);
|
|
61
|
+
setStatus('error');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
[reset, onSuccess]
|
|
66
|
+
);
|
|
67
|
+
// @endif
|
|
68
|
+
|
|
69
|
+
// @if:hasPhone
|
|
70
|
+
const loginWithPhone = useCallback(
|
|
71
|
+
async (phone: string): Promise<boolean> => {
|
|
72
|
+
try {
|
|
73
|
+
reset();
|
|
74
|
+
setStatus('loading');
|
|
75
|
+
|
|
76
|
+
const authState = await para.signUpOrLogIn({
|
|
77
|
+
auth: { phone: phone as `+${number}` },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (authState?.stage === 'verify' && 'loginUrl' in authState && authState.loginUrl) {
|
|
81
|
+
const result = await openAuthUrl(authState.loginUrl);
|
|
82
|
+
|
|
83
|
+
if (!result.success) {
|
|
84
|
+
throw new Error('Authentication was cancelled');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (authState.nextStage === 'login') {
|
|
88
|
+
await para.waitForLogin({});
|
|
89
|
+
} else {
|
|
90
|
+
await para.waitForWalletCreation({});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setStatus('success');
|
|
94
|
+
onSuccess();
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new Error('One-click login not available for this account');
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = err instanceof Error ? err.message : 'Login failed';
|
|
101
|
+
setError(message);
|
|
102
|
+
setStatus('error');
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[reset, onSuccess]
|
|
107
|
+
);
|
|
108
|
+
// @endif
|
|
109
|
+
|
|
110
|
+
// @if:hasOAuth
|
|
111
|
+
const loginWithGoogle = useCallback(async (): Promise<boolean> => {
|
|
112
|
+
try {
|
|
113
|
+
reset();
|
|
114
|
+
setStatus('loading');
|
|
115
|
+
|
|
116
|
+
const oauthUrl = await para.getOAuthUrl({ method: 'GOOGLE' });
|
|
117
|
+
const result = await openAuthUrl(oauthUrl);
|
|
118
|
+
|
|
119
|
+
if (!result.success) {
|
|
120
|
+
throw new Error('Authentication was cancelled');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const authState = await para.verifyOAuth({ method: 'GOOGLE' });
|
|
124
|
+
|
|
125
|
+
if (authState.stage === 'done') {
|
|
126
|
+
if (authState.isNewUser) {
|
|
127
|
+
await para.waitForWalletCreation({});
|
|
128
|
+
} else {
|
|
129
|
+
await para.waitForLogin({});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setStatus('success');
|
|
133
|
+
onSuccess();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error('Unexpected OAuth state');
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const message = err instanceof Error ? err.message : 'Google login failed';
|
|
140
|
+
setError(message);
|
|
141
|
+
setStatus('error');
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}, [reset, onSuccess]);
|
|
145
|
+
// @endif
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
status,
|
|
149
|
+
error,
|
|
150
|
+
// @if:hasEmail
|
|
151
|
+
loginWithEmail,
|
|
152
|
+
// @endif
|
|
153
|
+
// @if:hasPhone
|
|
154
|
+
loginWithPhone,
|
|
155
|
+
// @endif
|
|
156
|
+
// @if:hasOAuth
|
|
157
|
+
loginWithGoogle,
|
|
158
|
+
// @endif
|
|
159
|
+
reset,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useMemo, useCallback, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
createWalletClient,
|
|
4
|
+
createPublicClient,
|
|
5
|
+
http,
|
|
6
|
+
formatEther,
|
|
7
|
+
parseEther,
|
|
8
|
+
type WalletClient,
|
|
9
|
+
type PublicClient,
|
|
10
|
+
type LocalAccount,
|
|
11
|
+
type Hex,
|
|
12
|
+
} from 'viem';
|
|
13
|
+
import { sepolia } from 'viem/chains';
|
|
14
|
+
import { createParaAccount } from '@getpara/viem-v2-integration';
|
|
15
|
+
|
|
16
|
+
import { para } from '@/lib/para';
|
|
17
|
+
import { usePara } from '@/providers/ParaProvider';
|
|
18
|
+
|
|
19
|
+
interface UseViemClientResult {
|
|
20
|
+
account: LocalAccount | null;
|
|
21
|
+
walletClient: WalletClient | null;
|
|
22
|
+
publicClient: PublicClient | null;
|
|
23
|
+
isReady: boolean;
|
|
24
|
+
getBalance: () => Promise<string | null>;
|
|
25
|
+
sendTransaction: (to: Hex, amount: string) => Promise<Hex | null>;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
error: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useViemClient(): UseViemClientResult {
|
|
31
|
+
const { isAuthenticated, wallets } = usePara();
|
|
32
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
33
|
+
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
|
|
35
|
+
const { account, walletClient, publicClient } = useMemo(() => {
|
|
36
|
+
if (!isAuthenticated || wallets.length === 0) {
|
|
37
|
+
return { account: null, walletClient: null, publicClient: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const walletAddress = wallets[0].address as Hex;
|
|
41
|
+
const paraAccount = createParaAccount(para, walletAddress);
|
|
42
|
+
|
|
43
|
+
const wallet = createWalletClient({
|
|
44
|
+
account: paraAccount,
|
|
45
|
+
chain: sepolia,
|
|
46
|
+
transport: http(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const public_ = createPublicClient({
|
|
50
|
+
chain: sepolia,
|
|
51
|
+
transport: http(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return { account: paraAccount, walletClient: wallet, publicClient: public_ };
|
|
55
|
+
}, [isAuthenticated, wallets]);
|
|
56
|
+
|
|
57
|
+
const getBalance = useCallback(async (): Promise<string | null> => {
|
|
58
|
+
if (!publicClient || !account) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
setError(null);
|
|
65
|
+
|
|
66
|
+
const balance = await publicClient.getBalance({ address: account.address });
|
|
67
|
+
const formatted = formatEther(balance);
|
|
68
|
+
|
|
69
|
+
return formatted;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : 'Failed to get balance';
|
|
72
|
+
setError(message);
|
|
73
|
+
return null;
|
|
74
|
+
} finally {
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}, [publicClient, account]);
|
|
78
|
+
|
|
79
|
+
const sendTransaction = useCallback(
|
|
80
|
+
async (to: Hex, amount: string): Promise<Hex | null> => {
|
|
81
|
+
if (!walletClient || !account) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
setIsLoading(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
const value = parseEther(amount);
|
|
89
|
+
|
|
90
|
+
const hash = await walletClient.sendTransaction({
|
|
91
|
+
to,
|
|
92
|
+
value,
|
|
93
|
+
chain: sepolia,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return hash;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : 'Failed to send transaction';
|
|
99
|
+
setError(message);
|
|
100
|
+
return null;
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[walletClient, account]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
account,
|
|
110
|
+
walletClient,
|
|
111
|
+
publicClient,
|
|
112
|
+
isReady: account !== null && walletClient !== null && publicClient !== null,
|
|
113
|
+
getBalance,
|
|
114
|
+
sendTransaction,
|
|
115
|
+
isLoading,
|
|
116
|
+
error,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { para } from '@/lib/para';
|
|
3
|
+
import type { Wallet } from '@/types';
|
|
4
|
+
|
|
5
|
+
interface UseWalletsResult {
|
|
6
|
+
wallets: Wallet[];
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: string | null;
|
|
9
|
+
loadWallets: () => Promise<void>;
|
|
10
|
+
clearWallets: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useWallets(): UseWalletsResult {
|
|
14
|
+
const [wallets, setWallets] = useState<Wallet[]>([]);
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
const loadWallets = useCallback(async () => {
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const evmWallets = await para.getWalletsByType('EVM');
|
|
24
|
+
|
|
25
|
+
const walletList: Wallet[] = Object.values(evmWallets || {}).map((w) => ({
|
|
26
|
+
id: w.id,
|
|
27
|
+
address: w.address || '',
|
|
28
|
+
type: 'EVM',
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
setWallets(walletList);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const message = err instanceof Error ? err.message : 'Failed to load wallets';
|
|
34
|
+
setError(message);
|
|
35
|
+
} finally {
|
|
36
|
+
setIsLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const clearWallets = useCallback(() => {
|
|
41
|
+
setWallets([]);
|
|
42
|
+
setError(null);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
wallets,
|
|
47
|
+
isLoading,
|
|
48
|
+
error,
|
|
49
|
+
loadWallets,
|
|
50
|
+
clearWallets,
|
|
51
|
+
};
|
|
52
|
+
}
|