ignite-parse-auth-kit 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 (241) hide show
  1. package/CONTRIBUTING.md +0 -0
  2. package/LICENSE +21 -0
  3. package/README.md +492 -0
  4. package/app/app.tsx +116 -0
  5. package/app/components/AlertTongle.tsx +105 -0
  6. package/app/components/AutoImage.tsx +89 -0
  7. package/app/components/Button.tsx +248 -0
  8. package/app/components/Card.tsx +314 -0
  9. package/app/components/EmptyState.tsx +248 -0
  10. package/app/components/Header.tsx +332 -0
  11. package/app/components/Icon.tsx +140 -0
  12. package/app/components/ListItem.tsx +243 -0
  13. package/app/components/ListView.tsx +42 -0
  14. package/app/components/Screen.tsx +305 -0
  15. package/app/components/Text.test.tsx +23 -0
  16. package/app/components/Text.tsx +116 -0
  17. package/app/components/TextField.tsx +292 -0
  18. package/app/components/Toggle/Checkbox.tsx +123 -0
  19. package/app/components/Toggle/Radio.tsx +106 -0
  20. package/app/components/Toggle/Switch.tsx +264 -0
  21. package/app/components/Toggle/Toggle.tsx +285 -0
  22. package/app/components/index copy.ts +15 -0
  23. package/app/components/index.ts +18 -0
  24. package/app/config/config.base.ts +26 -0
  25. package/app/config/config.dev.ts +10 -0
  26. package/app/config/config.prod.ts +10 -0
  27. package/app/config/index.ts +28 -0
  28. package/app/context/AuthContext.tsx +14 -0
  29. package/app/context/EpisodeContext.tsx +136 -0
  30. package/app/context/auth/AuthProvider.tsx +340 -0
  31. package/app/context/auth/hooks.ts +29 -0
  32. package/app/context/auth/index.ts +38 -0
  33. package/app/context/auth/reducer.ts +68 -0
  34. package/app/context/auth/services.ts +394 -0
  35. package/app/context/auth/types.ts +99 -0
  36. package/app/context/auth/validation.ts +45 -0
  37. package/app/devtools/ReactotronClient.ts +9 -0
  38. package/app/devtools/ReactotronClient.web.ts +12 -0
  39. package/app/devtools/ReactotronConfig.ts +139 -0
  40. package/app/i18n/ar.ts +126 -0
  41. package/app/i18n/demo-ar.ts +464 -0
  42. package/app/i18n/demo-en.ts +462 -0
  43. package/app/i18n/demo-es.ts +469 -0
  44. package/app/i18n/demo-fr.ts +471 -0
  45. package/app/i18n/demo-hi.ts +468 -0
  46. package/app/i18n/demo-ja.ts +464 -0
  47. package/app/i18n/demo-ko.ts +457 -0
  48. package/app/i18n/en.ts +146 -0
  49. package/app/i18n/es.ts +132 -0
  50. package/app/i18n/fr.ts +132 -0
  51. package/app/i18n/hi.ts +131 -0
  52. package/app/i18n/index.ts +86 -0
  53. package/app/i18n/ja.ts +130 -0
  54. package/app/i18n/ko.ts +129 -0
  55. package/app/i18n/translate.ts +33 -0
  56. package/app/lib/Parse/index.ts +2 -0
  57. package/app/lib/Parse/parse.ts +62 -0
  58. package/app/navigators/AppNavigator.tsx +145 -0
  59. package/app/navigators/DemoNavigator.tsx +137 -0
  60. package/app/navigators/navigationUtilities.ts +208 -0
  61. package/app/screens/ChooseAuthScreen.tsx +224 -0
  62. package/app/screens/DemoCommunityScreen.tsx +141 -0
  63. package/app/screens/DemoDebugScreen.tsx +192 -0
  64. package/app/screens/DemoPodcastListScreen.tsx +387 -0
  65. package/app/screens/DemoShowroomScreen/DemoDivider.tsx +66 -0
  66. package/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx +313 -0
  67. package/app/screens/DemoShowroomScreen/DemoUseCase.tsx +52 -0
  68. package/app/screens/DemoShowroomScreen/DrawerIconButton.tsx +120 -0
  69. package/app/screens/DemoShowroomScreen/SectionListWithKeyboardAwareScrollView.tsx +59 -0
  70. package/app/screens/DemoShowroomScreen/demos/DemoAutoImage.tsx +230 -0
  71. package/app/screens/DemoShowroomScreen/demos/DemoButton.tsx +234 -0
  72. package/app/screens/DemoShowroomScreen/demos/DemoCard.tsx +181 -0
  73. package/app/screens/DemoShowroomScreen/demos/DemoEmptyState.tsx +78 -0
  74. package/app/screens/DemoShowroomScreen/demos/DemoHeader.tsx +151 -0
  75. package/app/screens/DemoShowroomScreen/demos/DemoIcon.tsx +115 -0
  76. package/app/screens/DemoShowroomScreen/demos/DemoListItem.tsx +218 -0
  77. package/app/screens/DemoShowroomScreen/demos/DemoText.tsx +144 -0
  78. package/app/screens/DemoShowroomScreen/demos/DemoTextField.tsx +233 -0
  79. package/app/screens/DemoShowroomScreen/demos/DemoToggle.tsx +354 -0
  80. package/app/screens/DemoShowroomScreen/demos/index.ts +12 -0
  81. package/app/screens/ErrorScreen/ErrorBoundary.tsx +76 -0
  82. package/app/screens/ErrorScreen/ErrorDetails.tsx +98 -0
  83. package/app/screens/ForgetPasswordScreen.tsx +180 -0
  84. package/app/screens/LoginScreen.tsx +260 -0
  85. package/app/screens/RegisterScreen.tsx +395 -0
  86. package/app/screens/WelcomeScreen.tsx +114 -0
  87. package/app/services/api/apiProblem.test.ts +73 -0
  88. package/app/services/api/apiProblem.ts +74 -0
  89. package/app/services/api/index.ts +91 -0
  90. package/app/services/api/types.ts +50 -0
  91. package/app/theme/colors.ts +85 -0
  92. package/app/theme/colorsDark.ts +50 -0
  93. package/app/theme/context.tsx +145 -0
  94. package/app/theme/context.utils.ts +25 -0
  95. package/app/theme/spacing.ts +14 -0
  96. package/app/theme/spacingDark.ts +14 -0
  97. package/app/theme/styles.ts +24 -0
  98. package/app/theme/theme.ts +23 -0
  99. package/app/theme/timing.ts +6 -0
  100. package/app/theme/types.ts +64 -0
  101. package/app/theme/typography.ts +71 -0
  102. package/app/utils/crashReporting.ts +62 -0
  103. package/app/utils/delay.ts +6 -0
  104. package/app/utils/formatDate.ts +49 -0
  105. package/app/utils/gestureHandler.native.ts +3 -0
  106. package/app/utils/gestureHandler.ts +6 -0
  107. package/app/utils/hasValidStringProp.ts +11 -0
  108. package/app/utils/openLinkInBrowser.ts +8 -0
  109. package/app/utils/storage/index.ts +82 -0
  110. package/app/utils/storage/storage.test.ts +61 -0
  111. package/app/utils/useHeader.tsx +37 -0
  112. package/app/utils/useIsMounted.ts +18 -0
  113. package/app/utils/useSafeAreaInsetsStyle.ts +46 -0
  114. package/app.config.ts +39 -0
  115. package/app.json +67 -0
  116. package/assets/icons/back.png +0 -0
  117. package/assets/icons/back@2x.png +0 -0
  118. package/assets/icons/back@3x.png +0 -0
  119. package/assets/icons/bell.png +0 -0
  120. package/assets/icons/bell@2x.png +0 -0
  121. package/assets/icons/bell@3x.png +0 -0
  122. package/assets/icons/caretLeft.png +0 -0
  123. package/assets/icons/caretLeft@2x.png +0 -0
  124. package/assets/icons/caretLeft@3x.png +0 -0
  125. package/assets/icons/caretRight.png +0 -0
  126. package/assets/icons/caretRight@2x.png +0 -0
  127. package/assets/icons/caretRight@3x.png +0 -0
  128. package/assets/icons/check.png +0 -0
  129. package/assets/icons/check@2x.png +0 -0
  130. package/assets/icons/check@3x.png +0 -0
  131. package/assets/icons/demo/clap.png +0 -0
  132. package/assets/icons/demo/clap@2x.png +0 -0
  133. package/assets/icons/demo/clap@3x.png +0 -0
  134. package/assets/icons/demo/community.png +0 -0
  135. package/assets/icons/demo/community@2x.png +0 -0
  136. package/assets/icons/demo/community@3x.png +0 -0
  137. package/assets/icons/demo/components.png +0 -0
  138. package/assets/icons/demo/components@2x.png +0 -0
  139. package/assets/icons/demo/components@3x.png +0 -0
  140. package/assets/icons/demo/debug.png +0 -0
  141. package/assets/icons/demo/debug@2x.png +0 -0
  142. package/assets/icons/demo/debug@3x.png +0 -0
  143. package/assets/icons/demo/github.png +0 -0
  144. package/assets/icons/demo/github@2x.png +0 -0
  145. package/assets/icons/demo/github@3x.png +0 -0
  146. package/assets/icons/demo/heart.png +0 -0
  147. package/assets/icons/demo/heart@2x.png +0 -0
  148. package/assets/icons/demo/heart@3x.png +0 -0
  149. package/assets/icons/demo/pin.png +0 -0
  150. package/assets/icons/demo/pin@2x.png +0 -0
  151. package/assets/icons/demo/pin@3x.png +0 -0
  152. package/assets/icons/demo/podcast.png +0 -0
  153. package/assets/icons/demo/podcast@2x.png +0 -0
  154. package/assets/icons/demo/podcast@3x.png +0 -0
  155. package/assets/icons/demo/slack.png +0 -0
  156. package/assets/icons/demo/slack@2x.png +0 -0
  157. package/assets/icons/demo/slack@3x.png +0 -0
  158. package/assets/icons/google.png +0 -0
  159. package/assets/icons/hidden.png +0 -0
  160. package/assets/icons/hidden@2x.png +0 -0
  161. package/assets/icons/hidden@3x.png +0 -0
  162. package/assets/icons/ladybug.png +0 -0
  163. package/assets/icons/ladybug@2x.png +0 -0
  164. package/assets/icons/ladybug@3x.png +0 -0
  165. package/assets/icons/lock.png +0 -0
  166. package/assets/icons/lock@2x.png +0 -0
  167. package/assets/icons/lock@3x.png +0 -0
  168. package/assets/icons/menu.png +0 -0
  169. package/assets/icons/menu@2x.png +0 -0
  170. package/assets/icons/menu@3x.png +0 -0
  171. package/assets/icons/more.png +0 -0
  172. package/assets/icons/more@2x.png +0 -0
  173. package/assets/icons/more@3x.png +0 -0
  174. package/assets/icons/settings.png +0 -0
  175. package/assets/icons/settings@2x.png +0 -0
  176. package/assets/icons/settings@3x.png +0 -0
  177. package/assets/icons/view.png +0 -0
  178. package/assets/icons/view@2x.png +0 -0
  179. package/assets/icons/view@3x.png +0 -0
  180. package/assets/icons/x.png +0 -0
  181. package/assets/icons/x@2x.png +0 -0
  182. package/assets/icons/x@3x.png +0 -0
  183. package/assets/images/app-icon-all.png +0 -0
  184. package/assets/images/app-icon-android-adaptive-background.png +0 -0
  185. package/assets/images/app-icon-android-adaptive-foreground.png +0 -0
  186. package/assets/images/app-icon-android-legacy.png +0 -0
  187. package/assets/images/app-icon-ios.png +0 -0
  188. package/assets/images/app-icon-web-favicon.png +0 -0
  189. package/assets/images/demo/cr-logo.png +0 -0
  190. package/assets/images/demo/cr-logo@2x.png +0 -0
  191. package/assets/images/demo/cr-logo@3x.png +0 -0
  192. package/assets/images/demo/rnl-logo.png +0 -0
  193. package/assets/images/demo/rnl-logo@2x.png +0 -0
  194. package/assets/images/demo/rnl-logo@3x.png +0 -0
  195. package/assets/images/demo/rnn-logo.png +0 -0
  196. package/assets/images/demo/rnn-logo@2x.png +0 -0
  197. package/assets/images/demo/rnn-logo@3x.png +0 -0
  198. package/assets/images/demo/rnr-image-1.png +0 -0
  199. package/assets/images/demo/rnr-image-1@2x.png +0 -0
  200. package/assets/images/demo/rnr-image-1@3x.png +0 -0
  201. package/assets/images/demo/rnr-image-2.png +0 -0
  202. package/assets/images/demo/rnr-image-2@2x.png +0 -0
  203. package/assets/images/demo/rnr-image-2@3x.png +0 -0
  204. package/assets/images/demo/rnr-image-3.png +0 -0
  205. package/assets/images/demo/rnr-image-3@2x.png +0 -0
  206. package/assets/images/demo/rnr-image-3@3x.png +0 -0
  207. package/assets/images/demo/rnr-logo.png +0 -0
  208. package/assets/images/demo/rnr-logo@2x.png +0 -0
  209. package/assets/images/demo/rnr-logo@3x.png +0 -0
  210. package/assets/images/logo.png +0 -0
  211. package/assets/images/logo@2x.png +0 -0
  212. package/assets/images/logo@3x.png +0 -0
  213. package/assets/images/sad-face.png +0 -0
  214. package/assets/images/sad-face@2x.png +0 -0
  215. package/assets/images/sad-face@3x.png +0 -0
  216. package/assets/images/welcome-face.png +0 -0
  217. package/assets/images/welcome-face@2x.png +0 -0
  218. package/assets/images/welcome-face@3x.png +0 -0
  219. package/babel.config.js +7 -0
  220. package/bin/cli.js +196 -0
  221. package/ignite/templates/app-icon/android-adaptive-background.png +0 -0
  222. package/ignite/templates/app-icon/android-adaptive-foreground.png +0 -0
  223. package/ignite/templates/app-icon/android-legacy.png +0 -0
  224. package/ignite/templates/app-icon/ios-universal.png +0 -0
  225. package/ignite/templates/component/NAME.tsx.ejs +39 -0
  226. package/ignite/templates/navigator/NAMENavigator.tsx.ejs +18 -0
  227. package/ignite/templates/screen/NAMEScreen.tsx.ejs +29 -0
  228. package/ignite/templates/splash-screen/logo.png +0 -0
  229. package/index.tsx +9 -0
  230. package/jest.config.js +5 -0
  231. package/metro.config.js +31 -0
  232. package/package.json +166 -0
  233. package/plugins/withSplashScreen.ts +69 -0
  234. package/src/app/_layout.tsx +58 -0
  235. package/src/app/index.tsx +5 -0
  236. package/test/i18n.test.ts +75 -0
  237. package/test/mockFile.ts +6 -0
  238. package/test/setup.ts +58 -0
  239. package/test/test-tsconfig.json +8 -0
  240. package/tsconfig.json +52 -0
  241. package/types/lib.es5.d.ts +25 -0
