@churchapps/apphelper-login 0.5.0 → 0.5.5

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.
@@ -1,298 +1,298 @@
1
- "use client";
2
-
3
- import React from "react";
4
- import { IconButton, InputAdornment, TextField, Typography, Card, CardContent, Button } from "@mui/material";
5
- import { Visibility, VisibilityOff } from "@mui/icons-material";
6
- import { LoginResponseInterface, UserInterface } from "@churchapps/helpers";
7
- import { ApiHelper } from "@churchapps/helpers";
8
- import { Locale } from "../helpers";
9
-
10
- interface Props {
11
- appName: string,
12
- appUrl: string,
13
- setErrors: (errors: string[]) => void,
14
- setShowForgot: (showForgot: boolean) => void,
15
- isSubmitting: boolean,
16
- auth: string,
17
- login: (data: any) => void,
18
- }
19
-
20
- export const LoginSetPassword: React.FC<Props> = props => {
21
- const [password, setPassword] = React.useState("");
22
- const [verifyPassword, setVerifyPassword] = React.useState("");
23
- const [user, setUser] = React.useState<UserInterface>(null);
24
- const [showPassword, setShowPassword] = React.useState(false);
25
- const [linkExpired, setLinkExpired] = React.useState(false);
26
-
27
- const validate = () => {
28
- const result = [];
29
- if (!password) result.push(Locale.label("login.validate.password"));
30
- else if (password.length < 8) result.push(Locale.label("login.validate.passwordLength"));
31
- else if (password !== verifyPassword) result.push(Locale.label("login.validate.passwordMatch"));
32
- props.setErrors(result);
33
- return result.length === 0;
34
- }
35
-
36
- const submitChangePassword = () => {
37
- if (linkExpired) {
38
- window.open("/login", "_blank");
39
- } else if (validate()) {
40
- submit();
41
- }
42
- }
43
-
44
- const loadUser = () => {
45
- ApiHelper.postAnonymous("/users/login", { authGuid: props.auth }, "MembershipApi").then((resp: LoginResponseInterface) => {
46
-
47
- console.log("RESPONSE", resp);
48
- if (resp.user) setUser(resp.user);
49
- else props.setShowForgot(true);
50
- }).catch(() => {
51
- props.setShowForgot(true);
52
- });
53
- }
54
-
55
- const submit = async () => {
56
- const resp = await ApiHelper.postAnonymous("/users/setPasswordGuid", { authGuid: props.auth, newPassword: password, appName: props.appName, appUrl: props.appUrl }, "MembershipApi");
57
- if (resp.success) props.login({ email: user.email, password });
58
- else props.setShowForgot(true);
59
- }
60
-
61
- React.useEffect(() => {
62
- //Get the timestamp from the URL
63
- const urlParams = new URLSearchParams(window.location.search);
64
- const timestampParam = urlParams.get("timestamp");
65
- if (timestampParam) {
66
- const linkTimestamp = parseInt(timestampParam, 10);
67
- const currentTime = Date.now();
68
-
69
- //Check if the link is expired (2 min)
70
- if (currentTime - linkTimestamp > 600000) {
71
- setLinkExpired(true);
72
- } else {
73
- loadUser();
74
- }
75
- } else {
76
- setLinkExpired(true); //No timestamp means link is invalid
77
- }
78
- }, []);
79
-
80
- return (
81
- <div style={{ minHeight: '100vh', backgroundColor: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '16px' }}>
82
- <Card sx={{
83
- width: '100%',
84
- maxWidth: { xs: '400px', sm: '500px' },
85
- backgroundColor: 'white',
86
- border: '1px solid #e5e7eb',
87
- boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)'
88
- }}>
89
- <CardContent sx={{ textAlign: 'center', padding: '32px' }}>
90
- <div style={{ marginBottom: '32px' }}>
91
- <img
92
- src="/images/logo-login.png"
93
- alt="Church Logo"
94
- style={{
95
- maxWidth: '100%',
96
- width: 'auto',
97
- height: 'auto',
98
- maxHeight: '80px',
99
- marginBottom: '16px',
100
- objectFit: 'contain'
101
- }}
102
- />
103
- </div>
104
- <Typography
105
- component="h1"
106
- sx={{
107
- fontSize: '24px',
108
- fontWeight: 'bold',
109
- color: '#111827',
110
- marginBottom: '8px'
111
- }}
112
- >
113
- {Locale.label("login.setPassword")}
114
- </Typography>
115
- <Typography
116
- sx={{
117
- color: '#6b7280',
118
- marginBottom: '32px'
119
- }}
120
- >
121
- {linkExpired ? 'Your link has expired' : `Welcome back ${user?.firstName || ''}. Please set your password.`}
122
- </Typography>
123
-
124
- {linkExpired ? (
125
- <div style={{ textAlign: 'center', display: 'flex', flexDirection: 'column', gap: '16px' }}>
126
- <Typography sx={{ color: '#dc2626', marginBottom: '16px' }}>
127
- {Locale.label("login.expiredLink")}
128
- </Typography>
129
- <Button
130
- variant="contained"
131
- fullWidth
132
- onClick={submitChangePassword}
133
- disabled={props.isSubmitting}
134
- sx={{
135
- backgroundColor: 'hsl(218, 85%, 55%)',
136
- color: 'white',
137
- padding: '12px',
138
- textTransform: 'none',
139
- fontSize: '16px',
140
- fontWeight: 500,
141
- borderRadius: '6px',
142
- '&:hover': {
143
- backgroundColor: 'hsl(218, 85%, 50%)'
144
- },
145
- '&:disabled': {
146
- backgroundColor: '#9ca3af'
147
- }
148
- }}
149
- >
150
- {Locale.label("login.requestLink")}
151
- </Button>
152
- </div>
153
- ) : (
154
- <form onSubmit={(e) => { e.preventDefault(); submitChangePassword(); }} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
155
- {user && (
156
- <Typography sx={{ color: '#6b7280', textAlign: 'center', marginBottom: '8px' }}>
157
- {Locale.label("login.welcomeBack")} {user.firstName}.
158
- </Typography>
159
- )}
160
-
161
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
162
- <label htmlFor="new-password" style={{ fontSize: '14px', fontWeight: 500, color: '#374151', textAlign: 'left' }}>
163
- {Locale.label("login.setPassword")}
164
- </label>
165
- <TextField
166
- id="new-password"
167
- name="new-password"
168
- type={showPassword ? "text" : "password"}
169
- placeholder={Locale.label("login.setPassword")}
170
- value={password}
171
- onChange={(e) => { e.preventDefault(); setPassword(e.target.value) }}
172
- required
173
- autoComplete="new-password"
174
- variant="outlined"
175
- fullWidth
176
- InputProps={{
177
- endAdornment: (
178
- <InputAdornment position="end">
179
- <IconButton
180
- aria-label="toggle password visibility"
181
- onClick={() => setShowPassword(!showPassword)}
182
- edge="end"
183
- sx={{ color: '#6b7280' }}
184
- >
185
- {showPassword ? <VisibilityOff /> : <Visibility />}
186
- </IconButton>
187
- </InputAdornment>
188
- )
189
- }}
190
- sx={{
191
- '& .MuiOutlinedInput-root': {
192
- backgroundColor: 'white',
193
- paddingRight: '10px',
194
- '& fieldset': {
195
- borderColor: '#d1d5db'
196
- },
197
- '&:hover fieldset': {
198
- borderColor: '#d1d5db'
199
- },
200
- '&.Mui-focused fieldset': {
201
- borderColor: '#3b82f6'
202
- },
203
- '& input': {
204
- color: '#111827',
205
- fontSize: '16px'
206
- }
207
- },
208
- '& .MuiInputLabel-root': {
209
- display: 'none'
210
- }
211
- }}
212
- />
213
- </div>
214
-
215
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
216
- <label htmlFor="verify-password" style={{ fontSize: '14px', fontWeight: 500, color: '#374151', textAlign: 'left' }}>
217
- {Locale.label("login.verifyPassword")}
218
- </label>
219
- <TextField
220
- id="verify-password"
221
- name="verify-password"
222
- type={showPassword ? "text" : "password"}
223
- placeholder={Locale.label("login.verifyPassword")}
224
- value={verifyPassword}
225
- onChange={(e) => { e.preventDefault(); setVerifyPassword(e.target.value) }}
226
- required
227
- autoComplete="new-password"
228
- variant="outlined"
229
- fullWidth
230
- InputProps={{
231
- endAdornment: (
232
- <InputAdornment position="end">
233
- <IconButton
234
- aria-label="toggle password visibility"
235
- onClick={() => setShowPassword(!showPassword)}
236
- edge="end"
237
- sx={{ color: '#6b7280' }}
238
- >
239
- {showPassword ? <VisibilityOff /> : <Visibility />}
240
- </IconButton>
241
- </InputAdornment>
242
- )
243
- }}
244
- sx={{
245
- '& .MuiOutlinedInput-root': {
246
- backgroundColor: 'white',
247
- paddingRight: '10px',
248
- '& fieldset': {
249
- borderColor: '#d1d5db'
250
- },
251
- '&:hover fieldset': {
252
- borderColor: '#d1d5db'
253
- },
254
- '&.Mui-focused fieldset': {
255
- borderColor: '#3b82f6'
256
- },
257
- '& input': {
258
- color: '#111827',
259
- fontSize: '16px'
260
- }
261
- },
262
- '& .MuiInputLabel-root': {
263
- display: 'none'
264
- }
265
- }}
266
- />
267
- </div>
268
-
269
- <Button
270
- type="submit"
271
- variant="contained"
272
- fullWidth
273
- disabled={props.isSubmitting || !user}
274
- sx={{
275
- backgroundColor: 'hsl(218, 85%, 55%)',
276
- color: 'white',
277
- padding: '12px',
278
- textTransform: 'none',
279
- fontSize: '16px',
280
- fontWeight: 500,
281
- borderRadius: '6px',
282
- '&:hover': {
283
- backgroundColor: 'hsl(218, 85%, 50%)'
284
- },
285
- '&:disabled': {
286
- backgroundColor: '#9ca3af'
287
- }
288
- }}
289
- >
290
- {(props.isSubmitting || !user) ? Locale.label("common.pleaseWait") : Locale.label("login.signIn")}
291
- </Button>
292
- </form>
293
- )}
294
- </CardContent>
295
- </Card>
296
- </div>
297
- );
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { IconButton, InputAdornment, TextField, Typography, Card, CardContent, Button } from "@mui/material";
5
+ import { Visibility, VisibilityOff } from "@mui/icons-material";
6
+ import { LoginResponseInterface, UserInterface } from "@churchapps/helpers";
7
+ import { ApiHelper } from "@churchapps/helpers";
8
+ import { Locale } from "../helpers";
9
+
10
+ interface Props {
11
+ appName: string,
12
+ appUrl: string,
13
+ setErrors: (errors: string[]) => void,
14
+ setShowForgot: (showForgot: boolean) => void,
15
+ isSubmitting: boolean,
16
+ auth: string,
17
+ login: (data: any) => void,
18
+ }
19
+
20
+ export const LoginSetPassword: React.FC<Props> = props => {
21
+ const [password, setPassword] = React.useState("");
22
+ const [verifyPassword, setVerifyPassword] = React.useState("");
23
+ const [user, setUser] = React.useState<UserInterface>(null);
24
+ const [showPassword, setShowPassword] = React.useState(false);
25
+ const [linkExpired, setLinkExpired] = React.useState(false);
26
+
27
+ const validate = () => {
28
+ const result = [];
29
+ if (!password) result.push(Locale.label("login.validate.password"));
30
+ else if (password.length < 8) result.push(Locale.label("login.validate.passwordLength"));
31
+ else if (password !== verifyPassword) result.push(Locale.label("login.validate.passwordMatch"));
32
+ props.setErrors(result);
33
+ return result.length === 0;
34
+ }
35
+
36
+ const submitChangePassword = () => {
37
+ if (linkExpired) {
38
+ window.open("/login", "_blank");
39
+ } else if (validate()) {
40
+ submit();
41
+ }
42
+ }
43
+
44
+ const loadUser = () => {
45
+ ApiHelper.postAnonymous("/users/login", { authGuid: props.auth }, "MembershipApi").then((resp: LoginResponseInterface) => {
46
+
47
+ console.log("RESPONSE", resp);
48
+ if (resp.user) setUser(resp.user);
49
+ else props.setShowForgot(true);
50
+ }).catch(() => {
51
+ props.setShowForgot(true);
52
+ });
53
+ }
54
+
55
+ const submit = async () => {
56
+ const resp = await ApiHelper.postAnonymous("/users/setPasswordGuid", { authGuid: props.auth, newPassword: password, appName: props.appName, appUrl: props.appUrl }, "MembershipApi");
57
+ if (resp.success) props.login({ email: user.email, password });
58
+ else props.setShowForgot(true);
59
+ }
60
+
61
+ React.useEffect(() => {
62
+ //Get the timestamp from the URL
63
+ const urlParams = new URLSearchParams(window.location.search);
64
+ const timestampParam = urlParams.get("timestamp");
65
+ if (timestampParam) {
66
+ const linkTimestamp = parseInt(timestampParam, 10);
67
+ const currentTime = Date.now();
68
+
69
+ //Check if the link is expired (2 min)
70
+ if (currentTime - linkTimestamp > 600000) {
71
+ setLinkExpired(true);
72
+ } else {
73
+ loadUser();
74
+ }
75
+ } else {
76
+ setLinkExpired(true); //No timestamp means link is invalid
77
+ }
78
+ }, []);
79
+
80
+ return (
81
+ <div style={{ minHeight: '100vh', backgroundColor: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '16px' }}>
82
+ <Card sx={{
83
+ width: '100%',
84
+ maxWidth: { xs: '400px', sm: '500px' },
85
+ backgroundColor: 'white',
86
+ border: '1px solid #e5e7eb',
87
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)'
88
+ }}>
89
+ <CardContent sx={{ textAlign: 'center', padding: '32px' }}>
90
+ <div style={{ marginBottom: '32px' }}>
91
+ <img
92
+ src="/images/logo-login.png"
93
+ alt="Church Logo"
94
+ style={{
95
+ maxWidth: '100%',
96
+ width: 'auto',
97
+ height: 'auto',
98
+ maxHeight: '80px',
99
+ marginBottom: '16px',
100
+ objectFit: 'contain'
101
+ }}
102
+ />
103
+ </div>
104
+ <Typography
105
+ component="h1"
106
+ sx={{
107
+ fontSize: '24px',
108
+ fontWeight: 'bold',
109
+ color: '#111827',
110
+ marginBottom: '8px'
111
+ }}
112
+ >
113
+ {Locale.label("login.setPassword")}
114
+ </Typography>
115
+ <Typography
116
+ sx={{
117
+ color: '#6b7280',
118
+ marginBottom: '32px'
119
+ }}
120
+ >
121
+ {linkExpired ? 'Your link has expired' : `Welcome back ${user?.firstName || ''}. Please set your password.`}
122
+ </Typography>
123
+
124
+ {linkExpired ? (
125
+ <div style={{ textAlign: 'center', display: 'flex', flexDirection: 'column', gap: '16px' }}>
126
+ <Typography sx={{ color: '#dc2626', marginBottom: '16px' }}>
127
+ {Locale.label("login.expiredLink")}
128
+ </Typography>
129
+ <Button
130
+ variant="contained"
131
+ fullWidth
132
+ onClick={submitChangePassword}
133
+ disabled={props.isSubmitting}
134
+ sx={{
135
+ backgroundColor: 'hsl(218, 85%, 55%)',
136
+ color: 'white',
137
+ padding: '12px',
138
+ textTransform: 'none',
139
+ fontSize: '16px',
140
+ fontWeight: 500,
141
+ borderRadius: '6px',
142
+ '&:hover': {
143
+ backgroundColor: 'hsl(218, 85%, 50%)'
144
+ },
145
+ '&:disabled': {
146
+ backgroundColor: '#9ca3af'
147
+ }
148
+ }}
149
+ >
150
+ {Locale.label("login.requestLink")}
151
+ </Button>
152
+ </div>
153
+ ) : (
154
+ <form onSubmit={(e) => { e.preventDefault(); submitChangePassword(); }} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
155
+ {user && (
156
+ <Typography sx={{ color: '#6b7280', textAlign: 'center', marginBottom: '8px' }}>
157
+ {Locale.label("login.welcomeBack")} {user.firstName}.
158
+ </Typography>
159
+ )}
160
+
161
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
162
+ <label htmlFor="new-password" style={{ fontSize: '14px', fontWeight: 500, color: '#374151', textAlign: 'left' }}>
163
+ {Locale.label("login.setPassword")}
164
+ </label>
165
+ <TextField
166
+ id="new-password"
167
+ name="new-password"
168
+ type={showPassword ? "text" : "password"}
169
+ placeholder={Locale.label("login.setPassword")}
170
+ value={password}
171
+ onChange={(e) => { e.preventDefault(); setPassword(e.target.value) }}
172
+ required
173
+ autoComplete="new-password"
174
+ variant="outlined"
175
+ fullWidth
176
+ InputProps={{
177
+ endAdornment: (
178
+ <InputAdornment position="end">
179
+ <IconButton
180
+ aria-label="toggle password visibility"
181
+ onClick={() => setShowPassword(!showPassword)}
182
+ edge="end"
183
+ sx={{ color: '#6b7280' }}
184
+ >
185
+ {showPassword ? <VisibilityOff /> : <Visibility />}
186
+ </IconButton>
187
+ </InputAdornment>
188
+ )
189
+ }}
190
+ sx={{
191
+ '& .MuiOutlinedInput-root': {
192
+ backgroundColor: 'white',
193
+ paddingRight: '10px',
194
+ '& fieldset': {
195
+ borderColor: '#d1d5db'
196
+ },
197
+ '&:hover fieldset': {
198
+ borderColor: '#d1d5db'
199
+ },
200
+ '&.Mui-focused fieldset': {
201
+ borderColor: '#3b82f6'
202
+ },
203
+ '& input': {
204
+ color: '#111827',
205
+ fontSize: '16px'
206
+ }
207
+ },
208
+ '& .MuiInputLabel-root': {
209
+ display: 'none'
210
+ }
211
+ }}
212
+ />
213
+ </div>
214
+
215
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
216
+ <label htmlFor="verify-password" style={{ fontSize: '14px', fontWeight: 500, color: '#374151', textAlign: 'left' }}>
217
+ {Locale.label("login.verifyPassword")}
218
+ </label>
219
+ <TextField
220
+ id="verify-password"
221
+ name="verify-password"
222
+ type={showPassword ? "text" : "password"}
223
+ placeholder={Locale.label("login.verifyPassword")}
224
+ value={verifyPassword}
225
+ onChange={(e) => { e.preventDefault(); setVerifyPassword(e.target.value) }}
226
+ required
227
+ autoComplete="new-password"
228
+ variant="outlined"
229
+ fullWidth
230
+ InputProps={{
231
+ endAdornment: (
232
+ <InputAdornment position="end">
233
+ <IconButton
234
+ aria-label="toggle password visibility"
235
+ onClick={() => setShowPassword(!showPassword)}
236
+ edge="end"
237
+ sx={{ color: '#6b7280' }}
238
+ >
239
+ {showPassword ? <VisibilityOff /> : <Visibility />}
240
+ </IconButton>
241
+ </InputAdornment>
242
+ )
243
+ }}
244
+ sx={{
245
+ '& .MuiOutlinedInput-root': {
246
+ backgroundColor: 'white',
247
+ paddingRight: '10px',
248
+ '& fieldset': {
249
+ borderColor: '#d1d5db'
250
+ },
251
+ '&:hover fieldset': {
252
+ borderColor: '#d1d5db'
253
+ },
254
+ '&.Mui-focused fieldset': {
255
+ borderColor: '#3b82f6'
256
+ },
257
+ '& input': {
258
+ color: '#111827',
259
+ fontSize: '16px'
260
+ }
261
+ },
262
+ '& .MuiInputLabel-root': {
263
+ display: 'none'
264
+ }
265
+ }}
266
+ />
267
+ </div>
268
+
269
+ <Button
270
+ type="submit"
271
+ variant="contained"
272
+ fullWidth
273
+ disabled={props.isSubmitting || !user}
274
+ sx={{
275
+ backgroundColor: 'hsl(218, 85%, 55%)',
276
+ color: 'white',
277
+ padding: '12px',
278
+ textTransform: 'none',
279
+ fontSize: '16px',
280
+ fontWeight: 500,
281
+ borderRadius: '6px',
282
+ '&:hover': {
283
+ backgroundColor: 'hsl(218, 85%, 50%)'
284
+ },
285
+ '&:disabled': {
286
+ backgroundColor: '#9ca3af'
287
+ }
288
+ }}
289
+ >
290
+ {(props.isSubmitting || !user) ? Locale.label("common.pleaseWait") : Locale.label("login.signIn")}
291
+ </Button>
292
+ </form>
293
+ )}
294
+ </CardContent>
295
+ </Card>
296
+ </div>
297
+ );
298
298
  }