@highbeek/create-rnstarterkit 1.0.2-beta.5 → 1.1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/bin/create-rnstarterkit.js +205 -7
- package/dist/src/generators/appGenerator.js +2474 -58
- package/dist/src/generators/codeGenerator.js +289 -0
- package/dist/templates/cli-base/App.tsx +199 -21
- package/dist/templates/cli-base/assets/images/icon.png +0 -0
- package/dist/templates/cli-base/assets/images/partial-react-logo.png +0 -0
- package/dist/templates/cli-base/assets/images/react-logo.png +0 -0
- package/dist/templates/cli-base/babel.config.js +1 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +6 -0
- package/dist/templates/cli-base/ios/Podfile +5 -0
- package/dist/templates/cli-base/jest.config.js +4 -0
- package/dist/templates/cli-base/package.json +7 -4
- package/dist/templates/cli-base/tsconfig.json +1 -0
- package/dist/templates/expo-base/app/_layout.tsx +7 -18
- package/dist/templates/expo-base/app/home.tsx +37 -0
- package/dist/templates/expo-base/app/index.tsx +170 -0
- package/dist/templates/expo-base/app.json +2 -1
- package/dist/templates/expo-base/package.json +7 -3
- package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +33 -5
- package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +42 -7
- package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +42 -7
- package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +39 -2
- package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +43 -8
- package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +7 -10
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +61 -11
- package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +6 -9
- package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +34 -6
- package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +42 -7
- package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +42 -7
- package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/ci/.github/workflows/ci.yml +32 -0
- package/dist/templates/optional/error-boundary/components/ErrorBoundary.tsx +83 -0
- package/dist/templates/optional/formik/components/formik/FormikInput.tsx +45 -0
- package/dist/templates/optional/formik/components/formik/LoginForm.tsx +60 -0
- package/dist/templates/optional/formik/schemas/auth.schema.ts +17 -0
- package/dist/templates/optional/i18n/src/i18n/hooks/useAppTranslation.ts +28 -0
- package/dist/templates/optional/i18n/src/i18n/i18n.ts +30 -0
- package/dist/templates/optional/i18n/src/i18n/locales/en.json +32 -0
- package/dist/templates/optional/i18n/src/i18n/locales/es.json +32 -0
- package/dist/templates/optional/maestro/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/02_login.yaml +13 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/03_logout.yaml +16 -0
- package/dist/templates/optional/mmkv/utils/storage.ts +17 -0
- package/dist/templates/optional/react-hook-form/components/rhf/LoginForm.tsx +63 -0
- package/dist/templates/optional/react-hook-form/components/rhf/RHFInput.tsx +50 -0
- package/dist/templates/optional/react-hook-form/schemas/auth.schema.ts +29 -0
- package/dist/templates/optional/react-query/hooks/useAppMutation.ts +16 -0
- package/dist/templates/optional/react-query/hooks/useAppQuery.ts +12 -0
- package/dist/templates/optional/react-query/services/queryClient.ts +14 -0
- package/dist/templates/optional/redux/store/hooks.ts +6 -0
- package/dist/templates/optional/redux/store/store.ts +11 -0
- package/dist/templates/optional/sentry/src/utils/sentry.ts +24 -0
- package/dist/templates/optional/swr/hooks/useSWRFetch.ts +14 -0
- package/dist/templates/optional/swr/providers/SWRProvider.tsx +21 -0
- package/dist/templates/optional/tsconfig.json +17 -0
- package/dist/templates/optional/zustand/store/appStore.ts +13 -0
- package/package.json +40 -5
- package/dist/templates/expo-base/App.tsx +0 -32
- package/dist/templates/expo-base/app/(tabs)/_layout.tsx +0 -35
- package/dist/templates/expo-base/app/(tabs)/explore.tsx +0 -112
- package/dist/templates/expo-base/app/(tabs)/index.tsx +0 -98
- package/dist/templates/expo-base/app/modal.tsx +0 -29
- package/dist/templates/expo-base/components/external-link.tsx +0 -25
- package/dist/templates/expo-base/components/haptic-tab.tsx +0 -18
- package/dist/templates/expo-base/components/hello-wave.tsx +0 -19
- package/dist/templates/expo-base/components/parallax-scroll-view.tsx +0 -79
- package/dist/templates/expo-base/components/themed-text.tsx +0 -60
- package/dist/templates/expo-base/components/themed-view.tsx +0 -14
- package/dist/templates/expo-base/components/ui/collapsible.tsx +0 -45
- package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +0 -32
- package/dist/templates/expo-base/components/ui/icon-symbol.tsx +0 -41
- package/dist/templates/expo-base/constants/theme.ts +0 -53
- package/dist/templates/expo-base/hooks/use-color-scheme.ts +0 -1
- package/dist/templates/expo-base/hooks/use-color-scheme.web.ts +0 -21
- package/dist/templates/expo-base/hooks/use-theme-color.ts +0 -21
- package/dist/templates/expo-base/scripts/reset-project.js +0 -112
- package/dist/templates/optional/apiClient/api/client.axios.ts +0 -124
- /package/dist/templates/optional/auth-context/api/{authApi.ts → endpoints/auth.ts} +0 -0
- /package/dist/templates/optional/auth-redux/api/{authApi.ts → endpoints/auth.ts} +0 -0
- /package/dist/templates/optional/auth-zustand/api/{authApi.ts → endpoints/auth.ts} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
|
|
3
|
+
import { Formik } from 'formik';
|
|
4
|
+
import { FormikInput } from './FormikInput';
|
|
5
|
+
import { loginSchema, type LoginFormValues } from '../../schemas/auth.schema';
|
|
6
|
+
|
|
7
|
+
interface LoginFormProps {
|
|
8
|
+
onSubmit: (values: LoginFormValues) => Promise<void>;
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const initialValues: LoginFormValues = { email: '', password: '' };
|
|
13
|
+
|
|
14
|
+
export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Formik initialValues={initialValues} validationSchema={loginSchema} onSubmit={onSubmit}>
|
|
17
|
+
{({ handleSubmit }) => (
|
|
18
|
+
<View style={styles.container}>
|
|
19
|
+
<FormikInput
|
|
20
|
+
name="email"
|
|
21
|
+
label="Email"
|
|
22
|
+
autoCapitalize="none"
|
|
23
|
+
keyboardType="email-address"
|
|
24
|
+
autoComplete="email"
|
|
25
|
+
/>
|
|
26
|
+
<FormikInput
|
|
27
|
+
name="password"
|
|
28
|
+
label="Password"
|
|
29
|
+
secureTextEntry
|
|
30
|
+
autoComplete="current-password"
|
|
31
|
+
/>
|
|
32
|
+
<TouchableOpacity
|
|
33
|
+
style={[styles.button, isLoading && styles.buttonDisabled]}
|
|
34
|
+
onPress={() => handleSubmit()}
|
|
35
|
+
disabled={isLoading}
|
|
36
|
+
>
|
|
37
|
+
{isLoading ? (
|
|
38
|
+
<ActivityIndicator color="#FFFFFF" />
|
|
39
|
+
) : (
|
|
40
|
+
<Text style={styles.buttonText}>Sign In</Text>
|
|
41
|
+
)}
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
</View>
|
|
44
|
+
)}
|
|
45
|
+
</Formik>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
container: { width: '100%' },
|
|
51
|
+
button: {
|
|
52
|
+
backgroundColor: '#6366F1',
|
|
53
|
+
borderRadius: 8,
|
|
54
|
+
paddingVertical: 14,
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
marginTop: 8,
|
|
57
|
+
},
|
|
58
|
+
buttonDisabled: { opacity: 0.6 },
|
|
59
|
+
buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
|
|
60
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Yup from 'yup';
|
|
2
|
+
|
|
3
|
+
export const loginSchema = Yup.object({
|
|
4
|
+
email: Yup.string().email('Invalid email').required('Email is required'),
|
|
5
|
+
password: Yup.string().min(8, 'Minimum 8 characters').required('Password is required'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const registerSchema = Yup.object({
|
|
9
|
+
email: Yup.string().email('Invalid email').required('Email is required'),
|
|
10
|
+
password: Yup.string().min(8, 'Minimum 8 characters').required('Password is required'),
|
|
11
|
+
confirmPassword: Yup.string()
|
|
12
|
+
.oneOf([Yup.ref('password')], 'Passwords must match')
|
|
13
|
+
.required('Please confirm your password'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type LoginFormValues = Yup.InferType<typeof loginSchema>;
|
|
17
|
+
export type RegisterFormValues = Yup.InferType<typeof registerSchema>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import type en from '../locales/en.json';
|
|
3
|
+
|
|
4
|
+
// Derive the full key union from the English locale so translations are type-safe
|
|
5
|
+
type TranslationKeys = typeof en;
|
|
6
|
+
type DotPrefix<T extends string, K extends string> = `${T}.${K}`;
|
|
7
|
+
|
|
8
|
+
type LeafKeys<T, Prefix extends string = ''> = {
|
|
9
|
+
[K in keyof T]: T[K] extends object
|
|
10
|
+
? LeafKeys<T[K], Prefix extends '' ? Extract<K, string> : DotPrefix<Prefix, Extract<K, string>>>
|
|
11
|
+
: Prefix extends ''
|
|
12
|
+
? Extract<K, string>
|
|
13
|
+
: DotPrefix<Prefix, Extract<K, string>>;
|
|
14
|
+
}[keyof T];
|
|
15
|
+
|
|
16
|
+
export type AppTranslationKey = LeafKeys<TranslationKeys>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Typed wrapper around react-i18next's useTranslation.
|
|
20
|
+
* Keys are inferred from en.json so you get autocompletion + compile-time safety.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const { t } = useAppTranslation();
|
|
24
|
+
* <Text>{t('auth.signIn')}</Text>
|
|
25
|
+
*/
|
|
26
|
+
export function useAppTranslation() {
|
|
27
|
+
return useTranslation();
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
import { initReactI18next } from 'react-i18next';
|
|
3
|
+
import * as RNLocalize from 'react-native-localize';
|
|
4
|
+
|
|
5
|
+
import en from './locales/en.json';
|
|
6
|
+
import es from './locales/es.json';
|
|
7
|
+
|
|
8
|
+
const resources = {
|
|
9
|
+
en: { translation: en },
|
|
10
|
+
es: { translation: es },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const fallbackLocale = 'en';
|
|
14
|
+
const deviceLocales = RNLocalize.getLocales();
|
|
15
|
+
const bestMatch = deviceLocales[0]?.languageCode ?? fallbackLocale;
|
|
16
|
+
|
|
17
|
+
void i18n
|
|
18
|
+
.use(initReactI18next)
|
|
19
|
+
.init({
|
|
20
|
+
resources,
|
|
21
|
+
lng: bestMatch,
|
|
22
|
+
fallbackLng: fallbackLocale,
|
|
23
|
+
interpolation: {
|
|
24
|
+
// React already escapes values
|
|
25
|
+
escapeValue: false,
|
|
26
|
+
},
|
|
27
|
+
compatibilityJSON: 'v4',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export default i18n;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": {
|
|
3
|
+
"title": "Welcome",
|
|
4
|
+
"subtitle": "Get started with your app",
|
|
5
|
+
"getStarted": "Get Started"
|
|
6
|
+
},
|
|
7
|
+
"auth": {
|
|
8
|
+
"signIn": "Sign In",
|
|
9
|
+
"signUp": "Sign Up",
|
|
10
|
+
"signOut": "Sign Out",
|
|
11
|
+
"email": "Email",
|
|
12
|
+
"password": "Password",
|
|
13
|
+
"confirmPassword": "Confirm Password",
|
|
14
|
+
"forgotPassword": "Forgot password?",
|
|
15
|
+
"noAccount": "Don't have an account?",
|
|
16
|
+
"haveAccount": "Already have an account?"
|
|
17
|
+
},
|
|
18
|
+
"common": {
|
|
19
|
+
"loading": "Loading...",
|
|
20
|
+
"error": "Something went wrong",
|
|
21
|
+
"retry": "Try again",
|
|
22
|
+
"cancel": "Cancel",
|
|
23
|
+
"save": "Save",
|
|
24
|
+
"delete": "Delete",
|
|
25
|
+
"confirm": "Confirm"
|
|
26
|
+
},
|
|
27
|
+
"screens": {
|
|
28
|
+
"home": "Home",
|
|
29
|
+
"profile": "Profile",
|
|
30
|
+
"settings": "Settings"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": {
|
|
3
|
+
"title": "Bienvenido",
|
|
4
|
+
"subtitle": "Empieza con tu aplicación",
|
|
5
|
+
"getStarted": "Comenzar"
|
|
6
|
+
},
|
|
7
|
+
"auth": {
|
|
8
|
+
"signIn": "Iniciar sesión",
|
|
9
|
+
"signUp": "Registrarse",
|
|
10
|
+
"signOut": "Cerrar sesión",
|
|
11
|
+
"email": "Correo electrónico",
|
|
12
|
+
"password": "Contraseña",
|
|
13
|
+
"confirmPassword": "Confirmar contraseña",
|
|
14
|
+
"forgotPassword": "¿Olvidaste tu contraseña?",
|
|
15
|
+
"noAccount": "¿No tienes cuenta?",
|
|
16
|
+
"haveAccount": "¿Ya tienes cuenta?"
|
|
17
|
+
},
|
|
18
|
+
"common": {
|
|
19
|
+
"loading": "Cargando...",
|
|
20
|
+
"error": "Algo salió mal",
|
|
21
|
+
"retry": "Intentar de nuevo",
|
|
22
|
+
"cancel": "Cancelar",
|
|
23
|
+
"save": "Guardar",
|
|
24
|
+
"delete": "Eliminar",
|
|
25
|
+
"confirm": "Confirmar"
|
|
26
|
+
},
|
|
27
|
+
"screens": {
|
|
28
|
+
"home": "Inicio",
|
|
29
|
+
"profile": "Perfil",
|
|
30
|
+
"settings": "Configuración"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
appId: com.rnstarterkit.app
|
|
2
|
+
---
|
|
3
|
+
- launchApp
|
|
4
|
+
- tapOn: "Get Started"
|
|
5
|
+
- tapOn: "Sign In"
|
|
6
|
+
- tapOn:
|
|
7
|
+
id: "email-input"
|
|
8
|
+
- inputText: "test@example.com"
|
|
9
|
+
- tapOn:
|
|
10
|
+
id: "password-input"
|
|
11
|
+
- inputText: "password123"
|
|
12
|
+
- tapOn: "Sign In"
|
|
13
|
+
- assertVisible: "Home"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
appId: com.rnstarterkit.app
|
|
2
|
+
---
|
|
3
|
+
- launchApp
|
|
4
|
+
- tapOn: "Get Started"
|
|
5
|
+
- tapOn: "Sign In"
|
|
6
|
+
- tapOn:
|
|
7
|
+
id: "email-input"
|
|
8
|
+
- inputText: "test@example.com"
|
|
9
|
+
- tapOn:
|
|
10
|
+
id: "password-input"
|
|
11
|
+
- inputText: "password123"
|
|
12
|
+
- tapOn: "Sign In"
|
|
13
|
+
- assertVisible: "Home"
|
|
14
|
+
- tapOn: "Profile"
|
|
15
|
+
- tapOn: "Sign Out"
|
|
16
|
+
- assertVisible: "Welcome"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MMKV } from "react-native-mmkv";
|
|
2
|
+
|
|
3
|
+
const storage = new MMKV();
|
|
4
|
+
|
|
5
|
+
export const storeToken = (token: string): void => {
|
|
6
|
+
storage.set("@token", token);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getToken = (): string | null => {
|
|
10
|
+
return storage.getString("@token") ?? null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const removeToken = (): void => {
|
|
14
|
+
storage.delete("@token");
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { storage as mmkvStorage };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
|
|
3
|
+
import { useForm, FormProvider } from 'react-hook-form';
|
|
4
|
+
import { RHFInput } from './RHFInput';
|
|
5
|
+
import { loginRules, type LoginFormValues } from '../../schemas/auth.schema';
|
|
6
|
+
|
|
7
|
+
interface LoginFormProps {
|
|
8
|
+
onSubmit: (values: LoginFormValues) => Promise<void>;
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
|
|
13
|
+
const methods = useForm<LoginFormValues>({
|
|
14
|
+
defaultValues: { email: '', password: '' },
|
|
15
|
+
mode: 'onBlur',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<FormProvider {...methods}>
|
|
20
|
+
<View style={styles.container}>
|
|
21
|
+
<RHFInput
|
|
22
|
+
name="email"
|
|
23
|
+
label="Email"
|
|
24
|
+
rules={loginRules.email}
|
|
25
|
+
autoCapitalize="none"
|
|
26
|
+
keyboardType="email-address"
|
|
27
|
+
autoComplete="email"
|
|
28
|
+
/>
|
|
29
|
+
<RHFInput
|
|
30
|
+
name="password"
|
|
31
|
+
label="Password"
|
|
32
|
+
rules={loginRules.password}
|
|
33
|
+
secureTextEntry
|
|
34
|
+
autoComplete="current-password"
|
|
35
|
+
/>
|
|
36
|
+
<TouchableOpacity
|
|
37
|
+
style={[styles.button, isLoading && styles.buttonDisabled]}
|
|
38
|
+
onPress={methods.handleSubmit(onSubmit)}
|
|
39
|
+
disabled={isLoading}
|
|
40
|
+
>
|
|
41
|
+
{isLoading ? (
|
|
42
|
+
<ActivityIndicator color="#FFFFFF" />
|
|
43
|
+
) : (
|
|
44
|
+
<Text style={styles.buttonText}>Sign In</Text>
|
|
45
|
+
)}
|
|
46
|
+
</TouchableOpacity>
|
|
47
|
+
</View>
|
|
48
|
+
</FormProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
container: { width: '100%' },
|
|
54
|
+
button: {
|
|
55
|
+
backgroundColor: '#6366F1',
|
|
56
|
+
borderRadius: 8,
|
|
57
|
+
paddingVertical: 14,
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
marginTop: 8,
|
|
60
|
+
},
|
|
61
|
+
buttonDisabled: { opacity: 0.6 },
|
|
62
|
+
buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
|
|
63
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, TextInput, Text, StyleSheet, TextInputProps } from 'react-native';
|
|
3
|
+
import { Controller, useFormContext } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
interface RHFInputProps extends TextInputProps {
|
|
6
|
+
name: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function RHFInput({ name, label, ...props }: RHFInputProps) {
|
|
11
|
+
const { control, formState: { errors } } = useFormContext();
|
|
12
|
+
const error = errors[name]?.message as string | undefined;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<View style={styles.container}>
|
|
16
|
+
<Text style={styles.label}>{label}</Text>
|
|
17
|
+
<Controller
|
|
18
|
+
control={control}
|
|
19
|
+
name={name}
|
|
20
|
+
render={({ field: { onChange, onBlur, value } }) => (
|
|
21
|
+
<TextInput
|
|
22
|
+
style={[styles.input, error ? styles.inputError : undefined]}
|
|
23
|
+
value={value}
|
|
24
|
+
onChangeText={onChange}
|
|
25
|
+
onBlur={onBlur}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)}
|
|
29
|
+
/>
|
|
30
|
+
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const styles = StyleSheet.create({
|
|
36
|
+
container: { marginBottom: 16 },
|
|
37
|
+
label: { fontSize: 14, fontWeight: '500', marginBottom: 6, color: '#374151' },
|
|
38
|
+
input: {
|
|
39
|
+
borderWidth: 1,
|
|
40
|
+
borderColor: '#D1D5DB',
|
|
41
|
+
borderRadius: 8,
|
|
42
|
+
paddingHorizontal: 12,
|
|
43
|
+
paddingVertical: 10,
|
|
44
|
+
fontSize: 16,
|
|
45
|
+
color: '#111827',
|
|
46
|
+
backgroundColor: '#FFFFFF',
|
|
47
|
+
},
|
|
48
|
+
inputError: { borderColor: '#EF4444' },
|
|
49
|
+
errorText: { marginTop: 4, fontSize: 12, color: '#EF4444' },
|
|
50
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const loginRules = {
|
|
2
|
+
email: {
|
|
3
|
+
required: 'Email is required',
|
|
4
|
+
pattern: { value: /\S+@\S+\.\S+/, message: 'Invalid email address' },
|
|
5
|
+
},
|
|
6
|
+
password: {
|
|
7
|
+
required: 'Password is required',
|
|
8
|
+
minLength: { value: 8, message: 'Minimum 8 characters' },
|
|
9
|
+
},
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const registerRules = {
|
|
13
|
+
email: loginRules.email,
|
|
14
|
+
password: loginRules.password,
|
|
15
|
+
confirmPassword: {
|
|
16
|
+
required: 'Please confirm your password',
|
|
17
|
+
},
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export interface LoginFormValues {
|
|
21
|
+
email: string;
|
|
22
|
+
password: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RegisterFormValues {
|
|
26
|
+
email: string;
|
|
27
|
+
password: string;
|
|
28
|
+
confirmPassword: string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMutation,
|
|
3
|
+
type UseMutationOptions,
|
|
4
|
+
type UseMutationResult,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
6
|
+
|
|
7
|
+
export function useAppMutation<
|
|
8
|
+
TData = unknown,
|
|
9
|
+
TError = Error,
|
|
10
|
+
TVariables = void,
|
|
11
|
+
TContext = unknown,
|
|
12
|
+
>(
|
|
13
|
+
options: UseMutationOptions<TData, TError, TVariables, TContext>,
|
|
14
|
+
): UseMutationResult<TData, TError, TVariables, TContext> {
|
|
15
|
+
return useMutation<TData, TError, TVariables, TContext>(options);
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useQuery,
|
|
3
|
+
type UseQueryOptions,
|
|
4
|
+
type QueryKey,
|
|
5
|
+
type UseQueryResult,
|
|
6
|
+
} from "@tanstack/react-query";
|
|
7
|
+
|
|
8
|
+
export function useAppQuery<TData, TError = Error>(
|
|
9
|
+
options: UseQueryOptions<TData, TError, TData, QueryKey>,
|
|
10
|
+
): UseQueryResult<TData, TError> {
|
|
11
|
+
return useQuery<TData, TError, TData, QueryKey>(options);
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
export const queryClient = new QueryClient({
|
|
4
|
+
defaultOptions: {
|
|
5
|
+
queries: {
|
|
6
|
+
retry: 2,
|
|
7
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
8
|
+
gcTime: 1000 * 60 * 10, // 10 minutes
|
|
9
|
+
},
|
|
10
|
+
mutations: {
|
|
11
|
+
retry: 0,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { useDispatch, useSelector } from "react-redux";
|
|
2
|
+
import type { TypedUseSelectorHook } from "react-redux";
|
|
3
|
+
import type { RootState, AppDispatch } from "./store";
|
|
4
|
+
|
|
5
|
+
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
6
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { configureStore } from "@reduxjs/toolkit";
|
|
2
|
+
|
|
3
|
+
export const store = configureStore({
|
|
4
|
+
reducer: {
|
|
5
|
+
// Add your slice reducers here
|
|
6
|
+
// example: counter: counterReducer,
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
11
|
+
export type AppDispatch = typeof store.dispatch;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/react-native';
|
|
2
|
+
|
|
3
|
+
export function initSentry() {
|
|
4
|
+
Sentry.init({
|
|
5
|
+
// Replace with your Sentry DSN from https://sentry.io
|
|
6
|
+
dsn: '__YOUR_DSN__',
|
|
7
|
+
tracesSampleRate: 1.0,
|
|
8
|
+
// Disable verbose logging in production
|
|
9
|
+
debug: __DEV__,
|
|
10
|
+
// Attach JS/native stack traces to all events
|
|
11
|
+
attachStacktrace: true,
|
|
12
|
+
environment: __DEV__ ? 'development' : 'production',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Manually capture an exception (e.g. in a catch block) */
|
|
17
|
+
export function captureException(error: unknown) {
|
|
18
|
+
Sentry.captureException(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Add a breadcrumb for tracing user flows */
|
|
22
|
+
export function addBreadcrumb(message: string, category = 'app') {
|
|
23
|
+
Sentry.addBreadcrumb({ message, category });
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import useSWR, { type SWRConfiguration, type SWRResponse } from "swr";
|
|
2
|
+
|
|
3
|
+
const defaultFetcher = async (url: string): Promise<unknown> => {
|
|
4
|
+
const res = await fetch(url);
|
|
5
|
+
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
|
|
6
|
+
return res.json();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function useSWRFetch<TData = unknown, TError = Error>(
|
|
10
|
+
key: string | null,
|
|
11
|
+
config?: SWRConfiguration<TData, TError>,
|
|
12
|
+
): SWRResponse<TData, TError> {
|
|
13
|
+
return useSWR<TData, TError>(key, defaultFetcher as (key: string) => Promise<TData>, config);
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SWRConfig } from "swr";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function SWRProvider({ children }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<SWRConfig
|
|
11
|
+
value={{
|
|
12
|
+
revalidateOnFocus: true,
|
|
13
|
+
revalidateOnReconnect: true,
|
|
14
|
+
shouldRetryOnError: true,
|
|
15
|
+
errorRetryCount: 3,
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</SWRConfig>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"jsx": "react-native",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUnusedLocals": true,
|
|
8
|
+
"noUnusedParameters": true,
|
|
9
|
+
"noFallthroughCasesInSwitch": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"types": ["jest", "node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
16
|
+
"exclude": ["node_modules", "**/Pods", "dist"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
type AppState = {
|
|
4
|
+
// Add your global state here
|
|
5
|
+
// example: count: number;
|
|
6
|
+
// example: increment: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const useAppStore = create<AppState>(() => ({
|
|
10
|
+
// Initialize state here
|
|
11
|
+
// example: count: 0,
|
|
12
|
+
// example: increment: () => set((state) => ({ count: state.count + 1 })),
|
|
13
|
+
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@highbeek/create-rnstarterkit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI to scaffold production-ready React Native app structures.",
|
|
5
5
|
"main": "dist/src/generators/appGenerator.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,12 +12,42 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc -p tsconfig.build.json && node scripts/copy-templates.js",
|
|
15
|
+
"lint": "eslint",
|
|
16
|
+
"test": "node scripts/test-generator.mjs",
|
|
17
|
+
"test:ci": "SKIP_INSTALL=1 node scripts/test-generator.mjs",
|
|
15
18
|
"prepublishOnly": "npm run build",
|
|
16
|
-
"
|
|
19
|
+
"prepare": "husky"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react-native",
|
|
23
|
+
"expo",
|
|
24
|
+
"cli",
|
|
25
|
+
"starter-kit",
|
|
26
|
+
"boilerplate",
|
|
27
|
+
"scaffold",
|
|
28
|
+
"nativewind",
|
|
29
|
+
"tailwind",
|
|
30
|
+
"react-native-cli",
|
|
31
|
+
"create-app"
|
|
32
|
+
],
|
|
33
|
+
"author": "Ibukun Agboola <highbeek>",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/Highbeek/RNStarterKit.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/Highbeek/RNStarterKit#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Highbeek/RNStarterKit/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"lint-staged": {
|
|
47
|
+
"*.ts": [
|
|
48
|
+
"eslint --fix"
|
|
49
|
+
]
|
|
17
50
|
},
|
|
18
|
-
"keywords": [],
|
|
19
|
-
"author": "",
|
|
20
|
-
"license": "ISC",
|
|
21
51
|
"type": "commonjs",
|
|
22
52
|
"dependencies": {
|
|
23
53
|
"chalk": "^5.6.2",
|
|
@@ -29,6 +59,11 @@
|
|
|
29
59
|
"devDependencies": {
|
|
30
60
|
"@types/inquirer": "^9.0.9",
|
|
31
61
|
"@types/node": "^25.2.3",
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
63
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
64
|
+
"eslint": "^10.0.3",
|
|
65
|
+
"husky": "^9.1.7",
|
|
66
|
+
"lint-staged": "^16.3.3",
|
|
32
67
|
"ts-node": "^10.9.2",
|
|
33
68
|
"typescript": "^5.9.3"
|
|
34
69
|
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { SafeAreaView, StyleSheet, Text } from "react-native";
|
|
3
|
-
|
|
4
|
-
export default function App() {
|
|
5
|
-
return (
|
|
6
|
-
<SafeAreaView style={styles.container}>
|
|
7
|
-
<Text style={styles.title}>RN Starter Kit</Text>
|
|
8
|
-
<Text style={styles.subtitle}>
|
|
9
|
-
Build your app by adding screens and navigation.
|
|
10
|
-
</Text>
|
|
11
|
-
</SafeAreaView>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const styles = StyleSheet.create({
|
|
16
|
-
container: {
|
|
17
|
-
flex: 1,
|
|
18
|
-
alignItems: "center",
|
|
19
|
-
justifyContent: "center",
|
|
20
|
-
padding: 24,
|
|
21
|
-
},
|
|
22
|
-
title: {
|
|
23
|
-
fontSize: 24,
|
|
24
|
-
fontWeight: "700",
|
|
25
|
-
marginBottom: 8,
|
|
26
|
-
},
|
|
27
|
-
subtitle: {
|
|
28
|
-
fontSize: 15,
|
|
29
|
-
textAlign: "center",
|
|
30
|
-
color: "#666",
|
|
31
|
-
},
|
|
32
|
-
});
|