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,394 @@
|
|
|
1
|
+
import Parse from "@/lib/Parse";
|
|
2
|
+
import type { GoogleAuthResponse, PersistedUserData } from "./types";
|
|
3
|
+
import { validateEmail, validatePassword, validateUsername } from "./validation";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authentication services
|
|
7
|
+
* Contains all Parse-related authentication operations
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lightweight probe to confirm the Parse backend is reachable
|
|
12
|
+
*/
|
|
13
|
+
export const checkParseServerConnection = async (): Promise<boolean> => {
|
|
14
|
+
try {
|
|
15
|
+
const TestObject = Parse.Object.extend("TestConnection");
|
|
16
|
+
const query = new Parse.Query(TestObject);
|
|
17
|
+
query.limit(1);
|
|
18
|
+
await query.find();
|
|
19
|
+
return true;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error("Parse server connection check failed:", error);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Logs in a user with email and password
|
|
28
|
+
*/
|
|
29
|
+
export const loginUser = async (
|
|
30
|
+
email: string,
|
|
31
|
+
password: string
|
|
32
|
+
): Promise<{ success: boolean; error?: string; user?: Parse.User }> => {
|
|
33
|
+
try {
|
|
34
|
+
// 1) Validate inputs
|
|
35
|
+
const emailError = validateEmail(email);
|
|
36
|
+
const passwordError = validatePassword(password);
|
|
37
|
+
|
|
38
|
+
if (emailError || passwordError) {
|
|
39
|
+
const errorMessage = emailError || passwordError;
|
|
40
|
+
return { success: false, error: errorMessage };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log("Checking Parse server connection...");
|
|
44
|
+
const isServerRunning = await checkParseServerConnection();
|
|
45
|
+
|
|
46
|
+
if (!isServerRunning) {
|
|
47
|
+
const errorMessage =
|
|
48
|
+
"Parse server is not running. Please check your server connection.";
|
|
49
|
+
console.error("Server check failed:", errorMessage);
|
|
50
|
+
return { success: false, error: errorMessage };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2) Attempt Parse login
|
|
54
|
+
console.log("Parse server is running. Proceeding with login...");
|
|
55
|
+
|
|
56
|
+
const user = await Parse.User.logIn(email, password);
|
|
57
|
+
return { success: true, user };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
let errorMessage = "Failed to log in";
|
|
60
|
+
|
|
61
|
+
if (error instanceof Error) {
|
|
62
|
+
if (error.message.includes("Invalid username/password")) {
|
|
63
|
+
errorMessage = "Invalid email or password. Please try again.";
|
|
64
|
+
} else if (
|
|
65
|
+
error.message.includes("XMLHttpRequest") ||
|
|
66
|
+
error.message.includes("Network Error") ||
|
|
67
|
+
error.message.includes("Failed to fetch") ||
|
|
68
|
+
error.message.includes("ECONNREFUSED")
|
|
69
|
+
) {
|
|
70
|
+
errorMessage =
|
|
71
|
+
"Cannot connect to server. Please check if the Parse server is running.";
|
|
72
|
+
} else {
|
|
73
|
+
errorMessage = error.message;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.error("Login error:", errorMessage);
|
|
78
|
+
return { success: false, error: errorMessage };
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Signs up a new user
|
|
84
|
+
*/
|
|
85
|
+
export const signUpUser = async (
|
|
86
|
+
username: string,
|
|
87
|
+
email: string,
|
|
88
|
+
password: string
|
|
89
|
+
): Promise<{ success: boolean; error?: string; user?: Parse.User }> => {
|
|
90
|
+
try {
|
|
91
|
+
// 1) Validate inputs
|
|
92
|
+
const emailError = validateEmail(email);
|
|
93
|
+
const passwordError = validatePassword(password);
|
|
94
|
+
const usernameError = validateUsername(username);
|
|
95
|
+
|
|
96
|
+
if (emailError || passwordError || usernameError) {
|
|
97
|
+
const errorMessage = emailError || passwordError || usernameError;
|
|
98
|
+
return { success: false, error: errorMessage };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("Checking Parse server connection...");
|
|
102
|
+
const isServerRunning = await checkParseServerConnection();
|
|
103
|
+
|
|
104
|
+
if (!isServerRunning) {
|
|
105
|
+
const errorMessage =
|
|
106
|
+
"Parse server is not running. Please check your server connection.";
|
|
107
|
+
console.error("Server check failed:", errorMessage);
|
|
108
|
+
return { success: false, error: errorMessage };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2) Create and sign up a new Parse user
|
|
112
|
+
console.log("Parse server is running. Proceeding with signup...");
|
|
113
|
+
|
|
114
|
+
const user = new Parse.User();
|
|
115
|
+
user.set("username", username.trim());
|
|
116
|
+
user.set("email", email);
|
|
117
|
+
user.set("password", password);
|
|
118
|
+
|
|
119
|
+
const newUser = await user.signUp();
|
|
120
|
+
|
|
121
|
+
console.log("Signup successful!");
|
|
122
|
+
return { success: true, user: newUser };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
let errorMessage = "Failed to sign up";
|
|
125
|
+
|
|
126
|
+
if (error instanceof Error) {
|
|
127
|
+
if (error.message.includes("username") && error.message.includes("taken")) {
|
|
128
|
+
errorMessage = "Username is already taken. Please choose another one.";
|
|
129
|
+
} else if (error.message.includes("email") && error.message.includes("taken")) {
|
|
130
|
+
errorMessage = "Email is already registered. Please use another email or try logging in.";
|
|
131
|
+
} else if (error.message.includes("Account already exists")) {
|
|
132
|
+
errorMessage = "An account with this email already exists. Please try logging in.";
|
|
133
|
+
} else if (
|
|
134
|
+
error.message.includes("XMLHttpRequest") ||
|
|
135
|
+
error.message.includes("Network Error") ||
|
|
136
|
+
error.message.includes("Failed to fetch") ||
|
|
137
|
+
error.message.includes("ECONNREFUSED")
|
|
138
|
+
) {
|
|
139
|
+
errorMessage =
|
|
140
|
+
"Cannot connect to server. Please check if the Parse server is running.";
|
|
141
|
+
} else {
|
|
142
|
+
errorMessage = error.message;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.error("Signup error:", errorMessage);
|
|
147
|
+
return { success: false, error: errorMessage };
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Signs in a user with Google OAuth
|
|
153
|
+
*/
|
|
154
|
+
export const signInWithGoogle = async (
|
|
155
|
+
response: GoogleAuthResponse
|
|
156
|
+
): Promise<{ success: boolean; error?: string; user?: Parse.User }> => {
|
|
157
|
+
try {
|
|
158
|
+
// 1) Validate OAuth response
|
|
159
|
+
if (response?.type !== "success") {
|
|
160
|
+
console.log("Google authentication was not successful");
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: "Google authentication was cancelled or failed",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
!response.authentication?.idToken ||
|
|
169
|
+
!response.authentication?.accessToken
|
|
170
|
+
) {
|
|
171
|
+
console.error(
|
|
172
|
+
"Missing required authentication data in response",
|
|
173
|
+
response.authentication
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error:
|
|
178
|
+
"Incomplete authentication data. Please try signing in again.",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 2) Ensure backend is reachable
|
|
183
|
+
console.log("Checking Parse server connection...");
|
|
184
|
+
const isServerRunning = await checkParseServerConnection();
|
|
185
|
+
|
|
186
|
+
if (!isServerRunning) {
|
|
187
|
+
const errorMessage =
|
|
188
|
+
"Parse server is not running. Please check your server connection.";
|
|
189
|
+
console.error("Server check failed:", errorMessage);
|
|
190
|
+
return { success: false, error: errorMessage };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 3) Fetch Google profile and authenticate with Parse using authData
|
|
194
|
+
console.log("Parse server is running. Proceeding with Google sign-in...");
|
|
195
|
+
|
|
196
|
+
const { idToken, accessToken } = response.authentication;
|
|
197
|
+
|
|
198
|
+
// Fetch Google user profile
|
|
199
|
+
const profileResponse = await fetch(
|
|
200
|
+
"https://www.googleapis.com/oauth2/v3/userinfo",
|
|
201
|
+
{
|
|
202
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!profileResponse.ok) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Failed to fetch user profile: ${profileResponse.status}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const profile = await profileResponse.json();
|
|
213
|
+
|
|
214
|
+
const { sub: googleUserId, email, name, picture } = profile;
|
|
215
|
+
|
|
216
|
+
if (!googleUserId) {
|
|
217
|
+
throw new Error("Google ID (sub) not found in user profile");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!email || !name) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"Required user information missing from Google profile"
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Prepare Parse authentication data
|
|
227
|
+
const authData = {
|
|
228
|
+
id: googleUserId,
|
|
229
|
+
id_token: idToken,
|
|
230
|
+
access_token: accessToken,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Authenticate with Parse
|
|
234
|
+
const user = await Parse.User.logInWith("google", { authData });
|
|
235
|
+
|
|
236
|
+
// Update user profile
|
|
237
|
+
user.set("username", name.trim());
|
|
238
|
+
user.set("email", email);
|
|
239
|
+
|
|
240
|
+
console.log("Google login successful:", {
|
|
241
|
+
userId: googleUserId,
|
|
242
|
+
email,
|
|
243
|
+
username: name,
|
|
244
|
+
hasPicture: Boolean(picture),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return { success: true, user };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
let errorMessage = "Failed to sign in with Google";
|
|
250
|
+
|
|
251
|
+
if (error instanceof Error) {
|
|
252
|
+
if (
|
|
253
|
+
error.message.includes("XMLHttpRequest") ||
|
|
254
|
+
error.message.includes("Network Error") ||
|
|
255
|
+
error.message.includes("Failed to fetch") ||
|
|
256
|
+
error.message.includes("ECONNREFUSED")
|
|
257
|
+
) {
|
|
258
|
+
errorMessage =
|
|
259
|
+
"Cannot connect to server. Please check if the Parse server is running.";
|
|
260
|
+
} else {
|
|
261
|
+
errorMessage = error.message;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.error("Google sign-in error:", errorMessage);
|
|
266
|
+
return { success: false, error: errorMessage };
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Requests a password reset for the given email
|
|
272
|
+
*/
|
|
273
|
+
export const requestPasswordReset = async (
|
|
274
|
+
email: string
|
|
275
|
+
): Promise<{ success: boolean; error?: string; message?: string }> => {
|
|
276
|
+
try {
|
|
277
|
+
// 1) Validate email
|
|
278
|
+
const emailError = validateEmail(email);
|
|
279
|
+
if (emailError) {
|
|
280
|
+
return { success: false, error: emailError };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 2) Check server connection
|
|
284
|
+
console.log("Checking Parse server connection...");
|
|
285
|
+
const isServerRunning = await checkParseServerConnection();
|
|
286
|
+
|
|
287
|
+
if (!isServerRunning) {
|
|
288
|
+
const errorMessage =
|
|
289
|
+
"Parse server is not running. Please check your server connection.";
|
|
290
|
+
console.error("Server check failed:", errorMessage);
|
|
291
|
+
return { success: false, error: errorMessage };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 3) Request password reset
|
|
295
|
+
console.log("Parse server is running. Requesting password reset...");
|
|
296
|
+
await Parse.User.requestPasswordReset(email);
|
|
297
|
+
|
|
298
|
+
const successMessage = `Password reset email sent to ${email}. Please check your inbox and follow the instructions to reset your password.`;
|
|
299
|
+
|
|
300
|
+
console.log("Password reset request successful for:", email);
|
|
301
|
+
return { success: true, message: successMessage };
|
|
302
|
+
} catch (error) {
|
|
303
|
+
let errorMessage = "Failed to request password reset";
|
|
304
|
+
|
|
305
|
+
if (error instanceof Error) {
|
|
306
|
+
if (error.message.includes("no user found with email")) {
|
|
307
|
+
errorMessage = "No account found with this email address.";
|
|
308
|
+
} else if (error.message.includes("email adapter")) {
|
|
309
|
+
errorMessage = "Email service is not configured. Please contact support.";
|
|
310
|
+
} else if (
|
|
311
|
+
error.message.includes("XMLHttpRequest") ||
|
|
312
|
+
error.message.includes("Network Error") ||
|
|
313
|
+
error.message.includes("Failed to fetch") ||
|
|
314
|
+
error.message.includes("ECONNREFUSED")
|
|
315
|
+
) {
|
|
316
|
+
errorMessage =
|
|
317
|
+
"Cannot connect to server. Please check if the Parse server is running.";
|
|
318
|
+
} else {
|
|
319
|
+
errorMessage = error.message;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.error("Password reset error:", errorMessage);
|
|
324
|
+
return { success: false, error: errorMessage };
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Logs out the current user
|
|
330
|
+
*/
|
|
331
|
+
export const logoutUser = async (): Promise<{
|
|
332
|
+
success: boolean;
|
|
333
|
+
error?: string;
|
|
334
|
+
}> => {
|
|
335
|
+
try {
|
|
336
|
+
// Attempt Parse logout (may fail silently on RN; that's OK)
|
|
337
|
+
try {
|
|
338
|
+
await Parse.User.logOut();
|
|
339
|
+
} catch (logoutError) {
|
|
340
|
+
console.warn(
|
|
341
|
+
"Parse.User.logOut() failed (common in React Native):",
|
|
342
|
+
logoutError
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { success: true };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const errorMessage =
|
|
349
|
+
error instanceof Error ? error.message : "Failed to log out";
|
|
350
|
+
console.error("Logout error:", errorMessage);
|
|
351
|
+
return { success: false, error: errorMessage };
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Gets the current user from Parse
|
|
357
|
+
*/
|
|
358
|
+
export const getCurrentUser = async (): Promise<Parse.User | null> => {
|
|
359
|
+
try {
|
|
360
|
+
return await Parse.User.currentAsync();
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error("Error getting current user:", error);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Fetches all users from Parse
|
|
369
|
+
*/
|
|
370
|
+
export const fetchAllUsers = async (): Promise<Parse.User[]> => {
|
|
371
|
+
try {
|
|
372
|
+
const query = new Parse.Query(Parse.User);
|
|
373
|
+
const results = await query.find();
|
|
374
|
+
return results;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error("Error fetching users:", error);
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Restores a user session from a persisted token
|
|
383
|
+
*/
|
|
384
|
+
export const restoreUserSession = async (
|
|
385
|
+
token: string
|
|
386
|
+
): Promise<Parse.User | null> => {
|
|
387
|
+
try {
|
|
388
|
+
const user = await Parse.User.become(token);
|
|
389
|
+
return user;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error("Failed to restore user session:", error);
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import Parse from "@/lib/Parse";
|
|
2
|
+
import type { TokenResponse } from "expo-auth-session";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type definitions for authentication system
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Narrow Parse user shape we rely on across the UI
|
|
9
|
+
export interface IAppUser extends Parse.User {
|
|
10
|
+
getEmail(): string;
|
|
11
|
+
getUsername(): string;
|
|
12
|
+
getSessionToken(): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GoogleAuthResponse {
|
|
16
|
+
type: "success" | "dismiss" | "cancel" | "opened" | "locked" | "error";
|
|
17
|
+
errorCode?: string | null;
|
|
18
|
+
error?: any;
|
|
19
|
+
params?: Record<string, string>;
|
|
20
|
+
authentication?:
|
|
21
|
+
| (TokenResponse & {
|
|
22
|
+
idToken?: string;
|
|
23
|
+
accessToken?: string;
|
|
24
|
+
})
|
|
25
|
+
| null;
|
|
26
|
+
url?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// In-memory auth state kept in the provider
|
|
30
|
+
export interface AuthState {
|
|
31
|
+
authToken?: string;
|
|
32
|
+
authEmail: string;
|
|
33
|
+
authPassword: string;
|
|
34
|
+
isLoading: boolean;
|
|
35
|
+
error: string;
|
|
36
|
+
currentUser?: Parse.User;
|
|
37
|
+
username?: string;
|
|
38
|
+
resetPasswordMessage?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Reducer actions
|
|
42
|
+
export type AuthAction =
|
|
43
|
+
| { type: "SET_AUTH_EMAIL"; payload: string }
|
|
44
|
+
| { type: "SET_AUTH_PASSWORD"; payload: string }
|
|
45
|
+
| { type: "SET_ERROR"; payload: string }
|
|
46
|
+
| { type: "SET_AUTH_TOKEN"; payload: { token?: string } }
|
|
47
|
+
| { type: "SET_LOADING"; payload: boolean }
|
|
48
|
+
| { type: "SET_CURRENT_USER"; payload: Parse.User | undefined }
|
|
49
|
+
| { type: "SET_RESET_PASSWORD_MESSAGE"; payload: string }
|
|
50
|
+
| { type: "RESET_AUTH_STATE" }
|
|
51
|
+
| { type: "CLEAR_FORM" }
|
|
52
|
+
| { type: "CLEAR_RESET_MESSAGE" };
|
|
53
|
+
|
|
54
|
+
// Public API exposed via the Auth context
|
|
55
|
+
export type AuthContextType = {
|
|
56
|
+
// State
|
|
57
|
+
isAuthenticated: boolean;
|
|
58
|
+
authToken?: string;
|
|
59
|
+
authEmail: string;
|
|
60
|
+
authPassword: string;
|
|
61
|
+
isLoading: boolean;
|
|
62
|
+
error: string;
|
|
63
|
+
currentUser?: Parse.User;
|
|
64
|
+
username?: string;
|
|
65
|
+
resetPasswordMessage?: string;
|
|
66
|
+
|
|
67
|
+
// Actions
|
|
68
|
+
setAuthEmail: (email: string) => void;
|
|
69
|
+
setAuthPassword: (password: string) => void;
|
|
70
|
+
setError: (error: string) => void;
|
|
71
|
+
setAuthToken: (token?: string) => void;
|
|
72
|
+
resetAuthState: () => void;
|
|
73
|
+
clearForm: () => void;
|
|
74
|
+
clearResetMessage: () => void;
|
|
75
|
+
login: () => Promise<{ success: boolean; error?: string }>;
|
|
76
|
+
signUp: (username: string) => Promise<{ success: boolean; error?: string }>;
|
|
77
|
+
googleSignIn: (
|
|
78
|
+
response: GoogleAuthResponse
|
|
79
|
+
) => Promise<{ success: boolean; error?: string; user?: Parse.User }>;
|
|
80
|
+
requestPasswordReset: (email: string) => Promise<{ success: boolean; error?: string; message?: string }>;
|
|
81
|
+
logout: () => Promise<{ success: boolean; error?: string }>;
|
|
82
|
+
checkCurrentUser: () => Promise<boolean>;
|
|
83
|
+
checkServerStatus: () => Promise<{ isRunning: boolean; message: string }>;
|
|
84
|
+
|
|
85
|
+
// Validation helpers
|
|
86
|
+
validateEmail: (email: string) => string | undefined;
|
|
87
|
+
validatePassword: (password: string) => string | undefined;
|
|
88
|
+
validateUsername: (username: string) => string | undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export interface AuthProviderProps {}
|
|
92
|
+
|
|
93
|
+
// Persisted user data structure
|
|
94
|
+
export interface PersistedUserData {
|
|
95
|
+
objectId: string;
|
|
96
|
+
sessionToken: string;
|
|
97
|
+
username: string;
|
|
98
|
+
email: string;
|
|
99
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for authentication
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates an email address
|
|
7
|
+
* @param email - The email address to validate
|
|
8
|
+
* @returns Error message if invalid, undefined if valid
|
|
9
|
+
*/
|
|
10
|
+
export const validateEmail = (email: string): string | undefined => {
|
|
11
|
+
if (!email.length) return "Email can't be blank";
|
|
12
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
13
|
+
return "Must be a valid email address";
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates a password
|
|
20
|
+
* @param password - The password to validate
|
|
21
|
+
* @returns Error message if invalid, undefined if valid
|
|
22
|
+
*/
|
|
23
|
+
export const validatePassword = (password: string): string | undefined => {
|
|
24
|
+
if (!password.length) return "Password can't be blank";
|
|
25
|
+
if (password.length < 6) {
|
|
26
|
+
return "Password must be at least 6 characters";
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates a username
|
|
33
|
+
* @param username - The username to validate
|
|
34
|
+
* @returns Error message if invalid, undefined if valid
|
|
35
|
+
*/
|
|
36
|
+
export const validateUsername = (username: string): string | undefined => {
|
|
37
|
+
if (!username.trim().length) return "Username can't be blank";
|
|
38
|
+
if (username.trim().length < 3) {
|
|
39
|
+
return "Username must be at least 3 characters";
|
|
40
|
+
}
|
|
41
|
+
if (!/^[a-zA-Z0-9_]+$/.test(username.trim())) {
|
|
42
|
+
return "Username can only contain letters, numbers, and underscores";
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is loaded in web and exports the React.js version
|
|
3
|
+
* of Reactotron's client.
|
|
4
|
+
*
|
|
5
|
+
* React Native is loaded from ReactotronClient.ts.
|
|
6
|
+
*
|
|
7
|
+
* If your project does not need web support, you can delete this file and
|
|
8
|
+
* remove reactotron-react-js from your package.json dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import Reactotron from "reactotron-react-js"
|
|
11
|
+
|
|
12
|
+
export { Reactotron }
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file does the setup for integration with Reactotron, which is a
|
|
3
|
+
* free desktop app for inspecting and debugging your React Native app.
|
|
4
|
+
* @see https://github.com/infinitered/reactotron
|
|
5
|
+
*/
|
|
6
|
+
import { Platform, NativeModules } from "react-native"
|
|
7
|
+
import { ArgType } from "reactotron-core-client"
|
|
8
|
+
import { ReactotronReactNative } from "reactotron-react-native"
|
|
9
|
+
import mmkvPlugin from "reactotron-react-native-mmkv"
|
|
10
|
+
|
|
11
|
+
import { goBack, resetRoot, navigate } from "@/navigators/navigationUtilities"
|
|
12
|
+
import { storage } from "@/utils/storage"
|
|
13
|
+
|
|
14
|
+
import { Reactotron } from "./ReactotronClient"
|
|
15
|
+
|
|
16
|
+
const reactotron = Reactotron.configure({
|
|
17
|
+
name: require("../../package.json").name,
|
|
18
|
+
onConnect: () => {
|
|
19
|
+
/** since this file gets hot reloaded, let's clear the past logs every time we connect */
|
|
20
|
+
Reactotron.clear()
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
reactotron.use(mmkvPlugin<ReactotronReactNative>({ storage }))
|
|
25
|
+
|
|
26
|
+
if (Platform.OS !== "web") {
|
|
27
|
+
reactotron.useReactNative({
|
|
28
|
+
networking: {
|
|
29
|
+
ignoreUrls: /symbolicate/,
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reactotron allows you to define custom commands that you can run
|
|
36
|
+
* from Reactotron itself, and they will run in your app.
|
|
37
|
+
*
|
|
38
|
+
* Define them in the section below with `onCustomCommand`. Use your
|
|
39
|
+
* creativity -- this is great for development to quickly and easily
|
|
40
|
+
* get your app into the state you want.
|
|
41
|
+
*
|
|
42
|
+
* NOTE: If you edit this file while running the app, you will need to do a full refresh
|
|
43
|
+
* or else your custom commands won't be registered correctly.
|
|
44
|
+
*/
|
|
45
|
+
reactotron.onCustomCommand({
|
|
46
|
+
title: "Show Dev Menu",
|
|
47
|
+
description: "Opens the React Native dev menu",
|
|
48
|
+
command: "showDevMenu",
|
|
49
|
+
handler: () => {
|
|
50
|
+
Reactotron.log("Showing React Native dev menu")
|
|
51
|
+
NativeModules.DevMenu.show()
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
reactotron.onCustomCommand({
|
|
56
|
+
title: "Reset Navigation State",
|
|
57
|
+
description: "Resets the navigation state",
|
|
58
|
+
command: "resetNavigation",
|
|
59
|
+
handler: () => {
|
|
60
|
+
Reactotron.log("resetting navigation state")
|
|
61
|
+
resetRoot({ index: 0, routes: [] })
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
reactotron.onCustomCommand<[{ name: "route"; type: ArgType.String }]>({
|
|
66
|
+
command: "navigateTo",
|
|
67
|
+
handler: (args) => {
|
|
68
|
+
const { route } = args ?? {}
|
|
69
|
+
if (route) {
|
|
70
|
+
Reactotron.log(`Navigating to: ${route}`)
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
navigate(route as any) // this should be tied to the navigator, but since this is for debugging, we can navigate to illegal routes
|
|
73
|
+
} else {
|
|
74
|
+
Reactotron.log("Could not navigate. No route provided.")
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
title: "Navigate To Screen",
|
|
78
|
+
description: "Navigates to a screen by name.",
|
|
79
|
+
args: [{ name: "route", type: ArgType.String }],
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
reactotron.onCustomCommand({
|
|
83
|
+
title: "Go Back",
|
|
84
|
+
description: "Goes back",
|
|
85
|
+
command: "goBack",
|
|
86
|
+
handler: () => {
|
|
87
|
+
Reactotron.log("Going back")
|
|
88
|
+
goBack()
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* We're going to add `console.tron` to the Reactotron object.
|
|
94
|
+
* Now, anywhere in our app in development, we can use Reactotron like so:
|
|
95
|
+
*
|
|
96
|
+
* ```
|
|
97
|
+
* if (__DEV__) {
|
|
98
|
+
* console.tron.display({
|
|
99
|
+
* name: 'JOKE',
|
|
100
|
+
* preview: 'What's the best thing about Switzerland?',
|
|
101
|
+
* value: 'I don't know, but the flag is a big plus!',
|
|
102
|
+
* important: true
|
|
103
|
+
* })
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* Use this power responsibly! :)
|
|
108
|
+
*/
|
|
109
|
+
console.tron = reactotron
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* We tell typescript about our dark magic
|
|
113
|
+
*
|
|
114
|
+
* You can also import Reactotron yourself from ./reactotronClient
|
|
115
|
+
* and use it directly, like Reactotron.log('hello world')
|
|
116
|
+
*/
|
|
117
|
+
declare global {
|
|
118
|
+
interface Console {
|
|
119
|
+
/**
|
|
120
|
+
* Reactotron client for logging, displaying, measuring performance, and more.
|
|
121
|
+
* @see https://github.com/infinitered/reactotron
|
|
122
|
+
* @example
|
|
123
|
+
* if (__DEV__) {
|
|
124
|
+
* console.tron.display({
|
|
125
|
+
* name: 'JOKE',
|
|
126
|
+
* preview: 'What's the best thing about Switzerland?',
|
|
127
|
+
* value: 'I don't know, but the flag is a big plus!',
|
|
128
|
+
* important: true
|
|
129
|
+
* })
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
tron: typeof reactotron
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Now that we've setup all our Reactotron configuration, let's connect!
|
|
138
|
+
*/
|
|
139
|
+
reactotron.connect()
|