@@ -0,0 +1,136 @@
1
+ import {
2
+ createContext,
3
+ FC,
4
+ PropsWithChildren,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useState,
9
+ } from "react"
10
+
11
+ import { translate } from "@/i18n/translate"
12
+ import { api } from "@/services/api"
13
+ import type { EpisodeItem } from "@/services/api/types"
14
+ import { formatDate } from "@/utils/formatDate"
15
+
16
+ export type EpisodeContextType = {
17
+ totalEpisodes: number
18
+ totalFavorites: number
19
+ episodesForList: EpisodeItem[]
20
+ fetchEpisodes: () => Promise<void>
21
+ favoritesOnly: boolean
22
+ toggleFavoritesOnly: () => void
23
+ hasFavorite: (episode: EpisodeItem) => boolean
24
+ toggleFavorite: (episode: EpisodeItem) => void
25
+ }
26
+
27
+ export const EpisodeContext = createContext<EpisodeContextType | null>(null)
28
+
29
+ export interface EpisodeProviderProps {}
30
+
31
+ export const EpisodeProvider: FC<PropsWithChildren<EpisodeProviderProps>> = ({ children }) => {
32
+ const [episodes, setEpisodes] = useState<EpisodeItem[]>([])
33
+ const [favorites, setFavorites] = useState<string[]>([])
34
+ const [favoritesOnly, setFavoritesOnly] = useState<boolean>(false)
35
+
36
+ const fetchEpisodes = useCallback(async () => {
37
+ const response = await api.getEpisodes()
38
+ if (response.kind === "ok") {
39
+ setEpisodes(response.episodes)
40
+ } else {
41
+ console.error(`Error fetching episodes: ${JSON.stringify(response)}`)
42
+ }
43
+ }, [])
44
+
45
+ const toggleFavoritesOnly = useCallback(() => {
46
+ setFavoritesOnly((prev) => !prev)
47
+ }, [])
48
+
49
+ const toggleFavorite = useCallback(
50
+ (episode: EpisodeItem) => {
51
+ if (favorites.some((fav) => fav === episode.guid)) {
52
+ setFavorites((prev) => prev.filter((fav) => fav !== episode.guid))
53
+ } else {
54
+ setFavorites((prev) => [...prev, episode.guid])
55
+ }
56
+ },
57
+ [favorites],
58
+ )
59
+
60
+ const hasFavorite = useCallback(
61
+ (episode: EpisodeItem) => favorites.some((fav) => fav === episode.guid),
62
+ [favorites],
63
+ )
64
+
65
+ const episodesForList = useMemo(() => {
66
+ return favoritesOnly ? episodes.filter((episode) => favorites.includes(episode.guid)) : episodes
67
+ }, [episodes, favorites, favoritesOnly])
68
+
69
+ const value = {
70
+ totalEpisodes: episodes.length,
71
+ totalFavorites: favorites.length,
72
+ episodesForList,
73
+ fetchEpisodes,
74
+ favoritesOnly,
75
+ toggleFavoritesOnly,
76
+ hasFavorite,
77
+ toggleFavorite,
78
+ }
79
+
80
+ return <EpisodeContext.Provider value={value}>{children}</EpisodeContext.Provider>
81
+ }
82
+
83
+ export const useEpisodes = () => {
84
+ const context = useContext(EpisodeContext)
85
+ if (!context) throw new Error("useEpisodes must be used within an EpisodeProvider")
86
+ return context
87
+ }
88
+
89
+ // A helper hook to extract and format episode details
90
+ export const useEpisode = (episode: EpisodeItem) => {
91
+ const { hasFavorite } = useEpisodes()
92
+
93
+ const isFavorite = hasFavorite(episode)
94
+
95
+ let datePublished
96
+ try {
97
+ const formatted = formatDate(episode.pubDate)
98
+ datePublished = {
99
+ textLabel: formatted,
100
+ accessibilityLabel: translate("demoPodcastListScreen:accessibility.publishLabel", {
101
+ date: formatted,
102
+ }),
103
+ }
104
+ } catch {
105
+ datePublished = { textLabel: "", accessibilityLabel: "" }
106
+ }
107
+
108
+ const seconds = Number(episode.enclosure?.duration ?? 0)
109
+ const h = Math.floor(seconds / 3600)
110
+ const m = Math.floor((seconds % 3600) / 60)
111
+ const s = Math.floor((seconds % 3600) % 60)
112
+ const duration = {
113
+ textLabel: `${h > 0 ? `${h}:` : ""}${m > 0 ? `${m}:` : ""}${s}`,
114
+ accessibilityLabel: translate("demoPodcastListScreen:accessibility.durationLabel", {
115
+ hours: h,
116
+ minutes: m,
117
+ seconds: s,
118
+ }),
119
+ }
120
+
121
+ const trimmedTitle = episode.title?.trim()
122
+ const titleMatches = trimmedTitle?.match(/^(RNR.*\d)(?: - )(.*$)/)
123
+ const parsedTitleAndSubtitle =
124
+ titleMatches && titleMatches.length === 3
125
+ ? { title: titleMatches[1], subtitle: titleMatches[2] }
126
+ : { title: trimmedTitle, subtitle: "" }
127
+
128
+ return {
129
+ isFavorite,
130
+ datePublished,
131
+ duration,
132
+ parsedTitleAndSubtitle,
133
+ }
134
+ }
135
+
136
+ // @demo remove-file
@@ -0,0 +1,340 @@
1
+ import React, {
2
+ createContext,
3
+ FC,
4
+ PropsWithChildren,
5
+ useCallback,
6
+ useEffect,
7
+ useReducer,
8
+ } from "react";
9
+ import { useMMKVString, useMMKVObject } from "react-native-mmkv";
10
+ import Parse from "@/lib/Parse";
11
+ import type {
12
+ AuthContextType,
13
+ AuthProviderProps,
14
+ PersistedUserData,
15
+ GoogleAuthResponse,
16
+ } from "./types";
17
+ import { authReducer, initialAuthState } from "./reducer";
18
+ import {
19
+ loginUser,
20
+ signUpUser,
21
+ signInWithGoogle,
22
+ requestPasswordReset as requestPasswordResetService,
23
+ logoutUser,
24
+ getCurrentUser,
25
+ restoreUserSession,
26
+ checkParseServerConnection,
27
+ } from "./services";
28
+ import { validateEmail, validatePassword, validateUsername } from "./validation";
29
+
30
+ /**
31
+ * AuthContext
32
+ * - Centralizes authentication state and actions for the app using Parse.
33
+ * - Persists a minimal user snapshot and session token in MMKV for fast restore.
34
+ * - Exposes typed helpers for login, signup, Google OAuth, logout, password reset, and status checks.
35
+ */
36
+
37
+ export const AuthContext = createContext<AuthContextType | null>(null);
38
+
39
+ export const AuthProvider: FC<PropsWithChildren<AuthProviderProps>> = ({
40
+ children,
41
+ }) => {
42
+ const [state, dispatch] = useReducer(authReducer, initialAuthState);
43
+
44
+ // Persisted storage (fast restore across app restarts)
45
+ const [persistedAuthToken, setPersistedAuthToken] = useMMKVString(
46
+ "AuthProvider.authToken"
47
+ );
48
+ const [persistedUserData, setPersistedUserData] = useMMKVObject<PersistedUserData>(
49
+ "AuthProvider.userData"
50
+ );
51
+
52
+ // On boot: try to restore a valid session from persisted data
53
+ useEffect(() => {
54
+ const initializeAuth = async () => {
55
+ if (persistedAuthToken && persistedUserData) {
56
+ try {
57
+ // 1) Prefer the current user already held by Parse if the token matches
58
+ const currentUser = await Parse.User.currentAsync();
59
+ if (currentUser && currentUser.getSessionToken() === persistedAuthToken) {
60
+ dispatch({ type: "SET_CURRENT_USER", payload: currentUser });
61
+ return;
62
+ }
63
+ } catch (error) {
64
+ console.warn("Could not get current user from Parse:", error);
65
+ }
66
+
67
+ // 2) Fallback: try to restore session from token using Parse API
68
+ try {
69
+ const user = await restoreUserSession(persistedAuthToken);
70
+ if (user) {
71
+ dispatch({
72
+ type: "SET_AUTH_TOKEN",
73
+ payload: { token: persistedAuthToken },
74
+ });
75
+ dispatch({ type: "SET_CURRENT_USER", payload: user });
76
+ } else {
77
+ // Clear invalid persisted data to avoid loops
78
+ setPersistedAuthToken(undefined);
79
+ setPersistedUserData(undefined);
80
+ }
81
+ } catch (error) {
82
+ console.error("Failed to restore user session:", error);
83
+ // Clear invalid persisted data to avoid loops
84
+ setPersistedAuthToken(undefined);
85
+ setPersistedUserData(undefined);
86
+ }
87
+ }
88
+ };
89
+
90
+ initializeAuth();
91
+ }, [persistedAuthToken, persistedUserData, setPersistedAuthToken, setPersistedUserData]);
92
+
93
+ // Lightweight action wrappers
94
+ const setAuthEmail = useCallback((email: string) => {
95
+ dispatch({ type: "SET_AUTH_EMAIL", payload: email });
96
+ }, []);
97
+
98
+ const setAuthPassword = useCallback((password: string) => {
99
+ dispatch({ type: "SET_AUTH_PASSWORD", payload: password });
100
+ }, []);
101
+
102
+ const setError = useCallback((error: string) => {
103
+ dispatch({ type: "SET_ERROR", payload: error });
104
+ }, []);
105
+
106
+ const setAuthToken = useCallback(
107
+ (token?: string) => {
108
+ dispatch({ type: "SET_AUTH_TOKEN", payload: { token } });
109
+ setPersistedAuthToken(token);
110
+ },
111
+ [setPersistedAuthToken]
112
+ );
113
+
114
+ const clearResetMessage = useCallback(() => {
115
+ dispatch({ type: "CLEAR_RESET_MESSAGE" });
116
+ }, []);
117
+
118
+ // Single place to persist both token and a minimal user snapshot
119
+ const persistUserData = useCallback(
120
+ (user: Parse.User) => {
121
+ const userData: PersistedUserData = {
122
+ objectId: user.id,
123
+ sessionToken: user.getSessionToken(),
124
+ username: user.getUsername() || "",
125
+ email: user.getEmail() || "",
126
+ };
127
+ setPersistedAuthToken(user.getSessionToken());
128
+ setPersistedUserData(userData as any);
129
+ },
130
+ [setPersistedAuthToken, setPersistedUserData]
131
+ );
132
+
133
+ const resetAuthState = useCallback(() => {
134
+ dispatch({ type: "RESET_AUTH_STATE" });
135
+ }, []);
136
+
137
+ const clearForm = useCallback(() => {
138
+ dispatch({ type: "CLEAR_FORM" });
139
+ }, []);
140
+
141
+ const login = useCallback(async (): Promise<{
142
+ success: boolean;
143
+ error?: string;
144
+ }> => {
145
+ dispatch({ type: "SET_LOADING", payload: true });
146
+ dispatch({ type: "SET_ERROR", payload: "" });
147
+
148
+ try {
149
+ const result = await loginUser(state.authEmail, state.authPassword);
150
+
151
+ if (result.success && result.user) {
152
+ dispatch({ type: "SET_CURRENT_USER", payload: result.user });
153
+ persistUserData(result.user);
154
+ dispatch({ type: "CLEAR_FORM" }); // clear form fields only
155
+ } else if (result.error) {
156
+ dispatch({ type: "SET_ERROR", payload: result.error });
157
+ }
158
+
159
+ return result;
160
+ } finally {
161
+ dispatch({ type: "SET_LOADING", payload: false });
162
+ }
163
+ }, [state.authEmail, state.authPassword, persistUserData]);
164
+
165
+ const signUp = useCallback(
166
+ async (username: string): Promise<{ success: boolean; error?: string }> => {
167
+ dispatch({ type: "SET_LOADING", payload: true });
168
+ dispatch({ type: "SET_ERROR", payload: "" });
169
+
170
+ try {
171
+ const result = await signUpUser(username, state.authEmail, state.authPassword);
172
+
173
+ if (result.success && result.user) {
174
+ dispatch({ type: "SET_CURRENT_USER", payload: result.user });
175
+ persistUserData(result.user);
176
+ dispatch({ type: "CLEAR_FORM" }); // clear form fields only
177
+ } else if (result.error) {
178
+ dispatch({ type: "SET_ERROR", payload: result.error });
179
+ }
180
+
181
+ return result;
182
+ } finally {
183
+ dispatch({ type: "SET_LOADING", payload: false });
184
+ }
185
+ },
186
+ [state.authEmail, state.authPassword, persistUserData]
187
+ );
188
+
189
+ const googleSignIn = useCallback(
190
+ async (
191
+ response: GoogleAuthResponse
192
+ ): Promise<{ success: boolean; error?: string; user?: Parse.User }> => {
193
+ dispatch({ type: "SET_LOADING", payload: true });
194
+ dispatch({ type: "SET_ERROR", payload: "" });
195
+
196
+ try {
197
+ const result = await signInWithGoogle(response);
198
+
199
+ if (result.success && result.user) {
200
+ dispatch({ type: "SET_CURRENT_USER", payload: result.user });
201
+ persistUserData(result.user);
202
+ dispatch({ type: "CLEAR_FORM" });
203
+ } else if (result.error) {
204
+ dispatch({ type: "SET_ERROR", payload: result.error });
205
+ }
206
+
207
+ return result;
208
+ } finally {
209
+ dispatch({ type: "SET_LOADING", payload: false });
210
+ }
211
+ },
212
+ [persistUserData]
213
+ );
214
+
215
+ const requestPasswordResetAction = useCallback(
216
+ async (email: string): Promise<{ success: boolean; error?: string; message?: string }> => {
217
+ dispatch({ type: "SET_LOADING", payload: true });
218
+ dispatch({ type: "SET_ERROR", payload: "" });
219
+ dispatch({ type: "CLEAR_RESET_MESSAGE" });
220
+
221
+ try {
222
+ const result = await requestPasswordResetService(email);
223
+
224
+ if (result.success && result.message) {
225
+ dispatch({ type: "SET_RESET_PASSWORD_MESSAGE", payload: result.message });
226
+ } else if (result.error) {
227
+ dispatch({ type: "SET_ERROR", payload: result.error });
228
+ }
229
+
230
+ return result;
231
+ } finally {
232
+ dispatch({ type: "SET_LOADING", payload: false });
233
+ }
234
+ },
235
+ []
236
+ );
237
+
238
+ const logout = useCallback(async (): Promise<{
239
+ success: boolean;
240
+ error?: string;
241
+ }> => {
242
+ try {
243
+ // Clear persisted data first
244
+ setPersistedAuthToken(undefined);
245
+ setPersistedUserData(undefined);
246
+
247
+ // Reset all in-memory auth state
248
+ dispatch({ type: "RESET_AUTH_STATE" });
249
+
250
+ // Then attempt Parse logout
251
+ const result = await logoutUser();
252
+
253
+ if (result.error) {
254
+ dispatch({ type: "SET_ERROR", payload: result.error });
255
+ }
256
+
257
+ return result;
258
+ } catch (error) {
259
+ const errorMessage =
260
+ error instanceof Error ? error.message : "Failed to log out";
261
+ console.error("Logout error:", errorMessage);
262
+ dispatch({ type: "SET_ERROR", payload: errorMessage });
263
+ return { success: false, error: errorMessage };
264
+ }
265
+ }, [setPersistedAuthToken, setPersistedUserData]);
266
+
267
+ const checkCurrentUser = useCallback(async (): Promise<boolean> => {
268
+ try {
269
+ const currentUser = await getCurrentUser();
270
+ if (currentUser) {
271
+ dispatch({ type: "SET_CURRENT_USER", payload: currentUser });
272
+ persistUserData(currentUser);
273
+ return true;
274
+ }
275
+ return false;
276
+ } catch (error) {
277
+ const errorMessage =
278
+ error instanceof Error ? error.message : "Unknown error";
279
+ console.error("Error checking current user:", errorMessage);
280
+ return false;
281
+ }
282
+ }, [persistUserData]);
283
+
284
+ const checkServerStatus = useCallback(async (): Promise<{
285
+ isRunning: boolean;
286
+ message: string;
287
+ }> => {
288
+ try {
289
+ const isRunning = await checkParseServerConnection();
290
+ return {
291
+ isRunning,
292
+ message: isRunning ? "Server is running" : "Server is not accessible",
293
+ };
294
+ } catch (error) {
295
+ return { isRunning: false, message: "Failed to check server status" };
296
+ }
297
+ }, []);
298
+
299
+ // On mount: re-sync the current user from Parse if available
300
+ useEffect(() => {
301
+ checkCurrentUser();
302
+ }, [checkCurrentUser]);
303
+
304
+ // Memo-like object literal passed to consumers
305
+ const value: AuthContextType = {
306
+ // State
307
+ isAuthenticated: !!state.currentUser || !!state.authToken,
308
+ authToken: state.authToken,
309
+ authEmail: state.authEmail,
310
+ authPassword: state.authPassword,
311
+ isLoading: state.isLoading,
312
+ error: state.error,
313
+ currentUser: state.currentUser,
314
+ username: state.username,
315
+ resetPasswordMessage: state.resetPasswordMessage,
316
+
317
+ // Actions
318
+ setAuthEmail,
319
+ setAuthPassword,
320
+ setError,
321
+ setAuthToken,
322
+ resetAuthState,
323
+ clearForm,
324
+ clearResetMessage,
325
+ login,
326
+ signUp,
327
+ googleSignIn,
328
+ requestPasswordReset: requestPasswordResetAction,
329
+ logout,
330
+ checkCurrentUser,
331
+ checkServerStatus,
332
+
333
+ // Validation helpers
334
+ validateEmail,
335
+ validatePassword,
336
+ validateUsername,
337
+ };
338
+
339
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
340
+ };
@@ -0,0 +1,29 @@
1
+ import { useContext } from "react";
2
+ import { AuthContext } from "./AuthProvider";
3
+ import { getCurrentUser as getUser, fetchAllUsers } from "./services";
4
+
5
+ /**
6
+ * Custom hooks for authentication
7
+ */
8
+
9
+ /**
10
+ * Hook to access auth context
11
+ * Must be used within an AuthProvider
12
+ */
13
+ export const useAuth = () => {
14
+ const context = useContext(AuthContext);
15
+ if (!context) throw new Error("useAuth must be used within an AuthProvider");
16
+ return context;
17
+ };
18
+
19
+ /**
20
+ * Convenience helper to get current user
21
+ * Can be used outside of React components
22
+ */
23
+ export const getCurrentUser = getUser;
24
+
25
+ /**
26
+ * Convenience helper to fetch all users
27
+ * Can be used outside of React components
28
+ */
29
+ export const fetchAllUsersHelper = fetchAllUsers;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Auth module exports
3
+ * Central export point for all authentication functionality
4
+ */
5
+
6
+ // Types
7
+ export type {
8
+ IAppUser,
9
+ GoogleAuthResponse,
10
+ AuthState,
11
+ AuthAction,
12
+ AuthContextType,
13
+ AuthProviderProps,
14
+ PersistedUserData,
15
+ } from "./types";
16
+
17
+ // Provider and Context
18
+ export { AuthProvider, AuthContext } from "./AuthProvider";
19
+
20
+ // Hooks
21
+ export { useAuth, getCurrentUser, fetchAllUsersHelper as fetchAllUsers } from "./hooks";
22
+
23
+ // Validation utilities
24
+ export { validateEmail, validatePassword, validateUsername } from "./validation";
25
+
26
+ // Services (for advanced usage)
27
+ export {
28
+ checkParseServerConnection,
29
+ loginUser,
30
+ signUpUser,
31
+ signInWithGoogle,
32
+ requestPasswordReset,
33
+ logoutUser,
34
+ restoreUserSession,
35
+ } from "./services";
36
+
37
+ // Reducer (for advanced usage)
38
+ export { authReducer, initialAuthState } from "./reducer";
@@ -0,0 +1,68 @@
1
+ import type { AuthState, AuthAction } from "./types";
2
+
3
+ /**
4
+ * Auth reducer
5
+ * Handles all auth state transitions in one place
6
+ */
7
+
8
+ // Initial value for the reducer
9
+ export const initialAuthState: AuthState = {
10
+ authEmail: "",
11
+ authPassword: "",
12
+ isLoading: false,
13
+ error: "",
14
+ currentUser: undefined,
15
+ username: undefined,
16
+ resetPasswordMessage: undefined,
17
+ };
18
+
19
+ export const authReducer = (state: AuthState, action: AuthAction): AuthState => {
20
+ switch (action.type) {
21
+ case "SET_AUTH_EMAIL":
22
+ return { ...state, authEmail: action.payload.replace(/\s+/g, "") };
23
+ case "SET_AUTH_PASSWORD":
24
+ return { ...state, authPassword: action.payload };
25
+ case "SET_ERROR":
26
+ return { ...state, error: action.payload };
27
+ case "SET_AUTH_TOKEN":
28
+ return { ...state, authToken: action.payload.token };
29
+ case "SET_LOADING":
30
+ return { ...state, isLoading: action.payload };
31
+ case "SET_CURRENT_USER":
32
+ return {
33
+ ...state,
34
+ currentUser: action.payload,
35
+ authToken: action.payload?.getSessionToken(),
36
+ username: action.payload?.getUsername(),
37
+ };
38
+ case "SET_RESET_PASSWORD_MESSAGE":
39
+ return { ...state, resetPasswordMessage: action.payload };
40
+ case "CLEAR_RESET_MESSAGE":
41
+ return { ...state, resetPasswordMessage: undefined };
42
+ case "RESET_AUTH_STATE":
43
+ // Full reset (used for logout)
44
+ return {
45
+ ...state,
46
+ authEmail: "",
47
+ authPassword: "",
48
+ error: "",
49
+ isLoading: false,
50
+ currentUser: undefined,
51
+ authToken: undefined,
52
+ username: undefined,
53
+ resetPasswordMessage: undefined,
54
+ };
55
+ case "CLEAR_FORM":
56
+ // Clear only form fields; keep the authenticated user
57
+ return {
58
+ ...state,
59
+ authEmail: "",
60
+ authPassword: "",
61
+ error: "",
62
+ isLoading: false,
63
+ resetPasswordMessage: undefined,
64
+ };
65
+ default:
66
+ return state;
67
+ }
68
+ };