@churchapps/apphelper-login 0.4.13

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 (73) hide show
  1. package/README.md +104 -0
  2. package/dist/LoginPage.d.ts +25 -0
  3. package/dist/LoginPage.d.ts.map +1 -0
  4. package/dist/LoginPage.js +248 -0
  5. package/dist/LoginPage.js.map +1 -0
  6. package/dist/LogoutPage.d.ts +9 -0
  7. package/dist/LogoutPage.d.ts.map +1 -0
  8. package/dist/LogoutPage.js +33 -0
  9. package/dist/LogoutPage.js.map +1 -0
  10. package/dist/components/Forgot.d.ts +8 -0
  11. package/dist/components/Forgot.d.ts.map +1 -0
  12. package/dist/components/Forgot.js +127 -0
  13. package/dist/components/Forgot.js.map +1 -0
  14. package/dist/components/Login.d.ts +15 -0
  15. package/dist/components/Login.d.ts.map +1 -0
  16. package/dist/components/Login.js +126 -0
  17. package/dist/components/Login.js.map +1 -0
  18. package/dist/components/LoginSetPassword.d.ts +13 -0
  19. package/dist/components/LoginSetPassword.d.ts.map +1 -0
  20. package/dist/components/LoginSetPassword.js +167 -0
  21. package/dist/components/LoginSetPassword.js.map +1 -0
  22. package/dist/components/Register.d.ts +12 -0
  23. package/dist/components/Register.d.ts.map +1 -0
  24. package/dist/components/Register.js +217 -0
  25. package/dist/components/Register.js.map +1 -0
  26. package/dist/components/SelectChurchModal.d.ts +14 -0
  27. package/dist/components/SelectChurchModal.d.ts.map +1 -0
  28. package/dist/components/SelectChurchModal.js +39 -0
  29. package/dist/components/SelectChurchModal.js.map +1 -0
  30. package/dist/components/SelectChurchRegister.d.ts +11 -0
  31. package/dist/components/SelectChurchRegister.d.ts.map +1 -0
  32. package/dist/components/SelectChurchRegister.js +83 -0
  33. package/dist/components/SelectChurchRegister.js.map +1 -0
  34. package/dist/components/SelectChurchSearch.d.ts +10 -0
  35. package/dist/components/SelectChurchSearch.d.ts.map +1 -0
  36. package/dist/components/SelectChurchSearch.js +61 -0
  37. package/dist/components/SelectChurchSearch.js.map +1 -0
  38. package/dist/components/SelectableChurch.d.ts +9 -0
  39. package/dist/components/SelectableChurch.d.ts.map +1 -0
  40. package/dist/components/SelectableChurch.js +34 -0
  41. package/dist/components/SelectableChurch.js.map +1 -0
  42. package/dist/helpers/AnalyticsHelper.d.ts +7 -0
  43. package/dist/helpers/AnalyticsHelper.d.ts.map +1 -0
  44. package/dist/helpers/AnalyticsHelper.js +30 -0
  45. package/dist/helpers/AnalyticsHelper.js.map +1 -0
  46. package/dist/helpers/Locale.d.ts +13 -0
  47. package/dist/helpers/Locale.d.ts.map +1 -0
  48. package/dist/helpers/Locale.js +200 -0
  49. package/dist/helpers/Locale.js.map +1 -0
  50. package/dist/helpers/index.d.ts +3 -0
  51. package/dist/helpers/index.d.ts.map +1 -0
  52. package/dist/helpers/index.js +3 -0
  53. package/dist/helpers/index.js.map +1 -0
  54. package/dist/index.d.ts +11 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +11 -0
  57. package/dist/index.js.map +1 -0
  58. package/package.json +57 -0
  59. package/src/LoginPage.tsx +283 -0
  60. package/src/LogoutPage.tsx +43 -0
  61. package/src/components/Forgot.tsx +247 -0
  62. package/src/components/Login.tsx +250 -0
  63. package/src/components/LoginSetPassword.tsx +298 -0
  64. package/src/components/Register.tsx +371 -0
  65. package/src/components/SelectChurchModal.tsx +82 -0
  66. package/src/components/SelectChurchRegister.tsx +88 -0
  67. package/src/components/SelectChurchSearch.tsx +85 -0
  68. package/src/components/SelectableChurch.tsx +72 -0
  69. package/src/helpers/AnalyticsHelper.ts +32 -0
  70. package/src/helpers/Locale.ts +233 -0
  71. package/src/helpers/index.ts +2 -0
  72. package/src/index.ts +10 -0
  73. package/tsconfig.json +30 -0
