@croacroa/react-native-template 1.0.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.
Files changed (109) hide show
  1. package/.env.example +18 -0
  2. package/.eslintrc.js +55 -0
  3. package/.github/workflows/ci.yml +184 -0
  4. package/.github/workflows/eas-build.yml +55 -0
  5. package/.github/workflows/eas-update.yml +50 -0
  6. package/.gitignore +62 -0
  7. package/.prettierrc +11 -0
  8. package/.storybook/main.ts +28 -0
  9. package/.storybook/preview.tsx +30 -0
  10. package/CHANGELOG.md +106 -0
  11. package/CONTRIBUTING.md +377 -0
  12. package/README.md +399 -0
  13. package/__tests__/components/Button.test.tsx +74 -0
  14. package/__tests__/hooks/useAuth.test.tsx +499 -0
  15. package/__tests__/services/api.test.ts +535 -0
  16. package/__tests__/utils/cn.test.ts +39 -0
  17. package/app/(auth)/_layout.tsx +36 -0
  18. package/app/(auth)/home.tsx +117 -0
  19. package/app/(auth)/profile.tsx +152 -0
  20. package/app/(auth)/settings.tsx +147 -0
  21. package/app/(public)/_layout.tsx +21 -0
  22. package/app/(public)/forgot-password.tsx +127 -0
  23. package/app/(public)/login.tsx +120 -0
  24. package/app/(public)/onboarding.tsx +5 -0
  25. package/app/(public)/register.tsx +139 -0
  26. package/app/_layout.tsx +97 -0
  27. package/app/index.tsx +21 -0
  28. package/app.config.ts +72 -0
  29. package/assets/images/.gitkeep +7 -0
  30. package/assets/images/adaptive-icon.png +0 -0
  31. package/assets/images/favicon.png +0 -0
  32. package/assets/images/icon.png +0 -0
  33. package/assets/images/notification-icon.png +0 -0
  34. package/assets/images/splash.png +0 -0
  35. package/babel.config.js +10 -0
  36. package/components/ErrorBoundary.tsx +169 -0
  37. package/components/forms/FormInput.tsx +78 -0
  38. package/components/forms/index.ts +1 -0
  39. package/components/onboarding/OnboardingScreen.tsx +370 -0
  40. package/components/onboarding/index.ts +2 -0
  41. package/components/ui/AnimatedButton.tsx +156 -0
  42. package/components/ui/AnimatedCard.tsx +108 -0
  43. package/components/ui/Avatar.tsx +316 -0
  44. package/components/ui/Badge.tsx +416 -0
  45. package/components/ui/BottomSheet.tsx +307 -0
  46. package/components/ui/Button.stories.tsx +115 -0
  47. package/components/ui/Button.tsx +104 -0
  48. package/components/ui/Card.stories.tsx +84 -0
  49. package/components/ui/Card.tsx +32 -0
  50. package/components/ui/Checkbox.tsx +261 -0
  51. package/components/ui/Input.stories.tsx +106 -0
  52. package/components/ui/Input.tsx +117 -0
  53. package/components/ui/Modal.tsx +98 -0
  54. package/components/ui/OptimizedImage.tsx +369 -0
  55. package/components/ui/Select.tsx +240 -0
  56. package/components/ui/Skeleton.tsx +180 -0
  57. package/components/ui/index.ts +18 -0
  58. package/constants/config.ts +54 -0
  59. package/docs/adr/001-state-management.md +79 -0
  60. package/docs/adr/002-styling-approach.md +130 -0
  61. package/docs/adr/003-data-fetching.md +155 -0
  62. package/docs/adr/004-auth-adapter-pattern.md +144 -0
  63. package/docs/adr/README.md +78 -0
  64. package/eas.json +47 -0
  65. package/global.css +10 -0
  66. package/hooks/index.ts +25 -0
  67. package/hooks/useApi.ts +236 -0
  68. package/hooks/useAuth.tsx +290 -0
  69. package/hooks/useBiometrics.ts +295 -0
  70. package/hooks/useDeepLinking.ts +256 -0
  71. package/hooks/useNotifications.ts +138 -0
  72. package/hooks/useOffline.ts +69 -0
  73. package/hooks/usePerformance.ts +434 -0
  74. package/hooks/useTheme.tsx +85 -0
  75. package/hooks/useUpdates.ts +358 -0
  76. package/i18n/index.ts +77 -0
  77. package/i18n/locales/en.json +101 -0
  78. package/i18n/locales/fr.json +101 -0
  79. package/jest.config.js +32 -0
  80. package/maestro/README.md +113 -0
  81. package/maestro/config.yaml +35 -0
  82. package/maestro/flows/login.yaml +62 -0
  83. package/maestro/flows/navigation.yaml +68 -0
  84. package/maestro/flows/offline.yaml +60 -0
  85. package/maestro/flows/register.yaml +94 -0
  86. package/metro.config.js +6 -0
  87. package/nativewind-env.d.ts +1 -0
  88. package/package.json +170 -0
  89. package/scripts/init.ps1 +162 -0
  90. package/scripts/init.sh +174 -0
  91. package/services/analytics.ts +428 -0
  92. package/services/api.ts +340 -0
  93. package/services/authAdapter.ts +333 -0
  94. package/services/index.ts +22 -0
  95. package/services/queryClient.ts +97 -0
  96. package/services/sentry.ts +131 -0
  97. package/services/storage.ts +82 -0
  98. package/stores/appStore.ts +54 -0
  99. package/stores/index.ts +2 -0
  100. package/stores/notificationStore.ts +40 -0
  101. package/tailwind.config.js +47 -0
  102. package/tsconfig.json +26 -0
  103. package/types/index.ts +42 -0
  104. package/types/user.ts +63 -0
  105. package/utils/accessibility.ts +446 -0
  106. package/utils/cn.ts +14 -0
  107. package/utils/index.ts +43 -0
  108. package/utils/toast.ts +113 -0
  109. package/utils/validation.ts +67 -0
