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.
- package/CONTRIBUTING.md +0 -0
- package/LICENSE +21 -0
- package/README.md +492 -0
- package/app/app.tsx +116 -0
- package/app/components/AlertTongle.tsx +105 -0
- package/app/components/AutoImage.tsx +89 -0
- package/app/components/Button.tsx +248 -0
- package/app/components/Card.tsx +314 -0
- package/app/components/EmptyState.tsx +248 -0
- package/app/components/Header.tsx +332 -0
- package/app/components/Icon.tsx +140 -0
- package/app/components/ListItem.tsx +243 -0
- package/app/components/ListView.tsx +42 -0
- package/app/components/Screen.tsx +305 -0
- package/app/components/Text.test.tsx +23 -0
- package/app/components/Text.tsx +116 -0
- package/app/components/TextField.tsx +292 -0
- package/app/components/Toggle/Checkbox.tsx +123 -0
- package/app/components/Toggle/Radio.tsx +106 -0
- package/app/components/Toggle/Switch.tsx +264 -0
- package/app/components/Toggle/Toggle.tsx +285 -0
- package/app/components/index copy.ts +15 -0
- package/app/components/index.ts +18 -0
- package/app/config/config.base.ts +26 -0
- package/app/config/config.dev.ts +10 -0
- package/app/config/config.prod.ts +10 -0
- package/app/config/index.ts +28 -0
- package/app/context/AuthContext.tsx +14 -0
- package/app/context/EpisodeContext.tsx +136 -0
- package/app/context/auth/AuthProvider.tsx +340 -0
- package/app/context/auth/hooks.ts +29 -0
- package/app/context/auth/index.ts +38 -0
- package/app/context/auth/reducer.ts +68 -0
- package/app/context/auth/services.ts +394 -0
- package/app/context/auth/types.ts +99 -0
- package/app/context/auth/validation.ts +45 -0
- package/app/devtools/ReactotronClient.ts +9 -0
- package/app/devtools/ReactotronClient.web.ts +12 -0
- package/app/devtools/ReactotronConfig.ts +139 -0
- package/app/i18n/ar.ts +126 -0
- package/app/i18n/demo-ar.ts +464 -0
- package/app/i18n/demo-en.ts +462 -0
- package/app/i18n/demo-es.ts +469 -0
- package/app/i18n/demo-fr.ts +471 -0
- package/app/i18n/demo-hi.ts +468 -0
- package/app/i18n/demo-ja.ts +464 -0
- package/app/i18n/demo-ko.ts +457 -0
- package/app/i18n/en.ts +146 -0
- package/app/i18n/es.ts +132 -0
- package/app/i18n/fr.ts +132 -0
- package/app/i18n/hi.ts +131 -0
- package/app/i18n/index.ts +86 -0
- package/app/i18n/ja.ts +130 -0
- package/app/i18n/ko.ts +129 -0
- package/app/i18n/translate.ts +33 -0
- package/app/lib/Parse/index.ts +2 -0
- package/app/lib/Parse/parse.ts +62 -0
- package/app/navigators/AppNavigator.tsx +145 -0
- package/app/navigators/DemoNavigator.tsx +137 -0
- package/app/navigators/navigationUtilities.ts +208 -0
- package/app/screens/ChooseAuthScreen.tsx +224 -0
- package/app/screens/DemoCommunityScreen.tsx +141 -0
- package/app/screens/DemoDebugScreen.tsx +192 -0
- package/app/screens/DemoPodcastListScreen.tsx +387 -0
- package/app/screens/DemoShowroomScreen/DemoDivider.tsx +66 -0
- package/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx +313 -0
- package/app/screens/DemoShowroomScreen/DemoUseCase.tsx +52 -0
- package/app/screens/DemoShowroomScreen/DrawerIconButton.tsx +120 -0
- package/app/screens/DemoShowroomScreen/SectionListWithKeyboardAwareScrollView.tsx +59 -0
- package/app/screens/DemoShowroomScreen/demos/DemoAutoImage.tsx +230 -0
- package/app/screens/DemoShowroomScreen/demos/DemoButton.tsx +234 -0
- package/app/screens/DemoShowroomScreen/demos/DemoCard.tsx +181 -0
- package/app/screens/DemoShowroomScreen/demos/DemoEmptyState.tsx +78 -0
- package/app/screens/DemoShowroomScreen/demos/DemoHeader.tsx +151 -0
- package/app/screens/DemoShowroomScreen/demos/DemoIcon.tsx +115 -0
- package/app/screens/DemoShowroomScreen/demos/DemoListItem.tsx +218 -0
- package/app/screens/DemoShowroomScreen/demos/DemoText.tsx +144 -0
- package/app/screens/DemoShowroomScreen/demos/DemoTextField.tsx +233 -0
- package/app/screens/DemoShowroomScreen/demos/DemoToggle.tsx +354 -0
- package/app/screens/DemoShowroomScreen/demos/index.ts +12 -0
- package/app/screens/ErrorScreen/ErrorBoundary.tsx +76 -0
- package/app/screens/ErrorScreen/ErrorDetails.tsx +98 -0
- package/app/screens/ForgetPasswordScreen.tsx +180 -0
- package/app/screens/LoginScreen.tsx +260 -0
- package/app/screens/RegisterScreen.tsx +395 -0
- package/app/screens/WelcomeScreen.tsx +114 -0
- package/app/services/api/apiProblem.test.ts +73 -0
- package/app/services/api/apiProblem.ts +74 -0
- package/app/services/api/index.ts +91 -0
- package/app/services/api/types.ts +50 -0
- package/app/theme/colors.ts +85 -0
- package/app/theme/colorsDark.ts +50 -0
- package/app/theme/context.tsx +145 -0
- package/app/theme/context.utils.ts +25 -0
- package/app/theme/spacing.ts +14 -0
- package/app/theme/spacingDark.ts +14 -0
- package/app/theme/styles.ts +24 -0
- package/app/theme/theme.ts +23 -0
- package/app/theme/timing.ts +6 -0
- package/app/theme/types.ts +64 -0
- package/app/theme/typography.ts +71 -0
- package/app/utils/crashReporting.ts +62 -0
- package/app/utils/delay.ts +6 -0
- package/app/utils/formatDate.ts +49 -0
- package/app/utils/gestureHandler.native.ts +3 -0
- package/app/utils/gestureHandler.ts +6 -0
- package/app/utils/hasValidStringProp.ts +11 -0
- package/app/utils/openLinkInBrowser.ts +8 -0
- package/app/utils/storage/index.ts +82 -0
- package/app/utils/storage/storage.test.ts +61 -0
- package/app/utils/useHeader.tsx +37 -0
- package/app/utils/useIsMounted.ts +18 -0
- package/app/utils/useSafeAreaInsetsStyle.ts +46 -0
- package/app.config.ts +39 -0
- package/app.json +67 -0
- package/assets/icons/back.png +0 -0
- package/assets/icons/back@2x.png +0 -0
- package/assets/icons/back@3x.png +0 -0
- package/assets/icons/bell.png +0 -0
- package/assets/icons/bell@2x.png +0 -0
- package/assets/icons/bell@3x.png +0 -0
- package/assets/icons/caretLeft.png +0 -0
- package/assets/icons/caretLeft@2x.png +0 -0
- package/assets/icons/caretLeft@3x.png +0 -0
- package/assets/icons/caretRight.png +0 -0
- package/assets/icons/caretRight@2x.png +0 -0
- package/assets/icons/caretRight@3x.png +0 -0
- package/assets/icons/check.png +0 -0
- package/assets/icons/check@2x.png +0 -0
- package/assets/icons/check@3x.png +0 -0
- package/assets/icons/demo/clap.png +0 -0
- package/assets/icons/demo/clap@2x.png +0 -0
- package/assets/icons/demo/clap@3x.png +0 -0
- package/assets/icons/demo/community.png +0 -0
- package/assets/icons/demo/community@2x.png +0 -0
- package/assets/icons/demo/community@3x.png +0 -0
- package/assets/icons/demo/components.png +0 -0
- package/assets/icons/demo/components@2x.png +0 -0
- package/assets/icons/demo/components@3x.png +0 -0
- package/assets/icons/demo/debug.png +0 -0
- package/assets/icons/demo/debug@2x.png +0 -0
- package/assets/icons/demo/debug@3x.png +0 -0
- package/assets/icons/demo/github.png +0 -0
- package/assets/icons/demo/github@2x.png +0 -0
- package/assets/icons/demo/github@3x.png +0 -0
- package/assets/icons/demo/heart.png +0 -0
- package/assets/icons/demo/heart@2x.png +0 -0
- package/assets/icons/demo/heart@3x.png +0 -0
- package/assets/icons/demo/pin.png +0 -0
- package/assets/icons/demo/pin@2x.png +0 -0
- package/assets/icons/demo/pin@3x.png +0 -0
- package/assets/icons/demo/podcast.png +0 -0
- package/assets/icons/demo/podcast@2x.png +0 -0
- package/assets/icons/demo/podcast@3x.png +0 -0
- package/assets/icons/demo/slack.png +0 -0
- package/assets/icons/demo/slack@2x.png +0 -0
- package/assets/icons/demo/slack@3x.png +0 -0
- package/assets/icons/google.png +0 -0
- package/assets/icons/hidden.png +0 -0
- package/assets/icons/hidden@2x.png +0 -0
- package/assets/icons/hidden@3x.png +0 -0
- package/assets/icons/ladybug.png +0 -0
- package/assets/icons/ladybug@2x.png +0 -0
- package/assets/icons/ladybug@3x.png +0 -0
- package/assets/icons/lock.png +0 -0
- package/assets/icons/lock@2x.png +0 -0
- package/assets/icons/lock@3x.png +0 -0
- package/assets/icons/menu.png +0 -0
- package/assets/icons/menu@2x.png +0 -0
- package/assets/icons/menu@3x.png +0 -0
- package/assets/icons/more.png +0 -0
- package/assets/icons/more@2x.png +0 -0
- package/assets/icons/more@3x.png +0 -0
- package/assets/icons/settings.png +0 -0
- package/assets/icons/settings@2x.png +0 -0
- package/assets/icons/settings@3x.png +0 -0
- package/assets/icons/view.png +0 -0
- package/assets/icons/view@2x.png +0 -0
- package/assets/icons/view@3x.png +0 -0
- package/assets/icons/x.png +0 -0
- package/assets/icons/x@2x.png +0 -0
- package/assets/icons/x@3x.png +0 -0
- package/assets/images/app-icon-all.png +0 -0
- package/assets/images/app-icon-android-adaptive-background.png +0 -0
- package/assets/images/app-icon-android-adaptive-foreground.png +0 -0
- package/assets/images/app-icon-android-legacy.png +0 -0
- package/assets/images/app-icon-ios.png +0 -0
- package/assets/images/app-icon-web-favicon.png +0 -0
- package/assets/images/demo/cr-logo.png +0 -0
- package/assets/images/demo/cr-logo@2x.png +0 -0
- package/assets/images/demo/cr-logo@3x.png +0 -0
- package/assets/images/demo/rnl-logo.png +0 -0
- package/assets/images/demo/rnl-logo@2x.png +0 -0
- package/assets/images/demo/rnl-logo@3x.png +0 -0
- package/assets/images/demo/rnn-logo.png +0 -0
- package/assets/images/demo/rnn-logo@2x.png +0 -0
- package/assets/images/demo/rnn-logo@3x.png +0 -0
- package/assets/images/demo/rnr-image-1.png +0 -0
- package/assets/images/demo/rnr-image-1@2x.png +0 -0
- package/assets/images/demo/rnr-image-1@3x.png +0 -0
- package/assets/images/demo/rnr-image-2.png +0 -0
- package/assets/images/demo/rnr-image-2@2x.png +0 -0
- package/assets/images/demo/rnr-image-2@3x.png +0 -0
- package/assets/images/demo/rnr-image-3.png +0 -0
- package/assets/images/demo/rnr-image-3@2x.png +0 -0
- package/assets/images/demo/rnr-image-3@3x.png +0 -0
- package/assets/images/demo/rnr-logo.png +0 -0
- package/assets/images/demo/rnr-logo@2x.png +0 -0
- package/assets/images/demo/rnr-logo@3x.png +0 -0
- package/assets/images/logo.png +0 -0
- package/assets/images/logo@2x.png +0 -0
- package/assets/images/logo@3x.png +0 -0
- package/assets/images/sad-face.png +0 -0
- package/assets/images/sad-face@2x.png +0 -0
- package/assets/images/sad-face@3x.png +0 -0
- package/assets/images/welcome-face.png +0 -0
- package/assets/images/welcome-face@2x.png +0 -0
- package/assets/images/welcome-face@3x.png +0 -0
- package/babel.config.js +7 -0
- package/bin/cli.js +196 -0
- package/ignite/templates/app-icon/android-adaptive-background.png +0 -0
- package/ignite/templates/app-icon/android-adaptive-foreground.png +0 -0
- package/ignite/templates/app-icon/android-legacy.png +0 -0
- package/ignite/templates/app-icon/ios-universal.png +0 -0
- package/ignite/templates/component/NAME.tsx.ejs +39 -0
- package/ignite/templates/navigator/NAMENavigator.tsx.ejs +18 -0
- package/ignite/templates/screen/NAMEScreen.tsx.ejs +29 -0
- package/ignite/templates/splash-screen/logo.png +0 -0
- package/index.tsx +9 -0
- package/jest.config.js +5 -0
- package/metro.config.js +31 -0
- package/package.json +166 -0
- package/plugins/withSplashScreen.ts +69 -0
- package/src/app/_layout.tsx +58 -0
- package/src/app/index.tsx +5 -0
- package/test/i18n.test.ts +75 -0
- package/test/mockFile.ts +6 -0
- package/test/setup.ts +58 -0
- package/test/test-tsconfig.json +8 -0
- package/tsconfig.json +52 -0
- 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
|
+
};
|