@@ -0,0 +1,11 @@
1
+ export { LoginPage } from "./LoginPage";
2
+ export { LogoutPage } from "./LogoutPage";
3
+ export { Register } from "./components/Register";
4
+ export { Login } from "./components/Login";
5
+ export { Forgot } from "./components/Forgot";
6
+ export { LoginSetPassword } from "./components/LoginSetPassword";
7
+ export { SelectChurchModal } from "./components/SelectChurchModal";
8
+ export { SelectChurchRegister } from "./components/SelectChurchRegister";
9
+ export { SelectChurchSearch } from "./components/SelectChurchSearch";
10
+ export { SelectableChurch } from "./components/SelectableChurch";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { LoginPage } from "./LoginPage";
2
+ export { LogoutPage } from "./LogoutPage";
3
+ export { Register } from "./components/Register";
4
+ export { Login } from "./components/Login";
5
+ export { Forgot } from "./components/Forgot";
6
+ export { LoginSetPassword } from "./components/LoginSetPassword";
7
+ export { SelectChurchModal } from "./components/SelectChurchModal";
8
+ export { SelectChurchRegister } from "./components/SelectChurchRegister";
9
+ export { SelectChurchSearch } from "./components/SelectChurchSearch";
10
+ export { SelectableChurch } from "./components/SelectableChurch";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@churchapps/apphelper-login",
3
+ "version": "0.4.13",
4
+ "description": "Login components for ChurchApps AppHelper",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "test": "echo \"Error: no test specified\" && exit 1",
10
+ "clean": "rimraf dist",
11
+ "tsc": "tsc",
12
+ "build": "npm-run-all clean tsc"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/LiveChurchSolutions/AppHelper.git"
17
+ },
18
+ "keywords": [
19
+ "ChurchApps",
20
+ "login",
21
+ "authentication"
22
+ ],
23
+ "author": "ChurchApps.org",
24
+ "license": "MIT",
25
+ "bugs": {
26
+ "url": "https://github.com/LiveChurchSolutions/AppHelper/issues"
27
+ },
28
+ "homepage": "https://github.com/LiveChurchSolutions/AppHelper#readme",
29
+ "peerDependencies": {
30
+ "react": "^19.1.0",
31
+ "react-dom": "^19.1.0",
32
+ "react-router-dom": "^7.6.3"
33
+ },
34
+ "dependencies": {
35
+ "@churchapps/helpers": "^1.0.40",
36
+ "@churchapps/apphelper": "*",
37
+ "@emotion/react": "^11.14.0",
38
+ "@emotion/styled": "^11.14.1",
39
+ "@mui/material": "^7.2.0",
40
+ "axios": "^1.10.0",
41
+ "i18next": "^25.3.1",
42
+ "jwt-decode": "^4.0.0",
43
+ "react-cookie": "^8.0.1",
44
+ "react-google-recaptcha": "^3.1.0",
45
+ "react-i18next": "^15.6.0",
46
+ "react-ga4": "^2.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^24.0.10",
50
+ "@types/react": "^19.1.8",
51
+ "@types/react-dom": "^19.1.6",
52
+ "@types/react-google-recaptcha": "^2.1.5",
53
+ "npm-run-all2": "^8.0.4",
54
+ "rimraf": "^6.0.1",
55
+ "typescript": "^5.8.3"
56
+ }
57
+ }
@@ -0,0 +1,283 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ErrorMessages, FloatingSupport, Loading } from "@churchapps/apphelper";
5
+ import { LoginResponseInterface, UserContextInterface, ChurchInterface, UserInterface, LoginUserChurchInterface } from "@churchapps/helpers";
6
+ import { ApiHelper, ArrayHelper, UserHelper, CommonEnvironmentHelper } from "@churchapps/helpers";
7
+ import { AnalyticsHelper, Locale } from "./helpers";
8
+ import { useCookies, CookiesProvider } from "react-cookie"
9
+ import { jwtDecode } from "jwt-decode"
10
+ import { Register } from "./components/Register"
11
+ import { SelectChurchModal } from "./components/SelectChurchModal"
12
+ import { Forgot } from "./components/Forgot";
13
+ import { Alert, Box, PaperProps, Typography } from "@mui/material";
14
+ import { Login } from "./components/Login";
15
+ import { LoginSetPassword } from "./components/LoginSetPassword";
16
+ import ga4 from "react-ga4";
17
+
18
+ interface Props {
19
+ context: UserContextInterface,
20
+ jwt: string,
21
+ auth: string,
22
+ keyName?: string,
23
+ logo?: string,
24
+ appName?: string,
25
+ appUrl?: string,
26
+ returnUrl?: string,
27
+ loginSuccessOverride?: () => void,
28
+ userRegisteredCallback?: (user: UserInterface) => Promise<void>;
29
+ churchRegisteredCallback?: (church: ChurchInterface) => Promise<void>;
30
+ callbackErrors?: string[];
31
+ showLogo?: boolean;
32
+ loginContainerCssProps?: PaperProps;
33
+ defaultEmail?: string;
34
+ defaultPassword?: string;
35
+ handleRedirect?: (url: string) => void; // Function to handle redirects from parent component
36
+ }
37
+
38
+ const LoginPageContent: React.FC<Props> = ({ showLogo = true, loginContainerCssProps, ...props }) => {
39
+ const [welcomeBackName, setWelcomeBackName] = React.useState("");
40
+ const [pendingAutoLogin, setPendingAutoLogin] = React.useState(false);
41
+ const [errors, setErrors] = React.useState([]);
42
+ const [cookies, setCookie] = useCookies(["jwt", "name", "email", "lastChurchId"]);
43
+ const [showForgot, setShowForgot] = React.useState(false);
44
+ const [showRegister, setShowRegister] = React.useState(false);
45
+ const [showSelectModal, setShowSelectModal] = React.useState(false);
46
+ const [loginResponse, setLoginResponse] = React.useState<LoginResponseInterface>(null)
47
+ const [userJwt, setUserJwt] = React.useState("");
48
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
49
+
50
+ const loginFormRef = React.useRef(null);
51
+ const location = typeof window !== "undefined" && window.location;
52
+ let selectedChurchId = "";
53
+ let registeredChurch: ChurchInterface = null;
54
+ let userJwtBackup = ""; //use state copy for storing between page updates. This copy for instant availability.
55
+
56
+ const cleanAppUrl = () => {
57
+ if (!props.appUrl) return null;
58
+ else {
59
+ const index = props.appUrl.indexOf("/", 9);
60
+ if (index === -1) return props.appUrl;
61
+ else return props.appUrl.substring(0, index);
62
+ }
63
+ }
64
+
65
+ React.useEffect(() => {
66
+ if (props.callbackErrors?.length > 0) {
67
+ setErrors(props.callbackErrors)
68
+ }
69
+ }, [props.callbackErrors])
70
+
71
+ const init = () => {
72
+ const search = new URLSearchParams(location?.search);
73
+ const action = search.get("action");
74
+ if (action === "forgot") setShowForgot(true);
75
+ else if (action === "register") setShowRegister(true);
76
+ else {
77
+ if (!props.auth && props.jwt) {
78
+ setWelcomeBackName(cookies.name);
79
+ login({ jwt: props.jwt });
80
+ setPendingAutoLogin(true);
81
+ } else {
82
+ setPendingAutoLogin(false);
83
+ }
84
+ }
85
+ };
86
+
87
+ const handleLoginSuccess = async (resp: LoginResponseInterface) => {
88
+ userJwtBackup = resp.user.jwt;
89
+ setUserJwt(userJwtBackup);
90
+ ApiHelper.setDefaultPermissions(resp.user.jwt);
91
+ setLoginResponse(resp)
92
+ resp.userChurches.forEach(uc => { if (!uc.apis) uc.apis = []; });
93
+ UserHelper.userChurches = resp.userChurches;
94
+
95
+ setCookie("name", `${resp.user.firstName} ${resp.user.lastName}`, { path: "/" });
96
+ setCookie("email", resp.user.email, { path: "/" });
97
+ UserHelper.user = resp.user;
98
+
99
+ if (props.jwt) {
100
+ const decoded: any = jwtDecode(userJwtBackup)
101
+ selectedChurchId = decoded.churchId
102
+ }
103
+
104
+ const search = new URLSearchParams(location?.search);
105
+ const churchIdInParams = search.get("churchId");
106
+
107
+ if (props.keyName) selectChurchByKeyName();
108
+ else if (selectedChurchId) selectChurchById();
109
+ else if (churchIdInParams) selectChurch(churchIdInParams);
110
+ else if (props.jwt && cookies.lastChurchId && ArrayHelper.getOne(resp.userChurches, "church.id", cookies.lastChurchId)) {
111
+ selectedChurchId = cookies.lastChurchId;
112
+ selectChurchById();
113
+ }
114
+ else setShowSelectModal(true);
115
+ }
116
+
117
+ const selectChurchById = async () => {
118
+ await UserHelper.selectChurch(props.context, selectedChurchId, undefined);
119
+
120
+ setCookie("lastChurchId", selectedChurchId, { path: "/" });
121
+
122
+ if (registeredChurch) {
123
+ AnalyticsHelper.logEvent("Church", "Register", UserHelper.currentUserChurch.church.name);
124
+ try {
125
+ if (CommonEnvironmentHelper.GoogleAnalyticsTag && typeof (window) !== "undefined") {
126
+ ga4.gtag("event", "conversion", { send_to: "AW-427967381/Ba2qCLrXgJoYEJWHicwB" });
127
+ }
128
+ } catch { }
129
+ }
130
+ else AnalyticsHelper.logEvent("Church", "Select", UserHelper.currentUserChurch.church.name);
131
+
132
+ if (props.churchRegisteredCallback && registeredChurch) {
133
+ await props.churchRegisteredCallback(registeredChurch)
134
+ registeredChurch = null;
135
+ login({ jwt: userJwt || userJwtBackup });
136
+ } else await continueLoginProcess();
137
+ }
138
+
139
+ const selectChurchByKeyName = async () => {
140
+ if (!ArrayHelper.getOne(UserHelper.userChurches, "church.subDomain", props.keyName)) {
141
+ const userChurch: LoginUserChurchInterface = await ApiHelper.post("/churches/select", { subDomain: props.keyName }, "MembershipApi");
142
+ UserHelper.setupApiHelper(userChurch);
143
+ setCookie("lastChurchId", userChurch.church.id, { path: "/" });
144
+ //create/claim the person record and relogin
145
+ await ApiHelper.get("/people/claim/" + userChurch.church.id, "MembershipApi");
146
+ login({ jwt: userJwt || userJwtBackup });
147
+ return;
148
+ }
149
+ await UserHelper.selectChurch(props.context, undefined, props.keyName);
150
+ const selectedChurch = ArrayHelper.getOne(UserHelper.userChurches, "church.subDomain", props.keyName);
151
+ if (selectedChurch) setCookie("lastChurchId", selectedChurch.church.id, { path: "/" });
152
+ await continueLoginProcess()
153
+ return;
154
+ }
155
+
156
+ async function continueLoginProcess() {
157
+ if (UserHelper.currentUserChurch) {
158
+ UserHelper.currentUserChurch.apis.forEach(api => {
159
+ if (api.keyName === "MembershipApi") setCookie("jwt", api.jwt, { path: "/" });
160
+ })
161
+ try {
162
+ if (UserHelper.currentUserChurch.church.id) ApiHelper.patch(`/userChurch/${UserHelper.user.id}`, { churchId: UserHelper.currentUserChurch.church.id, appName: props.appName, lastAccessed: new Date() }, "MembershipApi")
163
+ } catch (e) {
164
+ console.log("Could not update user church accessed date")
165
+ }
166
+ }
167
+
168
+ if (props.loginSuccessOverride !== undefined) props.loginSuccessOverride();
169
+ else {
170
+ props.context.setUser(UserHelper.user);
171
+ props.context.setUserChurches(UserHelper.userChurches)
172
+ props.context.setUserChurch(UserHelper.currentUserChurch)
173
+ try {
174
+ const p = await ApiHelper.get(`/people/${UserHelper.currentUserChurch.person?.id}`, "MembershipApi");
175
+ if (p) props.context.setPerson(p);
176
+ } catch {
177
+ console.log("claiming person");
178
+ const personClaim = await ApiHelper.get("/people/claim/" + UserHelper.currentUserChurch.church.id, "MembershipApi");
179
+ props.context.setPerson(personClaim);
180
+ }
181
+
182
+ const search = new URLSearchParams(location?.search);
183
+ const returnUrl = search.get("returnUrl") || props.returnUrl;
184
+ if (returnUrl && typeof window !== "undefined") {
185
+ // Use handleRedirect function if available, otherwise fallback to window.location
186
+ if (props.handleRedirect) {
187
+ props.handleRedirect(returnUrl);
188
+ } else {
189
+ window.location.href = returnUrl;
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ async function selectChurch(churchId: string) {
196
+ try {
197
+ setErrors([])
198
+ selectedChurchId = churchId;
199
+ setCookie("lastChurchId", churchId, { path: "/" });
200
+ if (!ArrayHelper.getOne(UserHelper.userChurches, "church.id", churchId)) {
201
+ const userChurch: LoginUserChurchInterface = await ApiHelper.post("/churches/select", { churchId: churchId }, "MembershipApi");
202
+ UserHelper.setupApiHelper(userChurch);
203
+
204
+ //create/claim the person record and relogin
205
+ await ApiHelper.get("/people/claim/" + churchId, "MembershipApi");
206
+ login({ jwt: userJwt || userJwtBackup });
207
+ return;
208
+ }
209
+ UserHelper.selectChurch(props.context, churchId, null).then(() => { continueLoginProcess() });
210
+ } catch (err) {
211
+ console.log("Error in selecting church: ", err)
212
+ setErrors([Locale.label("login.validate.selectingChurch")])
213
+ loginFormRef?.current?.setSubmitting(false);
214
+ }
215
+
216
+ }
217
+
218
+ const handleLoginErrors = (errors: string[]) => {
219
+ setWelcomeBackName("");
220
+ console.log(errors);
221
+ setErrors([Locale.label("login.validate.invalid")]);
222
+ }
223
+
224
+ const login = async (data: any) => {
225
+ setErrors([])
226
+ setIsSubmitting(true);
227
+ try {
228
+ const resp: LoginResponseInterface = await ApiHelper.postAnonymous("/users/login", data, "MembershipApi");
229
+ setIsSubmitting(false);
230
+ handleLoginSuccess(resp);
231
+ } catch (e: any) {
232
+ setPendingAutoLogin(false);
233
+ if (!data.jwt) handleLoginErrors([e.toString()]);
234
+ else setWelcomeBackName("");
235
+ setIsSubmitting(false);
236
+ }
237
+ };
238
+
239
+ const getWelcomeBack = () => { if (welcomeBackName !== "") return (<><Alert severity="info"><div dangerouslySetInnerHTML={{ __html: Locale.label("login.welcomeName")?.replace("{}", welcomeBackName) }} /></Alert><Loading /></>); }
240
+ const getCheckEmail = () => { if (new URLSearchParams(location?.search).get("checkEmail") === "1") return <Alert severity="info">{Locale.label("login.registerThankYou")}</Alert> }
241
+ const handleRegisterCallback = () => { setShowForgot(false); setShowRegister(true); }
242
+ const handleLoginCallback = () => { setShowForgot(false); setShowRegister(false); }
243
+ const handleChurchRegistered = (church: ChurchInterface) => { registeredChurch = church; setShowRegister(false); console.log("Updated VERSION********") }
244
+
245
+ const getInputBox = () => {
246
+ if (showRegister) return (
247
+ <Box id="loginBox" sx={{ backgroundColor: "#FFF", border: "1px solid #CCC", borderRadius: "5px", padding: "20px" }}>
248
+ <Typography component="h2" sx={{ fontSize: "32px", fontWeight: 500, lineHeight: 1.2, margin: "0 0 8px 0" }}>{Locale.label("login.createAccount")}</Typography>
249
+ <Register updateErrors={setErrors} appName={props.appName} appUrl={cleanAppUrl()} loginCallback={handleLoginCallback} userRegisteredCallback={props.userRegisteredCallback} />
250
+ </Box>
251
+ );
252
+ else if (showForgot) return (<Forgot registerCallback={handleRegisterCallback} loginCallback={handleLoginCallback} />);
253
+ else if (props.auth) return (<LoginSetPassword setErrors={setErrors} setShowForgot={setShowForgot} isSubmitting={isSubmitting} auth={props.auth} login={login} appName={props.appName} appUrl={cleanAppUrl()} />)
254
+ else return <Login setShowRegister={setShowRegister} setShowForgot={setShowForgot} setErrors={setErrors} isSubmitting={isSubmitting} login={login} mainContainerCssProps={loginContainerCssProps} defaultEmail={props.defaultEmail} defaultPassword={props.defaultPassword} />;
255
+ }
256
+
257
+ React.useEffect(init, []); //eslint-disable-line
258
+
259
+ return (
260
+ <>
261
+ <ErrorMessages errors={errors} />
262
+ {getWelcomeBack()}
263
+ {getCheckEmail()}
264
+ {!pendingAutoLogin && getInputBox()}
265
+ <SelectChurchModal show={showSelectModal} userChurches={loginResponse?.userChurches} selectChurch={selectChurch} registeredChurchCallback={handleChurchRegistered} errors={errors} appName={props.appName} />
266
+ <FloatingSupport appName={props.appName} />
267
+ </>
268
+ );
269
+
270
+ };
271
+
272
+ export const LoginPage: React.FC<Props> = (props) => {
273
+ // Try to detect if CookiesProvider is available
274
+ const CookiesContext = React.createContext(null);
275
+ const context = React.useContext(CookiesContext);
276
+
277
+ // Always wrap with CookiesProvider to ensure context is available
278
+ return (
279
+ <CookiesProvider defaultSetOptions={{ path: '/' }}>
280
+ <LoginPageContent {...props} />
281
+ </CookiesProvider>
282
+ );
283
+ };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useCookies, CookiesProvider } from "react-cookie"
5
+ import { ApiHelper, UserContextInterface } from "@churchapps/helpers";
6
+
7
+ interface Props { context?: UserContextInterface, handleRedirect?: (url: string) => void }
8
+
9
+ const LogoutPageContent: React.FC<Props> = (props) => {
10
+ const [, , removeCookie] = useCookies(["jwt", "email", "name", "lastChurchId"]);
11
+
12
+ removeCookie("jwt");
13
+ removeCookie("email");
14
+ removeCookie("name");
15
+ removeCookie("lastChurchId");
16
+
17
+ ApiHelper.clearPermissions();
18
+ props.context?.setUser(null);
19
+ props.context?.setPerson(null);
20
+ props.context?.setUserChurches(null);
21
+ props.context?.setUserChurch(null);
22
+
23
+ setTimeout(() => {
24
+ // a must check for Nextjs
25
+ if (typeof window !== "undefined") {
26
+ // Use handleRedirect function if available, otherwise fallback to window.location
27
+ if (props.handleRedirect) {
28
+ props.handleRedirect("/");
29
+ } else {
30
+ window.location.href = "/";
31
+ }
32
+ }
33
+ }, 300);
34
+ return null;
35
+ }
36
+
37
+ export const LogoutPage: React.FC<Props> = (props) => {
38
+ return (
39
+ <CookiesProvider defaultSetOptions={{ path: '/' }}>
40
+ <LogoutPageContent {...props} />
41
+ </CookiesProvider>
42
+ );
43
+ }
@@ -0,0 +1,247 @@
1
+ "use client";
2
+
3
+ import React, { FormEventHandler } from "react";
4
+ import { ApiHelper } from "@churchapps/helpers";
5
+ import { Locale } from "../helpers";
6
+ import { ResetPasswordRequestInterface, ResetPasswordResponseInterface } from "@churchapps/helpers";
7
+ import { TextField, Typography, Card, CardContent, Button } from "@mui/material";
8
+
9
+ interface Props {
10
+ registerCallback: () => void,
11
+ loginCallback: () => void
12
+ }
13
+
14
+ export const Forgot: React.FC<Props> = props => {
15
+ const [errors, setErrors] = React.useState([]);
16
+ const [successMessage, setSuccessMessage] = React.useState<React.ReactElement>(null);
17
+ const [email, setEmail] = React.useState("");
18
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
19
+
20
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
21
+ setEmail(e.target.value);
22
+ }
23
+
24
+ const validateEmail = (email: string) => (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(email))
25
+
26
+ const validate = () => {
27
+ const result = [];
28
+ if (!email) result.push(Locale.label("login.validate.email"));
29
+ else if (!validateEmail(email)) result.push(Locale.label("login.validate.email"));
30
+ setErrors(result);
31
+ return result.length === 0;
32
+ }
33
+
34
+ const reset: FormEventHandler = (e) => {
35
+ e.preventDefault();
36
+ if (validate()) {
37
+ setIsSubmitting(true);
38
+ let req: ResetPasswordRequestInterface = { userEmail: email };
39
+ ApiHelper.postAnonymous("/users/forgot", req, "MembershipApi").then((resp: ResetPasswordResponseInterface) => {
40
+ if (resp.emailed) {
41
+ setErrors([]);
42
+ setSuccessMessage(
43
+ <Typography textAlign="center" marginTop="35px">
44
+ {Locale.label("login.resetSent")} <br /><br />
45
+ <button
46
+ style={{
47
+ background: 'none',
48
+ border: 'none',
49
+ color: '#3b82f6',
50
+ fontSize: '14px',
51
+ cursor: 'pointer',
52
+ textDecoration: 'none'
53
+ }}
54
+ onMouseOver={(e) => e.currentTarget.style.textDecoration = 'underline'}
55
+ onMouseOut={(e) => e.currentTarget.style.textDecoration = 'none'}
56
+ onClick={(e) => { e.preventDefault(); props.loginCallback(); }}
57
+ >
58
+ {Locale.label("login.goLogin")}
59
+ </button>
60
+ </Typography>
61
+ );
62
+ setEmail("");
63
+ } else {
64
+ setErrors(["We could not find an account with this email address"]);
65
+ setSuccessMessage(<></>);
66
+ }
67
+ }).finally(() => { setIsSubmitting(false); });
68
+ }
69
+ }
70
+
71
+ return (
72
+ <div style={{ minHeight: '100vh', backgroundColor: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '16px' }}>
73
+ <Card sx={{
74
+ width: '100%',
75
+ maxWidth: { xs: '400px', sm: '500px' },
76
+ backgroundColor: 'white',
77
+ border: '1px solid #e5e7eb',
78
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)'
79
+ }}>
80
+ <CardContent sx={{ textAlign: 'center', padding: '32px' }}>
81
+ <div style={{ marginBottom: '32px' }}>
82
+ <img
83
+ src="/images/logo-login.png"
84
+ alt="Church Logo"
85
+ style={{
86
+ maxWidth: '100%',
87
+ width: 'auto',
88
+ height: 'auto',
89
+ maxHeight: '80px',
90
+ marginBottom: '16px',
91
+ objectFit: 'contain'
92
+ }}
93
+ />
94
+ </div>
95
+ <Typography
96
+ component="h1"
97
+ sx={{
98
+ fontSize: '24px',
99
+ fontWeight: 'bold',
100
+ color: '#111827',
101
+ marginBottom: '8px'
102
+ }}
103
+ >
104
+ {Locale.label("login.resetPassword")}
105
+ </Typography>
106
+ <Typography
107
+ sx={{
108
+ color: '#6b7280',
109
+ marginBottom: '32px'
110
+ }}
111
+ >
112
+ Enter your email to receive password reset instructions
113
+ </Typography>
114
+
115
+ <form onSubmit={reset} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
116
+ {errors.length > 0 && (
117
+ <div style={{
118
+ backgroundColor: '#fef2f2',
119
+ border: '1px solid #fecaca',
120
+ borderRadius: '6px',
121
+ padding: '12px',
122
+ textAlign: 'left'
123
+ }}>
124
+ {errors.map((error, index) => (
125
+ <div key={index} style={{ color: '#dc2626', fontSize: '14px' }}>{error}</div>
126
+ ))}
127
+ </div>
128
+ )}
129
+
130
+ {successMessage ? (
131
+ <div style={{ textAlign: 'center', display: 'flex', flexDirection: 'column', gap: '16px' }}>
132
+ {successMessage}
133
+ </div>
134
+ ) : (
135
+ <>
136
+ <Typography variant="body2" sx={{ color: '#6b7280', fontSize: '14px', textAlign: 'left' }}>
137
+ {Locale.label("login.resetInstructions")}
138
+ </Typography>
139
+
140
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
141
+ <label htmlFor="forgot-email" style={{ fontSize: '14px', fontWeight: 500, color: '#374151', textAlign: 'left' }}>
142
+ {Locale.label("login.email")}
143
+ </label>
144
+ <TextField
145
+ id="forgot-email"
146
+ name="forgot-email"
147
+ type="email"
148
+ placeholder={Locale.label("login.email")}
149
+ value={email}
150
+ onChange={handleChange}
151
+ autoFocus
152
+ required
153
+ autoComplete="email"
154
+ variant="outlined"
155
+ fullWidth
156
+ onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => e.key === "Enter" && reset}
157
+ sx={{
158
+ '& .MuiOutlinedInput-root': {
159
+ backgroundColor: 'white',
160
+ '& fieldset': {
161
+ borderColor: '#d1d5db'
162
+ },
163
+ '&:hover fieldset': {
164
+ borderColor: '#d1d5db'
165
+ },
166
+ '&.Mui-focused fieldset': {
167
+ borderColor: '#3b82f6'
168
+ },
169
+ '& input': {
170
+ color: '#111827',
171
+ fontSize: '16px'
172
+ }
173
+ },
174
+ '& .MuiInputLabel-root': {
175
+ display: 'none'
176
+ }
177
+ }}
178
+ />
179
+ </div>
180
+
181
+ <Button
182
+ type="submit"
183
+ variant="contained"
184
+ fullWidth
185
+ disabled={isSubmitting}
186
+ sx={{
187
+ backgroundColor: 'hsl(218, 85%, 55%)',
188
+ color: 'white',
189
+ padding: '12px',
190
+ textTransform: 'none',
191
+ fontSize: '16px',
192
+ fontWeight: 500,
193
+ borderRadius: '6px',
194
+ '&:hover': {
195
+ backgroundColor: 'hsl(218, 85%, 50%)'
196
+ },
197
+ '&:disabled': {
198
+ backgroundColor: '#9ca3af'
199
+ }
200
+ }}
201
+ >
202
+ {isSubmitting ? "Sending..." : Locale.label("login.reset")}
203
+ </Button>
204
+
205
+ <div style={{ textAlign: 'center', display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }}>
206
+ <button
207
+ type="button"
208
+ onClick={(e) => { e.preventDefault(); props.registerCallback(); }}
209
+ style={{
210
+ background: 'none',
211
+ border: 'none',
212
+ color: '#3b82f6',
213
+ fontSize: '14px',
214
+ cursor: 'pointer',
215
+ textDecoration: 'none'
216
+ }}
217
+ onMouseOver={(e) => e.currentTarget.style.textDecoration = 'underline'}
218
+ onMouseOut={(e) => e.currentTarget.style.textDecoration = 'none'}
219
+ >
220
+ {Locale.label("login.register")}
221
+ </button>
222
+ <span style={{ fontSize: '14px', color: '#6b7280' }}>|</span>
223
+ <button
224
+ type="button"
225
+ onClick={(e) => { e.preventDefault(); props.loginCallback(); }}
226
+ style={{
227
+ background: 'none',
228
+ border: 'none',
229
+ color: '#3b82f6',
230
+ fontSize: '14px',
231
+ cursor: 'pointer',
232
+ textDecoration: 'none'
233
+ }}
234
+ onMouseOver={(e) => e.currentTarget.style.textDecoration = 'underline'}
235
+ onMouseOut={(e) => e.currentTarget.style.textDecoration = 'none'}
236
+ >
237
+ {Locale.label("login.login")}
238
+ </button>
239
+ </div>
240
+ </>
241
+ )}
242
+ </form>
243
+ </CardContent>
244
+ </Card>
245
+ </div>
246
+ );
247
+ }