@@ -0,0 +1,97 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
3
+ import AsyncStorage from "@react-native-async-storage/async-storage";
4
+ import NetInfo from "@react-native-community/netinfo";
5
+ import { onlineManager, focusManager } from "@tanstack/react-query";
6
+ import { AppState, Platform } from "react-native";
7
+ import type { AppStateStatus } from "react-native";
8
+
9
+ /**
10
+ * Configure online state management
11
+ * TanStack Query will pause queries when offline and resume when online
12
+ */
13
+ export function setupOnlineManager() {
14
+ onlineManager.setEventListener((setOnline) => {
15
+ return NetInfo.addEventListener((state) => {
16
+ setOnline(!!state.isConnected);
17
+ });
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Configure focus management
23
+ * Refetch queries when app comes to foreground
24
+ */
25
+ export function setupFocusManager() {
26
+ function onAppStateChange(status: AppStateStatus) {
27
+ if (Platform.OS !== "web") {
28
+ focusManager.setFocused(status === "active");
29
+ }
30
+ }
31
+
32
+ const subscription = AppState.addEventListener("change", onAppStateChange);
33
+ return () => subscription.remove();
34
+ }
35
+
36
+ /**
37
+ * Create the query client with production-ready defaults
38
+ */
39
+ export function createQueryClient() {
40
+ return new QueryClient({
41
+ defaultOptions: {
42
+ queries: {
43
+ // Cache data for 5 minutes by default
44
+ staleTime: 1000 * 60 * 5,
45
+ // Keep unused data in cache for 30 minutes
46
+ gcTime: 1000 * 60 * 30,
47
+ // Retry failed requests up to 3 times
48
+ retry: 3,
49
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
50
+ // Don't refetch on window focus in dev (annoying)
51
+ refetchOnWindowFocus: !__DEV__,
52
+ // Refetch on reconnect
53
+ refetchOnReconnect: true,
54
+ // Keep previous data while fetching new data
55
+ placeholderData: (previousData: unknown) => previousData,
56
+ },
57
+ mutations: {
58
+ // Retry mutations once on failure
59
+ retry: 1,
60
+ retryDelay: 1000,
61
+ },
62
+ },
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Create the async storage persister for offline support
68
+ */
69
+ export function createPersister() {
70
+ return createAsyncStoragePersister({
71
+ storage: AsyncStorage,
72
+ key: "REACT_QUERY_CACHE",
73
+ // Throttle writes to storage (1 second)
74
+ throttleTime: 1000,
75
+ // Serialize/deserialize functions
76
+ serialize: JSON.stringify,
77
+ deserialize: JSON.parse,
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Default persist options
83
+ */
84
+ export const persistOptions = {
85
+ persister: createPersister(),
86
+ // Maximum age of persisted data (24 hours)
87
+ maxAge: 1000 * 60 * 60 * 24,
88
+ // Only persist successful queries
89
+ dehydrateOptions: {
90
+ shouldDehydrateQuery: (query: { state: { status: string } }) => {
91
+ return query.state.status === "success";
92
+ },
93
+ },
94
+ };
95
+
96
+ // Pre-configured query client singleton
97
+ export const queryClient = createQueryClient();
@@ -0,0 +1,131 @@
1
+ import * as Sentry from "@sentry/react-native";
2
+ import { IS_DEV, IS_PROD, APP_VERSION } from "@/constants/config";
3
+
4
+ // TODO: Replace with your actual Sentry DSN
5
+ // Get it from: https://sentry.io -> Project Settings -> Client Keys (DSN)
6
+ const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN || "";
7
+
8
+ /**
9
+ * Initialize Sentry error tracking
10
+ * Call this early in app startup (before RootLayout)
11
+ */
12
+ export function initSentry() {
13
+ if (!SENTRY_DSN) {
14
+ if (IS_DEV) {
15
+ console.log("[Sentry] No DSN configured, skipping initialization");
16
+ }
17
+ return;
18
+ }
19
+
20
+ Sentry.init({
21
+ dsn: SENTRY_DSN,
22
+ debug: IS_DEV,
23
+ enabled: !IS_DEV, // Only enable in non-dev environments
24
+ environment: IS_PROD ? "production" : "staging",
25
+ release: APP_VERSION,
26
+
27
+ // Performance Monitoring
28
+ tracesSampleRate: IS_PROD ? 0.2 : 1.0, // 20% in prod, 100% in staging
29
+
30
+ // Session Replay (if needed)
31
+ // replaysSessionSampleRate: 0.1,
32
+ // replaysOnErrorSampleRate: 1.0,
33
+
34
+ // Integrations
35
+ integrations: [
36
+ Sentry.reactNativeTracingIntegration(),
37
+ ],
38
+
39
+ // Filter out certain errors
40
+ beforeSend(event) {
41
+ // Don't send events in dev
42
+ if (IS_DEV) {
43
+ console.log("[Sentry] Would send event:", event);
44
+ return null;
45
+ }
46
+
47
+ // Filter out network errors that are expected
48
+ if (event.message?.includes("Network request failed")) {
49
+ return null;
50
+ }
51
+
52
+ return event;
53
+ },
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Capture an exception with optional context
59
+ */
60
+ export function captureException(
61
+ error: Error,
62
+ context?: Record<string, unknown>
63
+ ) {
64
+ if (IS_DEV) {
65
+ console.error("[Sentry] Exception:", error, context);
66
+ return;
67
+ }
68
+
69
+ Sentry.captureException(error, {
70
+ extra: context,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Capture a message with optional level
76
+ */
77
+ export function captureMessage(
78
+ message: string,
79
+ level: Sentry.SeverityLevel = "info"
80
+ ) {
81
+ if (IS_DEV) {
82
+ console.log(`[Sentry] ${level}: ${message}`);
83
+ return;
84
+ }
85
+
86
+ Sentry.captureMessage(message, level);
87
+ }
88
+
89
+ /**
90
+ * Set user context for error tracking
91
+ */
92
+ export function setUser(user: { id: string; email?: string; name?: string } | null) {
93
+ if (user) {
94
+ Sentry.setUser({
95
+ id: user.id,
96
+ email: user.email,
97
+ username: user.name,
98
+ });
99
+ } else {
100
+ Sentry.setUser(null);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Add breadcrumb for debugging
106
+ */
107
+ export function addBreadcrumb(
108
+ category: string,
109
+ message: string,
110
+ data?: Record<string, unknown>
111
+ ) {
112
+ Sentry.addBreadcrumb({
113
+ category,
114
+ message,
115
+ data,
116
+ level: "info",
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Start a performance transaction
122
+ */
123
+ export function startTransaction(name: string, op: string) {
124
+ return Sentry.startInactiveSpan({
125
+ name,
126
+ op,
127
+ });
128
+ }
129
+
130
+ // Re-export Sentry for advanced usage
131
+ export { Sentry };
@@ -0,0 +1,82 @@
1
+ import AsyncStorage from "@react-native-async-storage/async-storage";
2
+ import * as SecureStore from "expo-secure-store";
3
+
4
+ /**
5
+ * Regular storage for non-sensitive data
6
+ * Uses AsyncStorage under the hood
7
+ */
8
+ export const storage = {
9
+ async get<T>(key: string): Promise<T | null> {
10
+ try {
11
+ const value = await AsyncStorage.getItem(key);
12
+ return value ? JSON.parse(value) : null;
13
+ } catch (error) {
14
+ console.error(`Failed to get ${key} from storage:`, error);
15
+ return null;
16
+ }
17
+ },
18
+
19
+ async set<T>(key: string, value: T): Promise<void> {
20
+ try {
21
+ await AsyncStorage.setItem(key, JSON.stringify(value));
22
+ } catch (error) {
23
+ console.error(`Failed to set ${key} in storage:`, error);
24
+ }
25
+ },
26
+
27
+ async remove(key: string): Promise<void> {
28
+ try {
29
+ await AsyncStorage.removeItem(key);
30
+ } catch (error) {
31
+ console.error(`Failed to remove ${key} from storage:`, error);
32
+ }
33
+ },
34
+
35
+ async clear(): Promise<void> {
36
+ try {
37
+ await AsyncStorage.clear();
38
+ } catch (error) {
39
+ console.error("Failed to clear storage:", error);
40
+ }
41
+ },
42
+
43
+ async getAllKeys(): Promise<readonly string[]> {
44
+ try {
45
+ return await AsyncStorage.getAllKeys();
46
+ } catch (error) {
47
+ console.error("Failed to get all keys from storage:", error);
48
+ return [];
49
+ }
50
+ },
51
+ };
52
+
53
+ /**
54
+ * Secure storage for sensitive data (tokens, credentials)
55
+ * Uses expo-secure-store (Keychain on iOS, EncryptedSharedPreferences on Android)
56
+ */
57
+ export const secureStorage = {
58
+ async get(key: string): Promise<string | null> {
59
+ try {
60
+ return await SecureStore.getItemAsync(key);
61
+ } catch (error) {
62
+ console.error(`Failed to get ${key} from secure storage:`, error);
63
+ return null;
64
+ }
65
+ },
66
+
67
+ async set(key: string, value: string): Promise<void> {
68
+ try {
69
+ await SecureStore.setItemAsync(key, value);
70
+ } catch (error) {
71
+ console.error(`Failed to set ${key} in secure storage:`, error);
72
+ }
73
+ },
74
+
75
+ async remove(key: string): Promise<void> {
76
+ try {
77
+ await SecureStore.deleteItemAsync(key);
78
+ } catch (error) {
79
+ console.error(`Failed to remove ${key} from secure storage:`, error);
80
+ }
81
+ },
82
+ };
@@ -0,0 +1,54 @@
1
+ import { create } from "zustand";
2
+ import { persist, createJSONStorage } from "zustand/middleware";
3
+ import AsyncStorage from "@react-native-async-storage/async-storage";
4
+
5
+ interface AppState {
6
+ // App-wide loading state
7
+ isLoading: boolean;
8
+ setIsLoading: (loading: boolean) => void;
9
+
10
+ // Onboarding
11
+ hasCompletedOnboarding: boolean;
12
+ setHasCompletedOnboarding: (completed: boolean) => void;
13
+
14
+ // Feature flags (example)
15
+ featureFlags: Record<string, boolean>;
16
+ setFeatureFlag: (key: string, value: boolean) => void;
17
+
18
+ // Reset all state
19
+ reset: () => void;
20
+ }
21
+
22
+ const initialState = {
23
+ isLoading: false,
24
+ hasCompletedOnboarding: false,
25
+ featureFlags: {},
26
+ };
27
+
28
+ export const useAppStore = create<AppState>()(
29
+ persist(
30
+ (set) => ({
31
+ ...initialState,
32
+
33
+ setIsLoading: (loading) => set({ isLoading: loading }),
34
+
35
+ setHasCompletedOnboarding: (completed) =>
36
+ set({ hasCompletedOnboarding: completed }),
37
+
38
+ setFeatureFlag: (key, value) =>
39
+ set((state) => ({
40
+ featureFlags: { ...state.featureFlags, [key]: value },
41
+ })),
42
+
43
+ reset: () => set(initialState),
44
+ }),
45
+ {
46
+ name: "app-storage",
47
+ storage: createJSONStorage(() => AsyncStorage),
48
+ partialize: (state) => ({
49
+ hasCompletedOnboarding: state.hasCompletedOnboarding,
50
+ featureFlags: state.featureFlags,
51
+ }),
52
+ }
53
+ )
54
+ );
@@ -0,0 +1,2 @@
1
+ export { useAppStore } from "./appStore";
2
+ export { useNotificationStore } from "./notificationStore";
@@ -0,0 +1,40 @@
1
+ import { create } from "zustand";
2
+ import { persist, createJSONStorage } from "zustand/middleware";
3
+ import AsyncStorage from "@react-native-async-storage/async-storage";
4
+ import * as Notifications from "expo-notifications";
5
+
6
+ interface NotificationState {
7
+ pushToken: string | null;
8
+ isEnabled: boolean;
9
+ lastNotification: Notifications.Notification | null;
10
+ setPushToken: (token: string | null) => void;
11
+ setIsEnabled: (enabled: boolean) => void;
12
+ toggleNotifications: () => void;
13
+ setLastNotification: (notification: Notifications.Notification | null) => void;
14
+ }
15
+
16
+ export const useNotificationStore = create<NotificationState>()(
17
+ persist(
18
+ (set, get) => ({
19
+ pushToken: null,
20
+ isEnabled: true,
21
+ lastNotification: null,
22
+
23
+ setPushToken: (token) => set({ pushToken: token }),
24
+
25
+ setIsEnabled: (enabled) => set({ isEnabled: enabled }),
26
+
27
+ toggleNotifications: () => set({ isEnabled: !get().isEnabled }),
28
+
29
+ setLastNotification: (notification) =>
30
+ set({ lastNotification: notification }),
31
+ }),
32
+ {
33
+ name: "notification-storage",
34
+ storage: createJSONStorage(() => AsyncStorage),
35
+ partialize: (state) => ({
36
+ isEnabled: state.isEnabled,
37
+ }),
38
+ }
39
+ )
40
+ );
@@ -0,0 +1,47 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./app/**/*.{js,jsx,ts,tsx}",
5
+ "./components/**/*.{js,jsx,ts,tsx}",
6
+ ],
7
+ presets: [require("nativewind/preset")],
8
+ theme: {
9
+ extend: {
10
+ colors: {
11
+ // Croacroa emerald green
12
+ primary: {
13
+ 50: "#ecfdf5",
14
+ 100: "#d1fae5",
15
+ 200: "#a7f3d0",
16
+ 300: "#6ee7b7",
17
+ 400: "#34d399",
18
+ 500: "#10b981",
19
+ 600: "#059669",
20
+ 700: "#047857",
21
+ 800: "#065f46",
22
+ 900: "#064e3b",
23
+ },
24
+ background: {
25
+ light: "#ffffff",
26
+ dark: "#0f172a",
27
+ },
28
+ surface: {
29
+ light: "#f8fafc",
30
+ dark: "#1e293b",
31
+ },
32
+ text: {
33
+ light: "#0f172a",
34
+ dark: "#f8fafc",
35
+ },
36
+ muted: {
37
+ light: "#64748b",
38
+ dark: "#94a3b8",
39
+ },
40
+ },
41
+ fontFamily: {
42
+ sans: ["Inter", "system-ui", "sans-serif"],
43
+ },
44
+ },
45
+ },
46
+ plugins: [],
47
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "extends": "expo/tsconfig.base",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "baseUrl": ".",
6
+ "paths": {
7
+ "@/*": ["./*"],
8
+ "@/components/*": ["components/*"],
9
+ "@/hooks/*": ["hooks/*"],
10
+ "@/stores/*": ["stores/*"],
11
+ "@/services/*": ["services/*"],
12
+ "@/theme/*": ["theme/*"],
13
+ "@/constants/*": ["constants/*"],
14
+ "@/utils/*": ["utils/*"],
15
+ "@/types/*": ["types/*"]
16
+ },
17
+ "types": ["jest", "@testing-library/jest-native"]
18
+ },
19
+ "include": [
20
+ "**/*.ts",
21
+ "**/*.tsx",
22
+ ".expo/types/**/*.ts",
23
+ "expo-env.d.ts"
24
+ ],
25
+ "exclude": ["node_modules"]
26
+ }
package/types/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ // Re-export all types from a single entry point
2
+ export * from "./user";
3
+
4
+ // API Response types
5
+ export interface ApiResponse<T> {
6
+ data: T;
7
+ message?: string;
8
+ success: boolean;
9
+ }
10
+
11
+ export interface PaginatedResponse<T> {
12
+ data: T[];
13
+ page: number;
14
+ pageSize: number;
15
+ total: number;
16
+ totalPages: number;
17
+ }
18
+
19
+ // Error types
20
+ export interface ApiError {
21
+ message: string;
22
+ code?: string;
23
+ status: number;
24
+ details?: Record<string, string[]>;
25
+ }
26
+
27
+ // Navigation types (extend as needed)
28
+ export type RootStackParamList = {
29
+ "(public)/login": undefined;
30
+ "(public)/register": undefined;
31
+ "(public)/forgot-password": undefined;
32
+ "(auth)/home": undefined;
33
+ "(auth)/profile": undefined;
34
+ "(auth)/settings": undefined;
35
+ };
36
+
37
+ // Notification types
38
+ export interface NotificationData {
39
+ type: "message" | "alert" | "promotion";
40
+ screen?: string;
41
+ params?: Record<string, unknown>;
42
+ }
package/types/user.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Centralized User types - Single source of truth
3
+ * Import from here across the entire app
4
+ */
5
+
6
+ export interface User {
7
+ id: string;
8
+ email: string;
9
+ name: string;
10
+ avatar?: string;
11
+ createdAt?: string;
12
+ updatedAt?: string;
13
+ }
14
+
15
+ export interface AuthTokens {
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ expiresAt: number;
19
+ }
20
+
21
+ export interface AuthState {
22
+ user: User | null;
23
+ tokens: AuthTokens | null;
24
+ isAuthenticated: boolean;
25
+ isLoading: boolean;
26
+ }
27
+
28
+ // API DTOs
29
+ export interface LoginRequest {
30
+ email: string;
31
+ password: string;
32
+ }
33
+
34
+ export interface LoginResponse {
35
+ user: User;
36
+ accessToken: string;
37
+ refreshToken: string;
38
+ expiresIn: number;
39
+ }
40
+
41
+ export interface RegisterRequest {
42
+ name: string;
43
+ email: string;
44
+ password: string;
45
+ }
46
+
47
+ export interface RegisterResponse extends LoginResponse {}
48
+
49
+ export interface RefreshTokenRequest {
50
+ refreshToken: string;
51
+ }
52
+
53
+ export interface RefreshTokenResponse {
54
+ accessToken: string;
55
+ refreshToken: string;
56
+ expiresIn: number;
57
+ }
58
+
59
+ export interface UpdateUserRequest {
60
+ name?: string;
61
+ email?: string;
62
+ avatar?: string;
63
+ }