homeflowjs 1.0.77 → 1.0.79

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.
@@ -14,3 +14,13 @@ export const setBranchesPagination = (payload) => ({
14
14
  type: BranchesActionTypes.SET_BRANCHES_PAGINATION,
15
15
  payload,
16
16
  });
17
+
18
+ export const setSearchedPolygonBranchesIDs = (payload) => ({
19
+ type: BranchesActionTypes.SET_SEARCHED_POLYGON_BRANCHES_IDS,
20
+ payload,
21
+ });
22
+
23
+ export const setBranchDepartmentFilter = (payload) => ({
24
+ type: BranchesActionTypes.SET_BRANCH_DEPARTMENT_FILTER,
25
+ payload,
26
+ });
@@ -2,6 +2,8 @@ const BranchesActionTypes = {
2
2
  SET_BRANCHES: 'SET_BRANCHES',
3
3
  SET_BRANCHES_SEARCH: 'SET_BRANCHES_SEARCH',
4
4
  SET_BRANCHES_PAGINATION: 'SET_BRANCHES_PAGINATION',
5
+ SET_SEARCHED_POLYGON_BRANCHES_IDS: 'SET_SEARCHED_POLYGON_BRANCHES_IDS',
6
+ SET_BRANCH_DEPARTMENT_FILTER: 'SET_BRANCH_DEPARTMENT_FILTER',
5
7
  };
6
8
 
7
9
  export default BranchesActionTypes;
@@ -67,56 +67,67 @@ export const createUser = (payload, recaptchaData = {}) => (dispatch, getState)
67
67
  }),
68
68
  })
69
69
  .then(async (response) => {
70
- const response_status = response.status;
71
- const json = await response.json();
70
+ const responseData = await response.json();
71
+ if (response.ok) {
72
+ // load saved properties and searches from localStorage
73
+ const serializedSavedProperties = localStorage.getItem('savedProperties');
74
+ const serializedSavedSearches = localStorage.getItem('savedSearches');
72
75
 
73
- if (response_status === 422) throw json?.errors
76
+ let promises = [];
74
77
 
75
- return json
76
- })
77
- .then(() => {
78
- // load saved properties and searches from localStorage
79
- const serializedSavedProperties = localStorage.getItem('savedProperties');
80
- const serializedSavedSearches = localStorage.getItem('savedSearches');
81
-
82
- let promises = [];
83
-
84
- if (serializedSavedProperties && serializedSavedProperties !== '[]') {
85
- const localSavedProperties = JSON.parse(serializedSavedProperties);
86
- const savedPropertyIDs = localSavedProperties.map((property) => property.property_id).join(',');
87
-
88
- promises.push(fetch('/saved_properties.ljson', {
89
- method: 'POST',
90
- headers: { 'Content-Type': 'application/json' },
91
- body: JSON.stringify({ property_ids: savedPropertyIDs }),
92
- }));
93
- }
78
+ if (serializedSavedProperties && serializedSavedProperties !== '[]') {
79
+ const localSavedProperties = JSON.parse(serializedSavedProperties);
80
+ const savedPropertyIDs = localSavedProperties.map((property) => property.property_id).join(',');
94
81
 
95
- if (serializedSavedSearches) {
96
- const localSavedSearches = JSON.parse(serializedSavedSearches);
97
-
98
- localSavedSearches.forEach((search) => {
99
- promises.push(fetch(`/search?${buildQueryString(search)}`, {
82
+ promises.push(fetch('/saved_properties.ljson', {
100
83
  method: 'POST',
101
84
  headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ property_ids: savedPropertyIDs }),
102
86
  }));
87
+ }
88
+
89
+ if (serializedSavedSearches) {
90
+ const localSavedSearches = JSON.parse(serializedSavedSearches);
91
+
92
+ localSavedSearches.forEach((search) => {
93
+ promises.push(fetch(`/search?${buildQueryString(search)}`, {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ }));
97
+ });
98
+ }
99
+
100
+ Homeflow.kickEvent('user_signed_up');
101
+
102
+ const event = new CustomEvent('formSubmission', {
103
+ detail: {
104
+ name: 'user signed up',
105
+ payload,
106
+ }
103
107
  });
104
- }
105
108
 
106
- Homeflow.kickEvent('user_signed_up');
109
+ document.dispatchEvent(event);
107
110
 
108
- const event = new CustomEvent('formSubmission', {
109
- detail: {
110
- name: 'user signed up',
111
- payload,
112
- }
113
- });
111
+ Promise.all(promises).then(() => dispatch(fetchUser()));
112
+
113
+ return responseData;
114
+ }
114
115
 
115
- document.dispatchEvent(event);
116
+ const hasValidationErrors = responseData.errors && Array.isArray(responseData.errors);
117
+ if (hasValidationErrors) {
118
+ return { validationErrors: responseData.errors };
119
+ }
116
120
 
117
- return Promise.all(promises);
121
+ if (responseData.message) {
122
+ throw new Error(responseData.message);
123
+ } else {
124
+ throw new Error();
125
+ }
118
126
  })
119
- .then(() => dispatch(fetchUser()));
127
+ .catch((err) => {
128
+ console.error('Something went wrong.', err.message);
129
+ throw new Error(err instanceof SyntaxError ? '' : err.message);
130
+ });
120
131
  };
121
132
 
122
133
  export const signInUser = (payload) => (dispatch, getState) => {
@@ -0,0 +1,216 @@
1
+ import React, {
2
+ useState, useRef, useEffect, useCallback,
3
+ useMemo,
4
+ } from 'react';
5
+ import PropTypes from 'prop-types';
6
+ import { useDispatch } from 'react-redux';
7
+ import Autosuggest from 'react-autosuggest';
8
+ import debounce from 'lodash.debounce';
9
+ import { notify } from 'homeflowjs';
10
+
11
+ import { DEBOUNCE_DELAY } from '../../utils';
12
+ import branchSearchUtils from '../../utils/requests-handler';
13
+ import { setSearchedPolygonBranchesIDs } from '../../actions/branches.actions';
14
+ import LoadingIcon from '../../shared/loading-icon/loading-icon.component';
15
+
16
+ const { fetchAddressSuggestions, fetchBranchDataBySearchedAddress, ERROR_DEFAULT_MESSAGE } = branchSearchUtils;
17
+
18
+ const BranchesPolygonSearchInput = ({
19
+ placeholder,
20
+ inputProps,
21
+ channel,
22
+ isDepartmentSearch,
23
+ hiddenBranchIDs,
24
+ icon: Icon,
25
+ loadingIcon: CustomLoadingIcon,
26
+ handleInputClick,
27
+ handleSuggestionSelected,
28
+ }) => {
29
+ const dispatch = useDispatch();
30
+
31
+ const [suggestions, setSuggestions] = useState([]);
32
+ const [searchedAddress, setSearchedAddress] = useState('');
33
+ const [requestIsLoading, setRequestIsLoading] = useState(false);
34
+ const [isFieldDisabled, setIsFieldDisabled] = useState(false);
35
+ const [selectedSuggestion, setSelectedSuggestion] = useState(null);
36
+
37
+ useEffect(() => {
38
+ dispatch(setSearchedPolygonBranchesIDs(null));
39
+ }, []);
40
+
41
+ const handleSearchedBranchData = (branchesIDs) => {
42
+ const searchedBranchesIDs = branchesIDs.filter(
43
+ (branchID) => !hiddenBranchIDs.includes(branchID.toString())
44
+ );
45
+ dispatch(setSearchedPolygonBranchesIDs(searchedBranchesIDs));
46
+ };
47
+
48
+ const resetLoadingStatus = () => {
49
+ setRequestIsLoading(false);
50
+ setIsFieldDisabled(false);
51
+ }
52
+
53
+ const handleError = (err) => {
54
+ let errorData = err;
55
+ if (
56
+ errorData instanceof SyntaxError
57
+ || errorData instanceof TypeError
58
+ || errorData instanceof ReferenceError
59
+ ) {
60
+ errorData = new Error(ERROR_DEFAULT_MESSAGE);
61
+ }
62
+ notify(errorData.message, 'error')
63
+ }
64
+
65
+ const getSearchedBranch = () => {
66
+ if (channel) {
67
+ fetchBranchDataBySearchedAddress({
68
+ addressID: selectedSuggestion,
69
+ usingPolygon: true,
70
+ channel,
71
+ isDepartmentSearch,
72
+ })
73
+ .then(handleSearchedBranchData)
74
+ .catch(handleError)
75
+ .finally(resetLoadingStatus)
76
+ } else {
77
+ Promise.all([
78
+ fetchBranchDataBySearchedAddress({
79
+ addressID: selectedSuggestion,
80
+ usingPolygon: true,
81
+ channel: 'sales',
82
+ isDepartmentSearch,
83
+ }),
84
+ fetchBranchDataBySearchedAddress({
85
+ addressID: selectedSuggestion,
86
+ usingPolygon: true,
87
+ channel: 'lettings',
88
+ isDepartmentSearch,
89
+ })
90
+ ])
91
+ .then(([salesBranchIDList, lettingsBranchIDList]) => {
92
+ handleSearchedBranchData([
93
+ ...salesBranchIDList,
94
+ ...lettingsBranchIDList,
95
+ ])
96
+ })
97
+ .catch(handleError)
98
+ .finally(resetLoadingStatus);
99
+ }
100
+ }
101
+
102
+ useEffect(() => {
103
+ if (selectedSuggestion) {
104
+ setRequestIsLoading(true);
105
+ setIsFieldDisabled(true);
106
+
107
+ getSearchedBranch();
108
+ }
109
+ }, [channel, selectedSuggestion]);
110
+
111
+ const getSuggestions = useCallback(
112
+ (searchedAddress) => {
113
+ setSelectedSuggestion(null);
114
+ setRequestIsLoading(true);
115
+ fetchAddressSuggestions({ searchedAddress })
116
+ .then((suggestions) => {
117
+ setRequestIsLoading(false);
118
+ setSuggestions(suggestions)
119
+ })
120
+ .catch((err) => notify(err.message, 'error'))
121
+ }, [])
122
+
123
+ const debouncedGetSuggestions = useCallback(debounce(({ value }) => {
124
+ setSearchedAddress(value);
125
+ getSuggestions(value);
126
+ }, DEBOUNCE_DELAY), []);
127
+
128
+ const onSuggestionSelected = useCallback((_, { suggestion }) => {
129
+ if (suggestion.id) {
130
+ if (handleSuggestionSelected) {
131
+ handleSuggestionSelected();
132
+ }
133
+ setSelectedSuggestion(suggestion.id);
134
+ }
135
+ }, [channel]);
136
+
137
+ const onSuggestionsClear = useCallback(() => setSuggestions([]), []);
138
+
139
+ const getSuggestionValue = useCallback((suggestion) => suggestion.address, []);
140
+
141
+ const renderSuggestion = useCallback((suggestion) => (
142
+ <span className="react-autosuggest_span">{suggestion.address}</span>
143
+ ), []);
144
+
145
+ const onInputChange = useCallback((_, { newValue }) => {
146
+ if (!newValue) {
147
+ dispatch(setSearchedPolygonBranchesIDs(null));
148
+ }
149
+ setSearchedAddress(newValue);
150
+ setSelectedSuggestion(null);
151
+ }, []);
152
+
153
+ const onInputClick = useCallback(() => {
154
+ if (handleInputClick) {
155
+ handleInputClick();
156
+ }
157
+ }, [handleInputClick]);
158
+
159
+ const additionalInputProps = useMemo(() => ({
160
+ placeholder,
161
+ name: 'react-autosuggest__input',
162
+ id: 'react-autosuggest__input',
163
+ value: searchedAddress,
164
+ onClick: onInputClick,
165
+ onChange: onInputChange,
166
+ ...((isFieldDisabled && requestIsLoading) ? { disabled: true } : {}),
167
+ }), [searchedAddress, isFieldDisabled, requestIsLoading, onInputClick]);
168
+
169
+ const SearchLoadingIcon = CustomLoadingIcon || LoadingIcon;
170
+
171
+ return (
172
+ <>
173
+ <input type="hidden" name="location" value={searchedAddress} />
174
+ <Autosuggest
175
+ suggestions={suggestions}
176
+ onSuggestionsClearRequested={onSuggestionsClear}
177
+ onSuggestionsFetchRequested={debouncedGetSuggestions}
178
+ onSuggestionSelected={onSuggestionSelected}
179
+ getSuggestionValue={getSuggestionValue}
180
+ renderSuggestion={renderSuggestion}
181
+ inputProps={{
182
+ ...additionalInputProps,
183
+ ...inputProps,
184
+ }}
185
+ highlightFirstSuggestion
186
+ />
187
+ {requestIsLoading ? <SearchLoadingIcon /> : <Icon />}
188
+ </>
189
+ );
190
+ };
191
+
192
+ BranchesPolygonSearchInput.propTypes = {
193
+ placeholder: PropTypes.string,
194
+ inputProps: PropTypes.object,
195
+ channel: PropTypes.string,
196
+ isDepartmentSearch: PropTypes.bool,
197
+ icon: PropTypes.func,
198
+ loadingIcon: PropTypes.func,
199
+ handleInputClick: PropTypes.func,
200
+ handleSuggestionSelected: PropTypes.func,
201
+ hiddenBranchIds: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
202
+ };
203
+
204
+ BranchesPolygonSearchInput.defaultProps = {
205
+ placeholder: '',
206
+ inputProps: {},
207
+ channel: null,
208
+ isDepartmentSearch: true,
209
+ icon: () => <div/>,
210
+ loadingIcon: null,
211
+ handleInputClick: null,
212
+ handleSuggestionSelected: null,
213
+ hiddenBranchIds: [],
214
+ };
215
+
216
+ export default BranchesPolygonSearchInput;
package/branches/index.js CHANGED
@@ -3,6 +3,7 @@ import BranchMap from './branch-map/branch-map.component';
3
3
  import BranchStreetview from './branch-streetview/branch-streetview.component';
4
4
  import BranchesSearchForm from './branches-search-form/branches-search-form.component';
5
5
  import BranchesSearchInput from './branches-search-form/branches-search-input.component';
6
+ import BranchesPolygonSearchInput from './branches-search-form/branches-polygon-search-input.component';
6
7
 
7
8
  export {
8
9
  BranchesMap,
@@ -10,4 +11,5 @@ export {
10
11
  BranchStreetview,
11
12
  BranchesSearchForm,
12
13
  BranchesSearchInput,
14
+ BranchesPolygonSearchInput
13
15
  };
@@ -92,12 +92,14 @@ const useRecaptcha = ({
92
92
  }, [stateIsRecaptchaLoaded]);
93
93
 
94
94
  const onRecaptchaRender = useCallback(() => {
95
- const isRecaptchaTokenReady = formRef
95
+ const recaptchaTokenEl = formRef
96
96
  .current
97
97
  .querySelector(`[name="${RECAPTCHA_CONFIG.FILED_NAME}"]`);
98
98
 
99
- if (isRecaptchaTokenReady){
100
- return;
99
+ if (recaptchaTokenEl){
100
+ onRecaptchaSubmit({
101
+ [RECAPTCHA_CONFIG.FILED_NAME]: recaptchaTokenEl.value
102
+ })
101
103
  } else {
102
104
  if (window.grecaptcha?.render) {
103
105
  triggerRecaptchaRender();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "sideEffects": [
5
5
  "modal/**/*",
6
6
  "user/default-profile/**/*",
@@ -13,6 +13,7 @@ const INITIAL_STATE = {
13
13
  propertiesMap: true,
14
14
  googleMaps: true,
15
15
  articles: false,
16
+ userPasswordReset: false,
16
17
  },
17
18
  themePreferences: {},
18
19
  themeSettings: Homeflow.get('theme_settings_defaults') || {},
@@ -4,6 +4,8 @@ const INITIAL_STATE = {
4
4
  branches: [],
5
5
  branchesSearch: '',
6
6
  pagination: {},
7
+ searchedBranchesIDs: null,
8
+ branchDepartmentFilter: 'all',
7
9
  };
8
10
 
9
11
  const branchesReducer = (state = INITIAL_STATE, action) => {
@@ -23,6 +25,16 @@ const branchesReducer = (state = INITIAL_STATE, action) => {
23
25
  ...state,
24
26
  pagination: action.payload,
25
27
  };
28
+ case BranchesActionTypes.SET_SEARCHED_POLYGON_BRANCHES_IDS:
29
+ return {
30
+ ...state,
31
+ searchedBranchesIDs: action.payload,
32
+ };
33
+ case BranchesActionTypes.SET_BRANCH_DEPARTMENT_FILTER:
34
+ return {
35
+ ...state,
36
+ branchDepartmentFilter: action.payload,
37
+ };
26
38
  default: return state;
27
39
  }
28
40
  };
@@ -0,0 +1,303 @@
1
+ import React, { useReducer } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import notify from '../../app/notify';
5
+
6
+ const ERROR_MESSAGE = 'Something went wrong updating your password. Please try again later.';
7
+
8
+ const ChangePasswordForm = ({
9
+ classNames,
10
+ onSuccessCallback,
11
+ onErrorCallback,
12
+ submitText,
13
+ submitTextSubmitting,
14
+ Loader,
15
+ }) => {
16
+ const user = useSelector((state) => state?.user?.currentUser) || null;
17
+ if (!user) return null;
18
+
19
+ const formInitialState = {
20
+ email: user.email,
21
+ current_password: '',
22
+ new_password: '',
23
+ new_password_confirmation: '',
24
+ errors: [],
25
+ success: false,
26
+ loading: false,
27
+ };
28
+
29
+ const [formData, dispatch] = useReducer((currentData, action) => {
30
+ switch (action?.type) {
31
+ case 'UPDATE_USER_DATA':
32
+ return {
33
+ ...currentData,
34
+ [action.field]: action.payload,
35
+ };
36
+ case 'SUCCESS':
37
+ return {
38
+ ...currentData,
39
+ success: true,
40
+ loading: false,
41
+ };
42
+ case 'ERRORS':
43
+ return {
44
+ ...currentData,
45
+ errors: action.payload,
46
+ loading: false,
47
+ };
48
+ case 'LOADING':
49
+ return {
50
+ ...currentData,
51
+ loading: action.payload,
52
+ };
53
+ case 'RESET':
54
+ return formInitialState;
55
+ case 'RESET_WITH_ERROR':
56
+ return {
57
+ ...formInitialState,
58
+ errors: action.payload,
59
+ };
60
+ default:
61
+ return currentData;
62
+ }
63
+ }, formInitialState);
64
+
65
+ const changePasswordClassNames = {
66
+ formWrapperClassName:
67
+ `change-password-form
68
+ ${classNames?.formWrapperClassName
69
+ ? classNames.formWrapperClassName
70
+ : ''
71
+ }`,
72
+ formClassName:
73
+ `change-password-form__form
74
+ ${classNames?.formClassName
75
+ ? classNames.formClassName
76
+ : ''
77
+ }`,
78
+ currentPasswordClassName:
79
+ `change-password-form__current-password
80
+ ${classNames?.currentPasswordClassName
81
+ ? classNames.currentPasswordClassName
82
+ : ''
83
+ }`,
84
+ passwordClassName:
85
+ `change-password-form__password
86
+ ${classNames?.passwordClassName
87
+ ? classNames.passwordClassName
88
+ : ''
89
+ }`,
90
+ confirmPasswordClassName:
91
+ `change-password-form__confirm-password
92
+ ${classNames?.confirmPasswordClassName
93
+ ? classNames.confirmPasswordClassName
94
+ : ''
95
+ }`,
96
+ buttonClassName:
97
+ `change-password-form__button
98
+ ${classNames?.buttonClassName
99
+ ? classNames.buttonClassName
100
+ : ''
101
+ }`,
102
+ buttonLoadingClassName:
103
+ `change-password-form__button--loading
104
+ ${classNames?.buttonLoadingClassName
105
+ ? classNames.buttonLoadingClassName
106
+ : ''
107
+ }`,
108
+ errorsListClassName:
109
+ `change-password-form__errors-list
110
+ ${classNames?.errorsListClassName
111
+ ? classNames.errorsListClassName
112
+ : ''
113
+ }`,
114
+ };
115
+
116
+ function resetForm({ errors, callBack }) {
117
+ if (errors) {
118
+ dispatch({
119
+ type: 'RESET_WITH_ERROR',
120
+ payload: errors,
121
+ });
122
+ } else {
123
+ dispatch({ type: 'RESET' });
124
+ }
125
+
126
+ if (callBack) {
127
+ callBack();
128
+ }
129
+ }
130
+
131
+ function updateUser() {
132
+ fetch('/passwords', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ Authorization: `${Homeflow.get('authenticityToken')}`,
137
+ },
138
+ body: JSON.stringify({
139
+ email: user.email,
140
+ current_password: formData.current_password,
141
+ new_password: formData.new_password,
142
+ new_password_confirmation: formData.new_password_confirmation,
143
+ }),
144
+ })
145
+ .then(async (response) => {
146
+ const responseData = await response.json();
147
+ const statusCode = response.status;
148
+
149
+ if (response.ok) {
150
+ // password changed sucessfully
151
+ resetForm({ callback: onSuccessCallback });
152
+ notify('You have successfully changed your password.', 'success');
153
+ }
154
+ else if (responseData.errors && Array.isArray(responseData.errors) && statusCode === 422) {
155
+ // new validation erorrs for form from Users API
156
+ resetForm({
157
+ errors: responseData.errors,
158
+ callback: onErrorCallback,
159
+ });
160
+ }
161
+ else if (responseData.message) {
162
+ // attis just single message response
163
+ throw new Error(responseData.message);
164
+ }
165
+ else {
166
+ // generic error fallback / 500
167
+ throw new Error(ERROR_MESSAGE);
168
+ }
169
+ })
170
+ .catch((err) => {
171
+ resetForm({ callback: onErrorCallback });
172
+ const errMessage = err instanceof SyntaxError
173
+ ? ERROR_MESSAGE
174
+ : err.message || ERROR_MESSAGE;
175
+
176
+ console.error('Something went wrong.', err.message);
177
+ notify(errMessage, 'error');
178
+ });
179
+ }
180
+
181
+ function handleSubmit(e) {
182
+ e.preventDefault();
183
+
184
+ dispatch({
185
+ type: 'LOADING',
186
+ payload: true,
187
+ });
188
+ updateUser();
189
+ }
190
+
191
+ function handleInputs(e) {
192
+ const { name, value } = e.target;
193
+ dispatch({
194
+ type: 'UPDATE_USER_DATA',
195
+ field: name,
196
+ payload: value,
197
+ });
198
+ }
199
+
200
+ const loadingClassName = formData.loading ? changePasswordClassNames.buttonLoadingClassName : '';
201
+
202
+ return (
203
+ <div className={changePasswordClassNames.formWrapperClassName}>
204
+ <form
205
+ className={changePasswordClassNames.formClassName}
206
+ onSubmit={(e) => handleSubmit(e)}
207
+ >
208
+ {(formData.errors?.length > 0) && (
209
+ <ul className={changePasswordClassNames.errorsListClassName}>
210
+ {formData.errors.map((err) => <li key={err}>{err}</li>)}
211
+ </ul>
212
+ )}
213
+ <input
214
+ type="password"
215
+ onChange={(e) => handleInputs(e)}
216
+ value={formData.current_password}
217
+ className={changePasswordClassNames.currentPasswordClassName}
218
+ name="current_password"
219
+ placeholder="Current password"
220
+ required
221
+ {...(formData.loading && {
222
+ disabled: true,
223
+ })}
224
+ />
225
+ <input
226
+ type="password"
227
+ onChange={(e) => handleInputs(e)}
228
+ value={formData.new_password}
229
+ className={changePasswordClassNames.passwordClassName}
230
+ name="new_password"
231
+ placeholder="New password"
232
+ required
233
+ {...(formData.loading && {
234
+ disabled: true,
235
+ })}
236
+ />
237
+ <input
238
+ type="password"
239
+ onChange={(e) => handleInputs(e)}
240
+ value={formData.new_password_confirmation}
241
+ className={changePasswordClassNames.confirmPasswordClassName}
242
+ name="new_password_confirmation"
243
+ placeholder="Repeat new password"
244
+ required
245
+ {...(formData.loading && {
246
+ disabled: true,
247
+ })}
248
+ />
249
+ <button
250
+ className={`${changePasswordClassNames.buttonClassName} ${loadingClassName}`}
251
+ type="submit"
252
+ {...(formData.loading && {
253
+ disabled: true,
254
+ })}
255
+ >
256
+ {(Loader && formData.loading) && <Loader />}
257
+ {formData.loading ? submitTextSubmitting : submitText}
258
+ </button>
259
+ </form>
260
+ </div>
261
+ );
262
+ };
263
+
264
+ ChangePasswordForm.propTypes = {
265
+ classNames: PropTypes.shape({
266
+ formWrapperClassName: PropTypes.string,
267
+ formClassName: PropTypes.string,
268
+ currentPasswordClassName: PropTypes.string,
269
+ passwordClassName: PropTypes.string,
270
+ confirmPasswordClassName: PropTypes.string,
271
+ buttonClassName: PropTypes.string,
272
+ buttonLoadingClassName: PropTypes.string,
273
+ errorsListClassName: PropTypes.string,
274
+ }),
275
+ onSuccessCallback: PropTypes.func,
276
+ onErrorCallback: PropTypes.func,
277
+ submitText: PropTypes.string,
278
+ pattern: PropTypes.string,
279
+ patternText: PropTypes.string,
280
+ submitTextSubmitting: PropTypes.string,
281
+ Loader: PropTypes.node,
282
+ };
283
+
284
+ ChangePasswordForm.defaultProps = {
285
+ classNames: {
286
+ formWrapperClassName: '',
287
+ formClassName: '',
288
+ currentPasswordClassName: '',
289
+ passwordClassName: '',
290
+ confirmPasswordClassName: '',
291
+ buttonClassName: '',
292
+ errorsListClassName: '',
293
+ },
294
+ onSuccessCallback: null,
295
+ onErrorCallback: null,
296
+ pattern: null,
297
+ patternText: '',
298
+ submitText: 'Update',
299
+ submitTextSubmitting: 'Updating',
300
+ Loader: null,
301
+ };
302
+
303
+ export default ChangePasswordForm;
@@ -11,9 +11,11 @@ const ForgottenPasswordForm = ({
11
11
  buttonSpanClass,
12
12
  honeypot,
13
13
  honeypotClass,
14
+ Loader,
14
15
  }) => {
15
16
  const formRef = useRef(null);
16
17
  const [email, setEmail] = useState('');
18
+ const [loading, setLoading] = useState(false);
17
19
  const [success, setSuccess] = useState(false);
18
20
 
19
21
  const fallbackHoneypotStyles = {
@@ -28,6 +30,7 @@ const ForgottenPasswordForm = ({
28
30
  };
29
31
 
30
32
  const onRecaptchaSubmit = useCallback((recaptchaData) => {
33
+ setLoading(true);
31
34
  const formData = new FormData(formRef.current);
32
35
  const honeypot = formData.get("body");
33
36
 
@@ -45,6 +48,7 @@ const ForgottenPasswordForm = ({
45
48
  }
46
49
  })
47
50
  .finally(() => {
51
+ setLoading(false);
48
52
  notify(
49
53
  'If the email you have entered has a user account we will send you a forgotten password reset request now.',
50
54
  'success',
@@ -80,6 +84,8 @@ const ForgottenPasswordForm = ({
80
84
  );
81
85
  }
82
86
 
87
+ const loadingClassName = loading ? 'forgotten-password-form__submit--loading' : '';
88
+
83
89
  return (
84
90
  <form
85
91
  ref={formRef}
@@ -110,8 +116,10 @@ const ForgottenPasswordForm = ({
110
116
 
111
117
  <button
112
118
  type="submit"
113
- className={`forgotten-password-form__submit ${buttonClass}`}
119
+ className={`forgotten-password-form__submit ${buttonClass} ${loadingClassName}`}
120
+ disabled={loading}
114
121
  >
122
+ {(loading && Loader) && (<Loader />)}
115
123
  <span className={buttonSpanClass}>Reset Password</span>
116
124
  </button>
117
125
  </form>
@@ -124,6 +132,7 @@ ForgottenPasswordForm.propTypes = {
124
132
  buttonSpanClass: PropTypes.string,
125
133
  honeypot: PropTypes.bool,
126
134
  honeypotClass: PropTypes.string,
135
+ Loader: PropTypes.element,
127
136
  };
128
137
 
129
138
  ForgottenPasswordForm.defaultProps = {
@@ -132,6 +141,7 @@ ForgottenPasswordForm.defaultProps = {
132
141
  buttonSpanClass: '',
133
142
  honeypot: false,
134
143
  honeypotClass: '',
144
+ Loader: null,
135
145
  };
136
146
 
137
147
  export default ForgottenPasswordForm;
package/user/index.js CHANGED
@@ -7,6 +7,7 @@ import SignOutButton from './sign-out-button/sign-out-button.component';
7
7
  import MarketingPreferencesForm from './marketing-preferences-form/marketing-preferences-form.component';
8
8
  import ForgottenPasswordForm from './forgotten-password-form/forgotten-password-form.component';
9
9
  import ResetPasswordForm from './reset-password-form/reset-password-form.component';
10
+ import ChangePasswordForm from './change-password-form/change-password-form.component';
10
11
  import UserEditForm from './user-edit-form/user-edit-form.component';
11
12
  import UserHoneypot from './user-honeypot/user-honeypot.component';
12
13
 
@@ -21,5 +22,6 @@ export {
21
22
  MarketingPreferencesForm,
22
23
  ForgottenPasswordForm,
23
24
  ResetPasswordForm,
25
+ ChangePasswordForm,
24
26
  UserHoneypot,
25
27
  };
@@ -3,9 +3,12 @@ import { connect } from 'react-redux';
3
3
  import { useHistory } from 'react-router-dom';
4
4
  import PropTypes from 'prop-types';
5
5
 
6
+ import { setLoading } from '../../actions/app.actions';
6
7
  import { setCurrentUser } from '../../actions/user.actions';
7
8
  import notify from '../../app/notify';
8
9
 
10
+ const ERROR_MESSAGE = 'Something went wrong updating your password. Please try again later.';
11
+
9
12
  const ResetPasswordForm = ({
10
13
  user,
11
14
  setCurrentUser,
@@ -17,12 +20,17 @@ const ResetPasswordForm = ({
17
20
  redirect,
18
21
  newPasswordInputProps,
19
22
  confirmPasswordInputProps,
23
+ Loader,
24
+ setLoading,
25
+ isRequestInProgress,
20
26
  }) => {
21
27
  const [password, setPassword] = useState('');
22
28
  const [passwordConfirmation, setPasswordConfirmation] = useState('');
29
+ const [validationErrors, setValidationErrors] = useState(null);
23
30
  const history = useHistory();
24
31
 
25
32
  const handleSubmit = (e) => {
33
+ setLoading({ userPasswordReset: true });
26
34
  e.preventDefault();
27
35
 
28
36
  fetch('/user.ljson', {
@@ -36,17 +44,59 @@ const ResetPasswordForm = ({
36
44
  password,
37
45
  password_confirmation: passwordConfirmation,
38
46
  }),
39
- }).then((response) => response.json())
40
- .then(({ user }) => {
41
- setCurrentUser(user);
42
- notify('You have successfully changed your password.', 'success');
43
- history.push(redirect);
47
+ })
48
+ .then(async (response) => {
49
+ const responseData = await response.json();
50
+ const statusCode = response.status;
51
+
52
+ if (response.ok) {
53
+ // 200 - password change good
54
+ setCurrentUser(user);
55
+ notify('You have successfully changed your password.', 'success');
56
+ history.push(redirect);
57
+ }
58
+ else if (response.status === 302) {
59
+ // 302 - password chnage most likely good, may see flash message from server
60
+ const redirectUrl = response.headers.get('Location');
61
+ if (redirectUrl) {
62
+ window.location.href = redirectUrl; // old school, is below correct?
63
+ } else {
64
+ throw new Error('Received a redirect without a destination URL');
65
+ }
66
+ }
67
+ else if (responseData.errors && Array.isArray(responseData.errors) && statusCode === 422) {
68
+ // New validation errors for form from Users API
69
+ setValidationErrors(responseData.errors);
70
+ }
71
+ else if (responseData.message) {
72
+ // Attis just single message response
73
+ throw new Error(responseData.message);
74
+ }
75
+ else {
76
+ // Generic error fallback / 500
77
+ throw new Error(ERROR_MESSAGE);
78
+ }
44
79
  })
45
- .catch((err) => console.error('Something went wrong.', err));
80
+ .catch((err) => {
81
+ const errMessage = err instanceof SyntaxError
82
+ ? ERROR_MESSAGE
83
+ : err.message || ERROR_MESSAGE;
84
+
85
+ console.error('Something went wrong.', err.message);
86
+ notify(errMessage, 'error');
87
+ })
88
+ .finally(() => setLoading({ userPasswordReset: false }));
46
89
  };
47
90
 
91
+ const loadingClassName = isRequestInProgress ? 'reset-password-form__submit--loading' : '';
92
+
48
93
  return (
49
94
  <form className="reset-password-form" onSubmit={handleSubmit}>
95
+ {(validationErrors && validationErrors?.length > 0) && (
96
+ <ul className="reset-password-form__errors-list">
97
+ {validationErrors.map((err) => <li key={err}>Password {err}</li>)}
98
+ </ul>
99
+ )}
50
100
  <input
51
101
  name="password"
52
102
  type="password"
@@ -72,8 +122,10 @@ const ResetPasswordForm = ({
72
122
 
73
123
  <button
74
124
  type="submit"
75
- className={`reset-password-form__submit ${buttonClass}`}
125
+ className={`reset-password-form__submit ${buttonClass} ${loadingClassName}`}
126
+ disabled={isRequestInProgress}
76
127
  >
128
+ {(isRequestInProgress && Loader) && (<Loader />)}
77
129
  <span className={buttonSpanClass}>Reset Password</span>
78
130
  </button>
79
131
  </form>
@@ -89,6 +141,11 @@ ResetPasswordForm.propTypes = {
89
141
  pattern: PropTypes.string,
90
142
  patternTitle: PropTypes.string,
91
143
  redirect: PropTypes.string,
144
+ Loader: PropTypes.element,
145
+ newPasswordInputProps: PropTypes.object,
146
+ confirmPasswordInputProps: PropTypes.object,
147
+ setLoading: PropTypes.func.isRequired,
148
+ isRequestInProgress: PropTypes.bool.isRequired,
92
149
  };
93
150
 
94
151
  ResetPasswordForm.defaultProps = {
@@ -100,14 +157,17 @@ ResetPasswordForm.defaultProps = {
100
157
  redirect: '/user',
101
158
  newPasswordInputProps: {},
102
159
  confirmPasswordInputProps: {},
160
+ Loader: null,
103
161
  };
104
162
 
105
163
  const mapStateToProps = (state) => ({
106
164
  user: state.user.currentUser,
165
+ isRequestInProgress: state.app.loading?.userPasswordReset,
107
166
  });
108
167
 
109
168
  const mapDispatchToProps = {
110
169
  setCurrentUser,
170
+ setLoading,
111
171
  };
112
172
 
113
173
  export default connect(
@@ -7,7 +7,7 @@ import { signOutUser } from '../../actions/user.actions';
7
7
  import notify from '../../app/notify';
8
8
 
9
9
  const SignOutButton = ({
10
- signOutUser, pushHistory, reload, children, ...otherProps
10
+ signOutUser, pushHistory, signOutRedirectUrl, reload, children, ...otherProps
11
11
  }) => {
12
12
  const history = useHistory();
13
13
  const handleClick = (e) => {
@@ -16,7 +16,7 @@ const SignOutButton = ({
16
16
  .then((success) => {
17
17
  if (success) {
18
18
  notify('You have been signed out', 'success');
19
- if (pushHistory) history.push('/');
19
+ if (pushHistory) history.push(signOutRedirectUrl || '/');
20
20
  if (reload) location.reload(false);
21
21
  } else {
22
22
  notify('There was an error signing out', 'error');
@@ -43,11 +43,13 @@ SignOutButton.propTypes = {
43
43
  ]).isRequired,
44
44
  pushHistory: PropTypes.bool,
45
45
  reload: PropTypes.bool,
46
+ signOutRedirectUrl: PropTypes.string,
46
47
  };
47
48
 
48
49
  SignOutButton.defaultProps = {
49
50
  pushHistory: false,
50
51
  reload: false,
52
+ signOutRedirectUrl: null,
51
53
  };
52
54
 
53
55
  const mapDispatchToProps = {
@@ -20,10 +20,10 @@ const UserEditForm = (props) => {
20
20
  setLoading({ userUpdate: true });
21
21
  updateUser(localUser)
22
22
  .then(() => {
23
- setLoading({ userUpdate: false });
24
23
  notify('Profile successfully updated.', 'success');
25
24
  if (callback) callback();
26
- });
25
+ })
26
+ .finally(() => setLoading({ userUpdate: false }))
27
27
  };
28
28
 
29
29
  return (
@@ -7,6 +7,8 @@ import { setLocalUser, createUser } from '../../actions/user.actions';
7
7
  import { setLoading } from '../../actions/app.actions';
8
8
  import notify from '../../app/notify';
9
9
 
10
+ const ERROR_MESSAGE = 'Sorry, something went wrong. You may already have an account with this email address, if so, please try signing in instead of registering.';
11
+
10
12
  const UserRegisterForm = (props) => {
11
13
  const {
12
14
  children,
@@ -18,18 +20,14 @@ const UserRegisterForm = (props) => {
18
20
  marketingStatement,
19
21
  redirectHash,
20
22
  withRecaptcha,
23
+ isRequestInProgress,
24
+ className,
21
25
  ...otherProps
22
26
  } = props;
23
27
 
24
28
  const formRef = useRef(null);
25
29
  const [isRequestCompleted, setIsRequestCompleted] = useState(null);
26
-
27
- const validateForm = () => {
28
- if (localUser.password === localUser.password_confirmation) return true;
29
-
30
- notify('Passwords do not match', 'error'); // TODO: Flash messages
31
- return false;
32
- };
30
+ const [validationErrors, setValidationErrors] = useState(null);
33
31
 
34
32
  const onRecaptchaSubmit = (recaptchaData) => {
35
33
  const { body } = Object.fromEntries(new FormData(formRef.current));
@@ -43,30 +41,33 @@ const UserRegisterForm = (props) => {
43
41
  ...localUser.marketing_preferences, opt_in_marketing_statement: marketingStatement,
44
42
  };
45
43
 
44
+ if (userParams?.body) {
45
+ window.location.href = '/';
46
+ }
47
+
46
48
  createUser(userParams)
47
- .then(() => {
48
- setLoading({ userRegister: false });
49
- notify('You have been successfully registered!', 'success');
49
+ .then((data) => {
50
+ if (data?.validationErrors?.length > 0) {
51
+ setValidationErrors(data.validationErrors);
52
+ } else {
53
+ notify('You have been successfully registered!', 'success');
50
54
 
51
- if (redirectHash) {
52
- window.location.hash = redirectHash;
55
+ if (redirectHash) {
56
+ window.location.hash = redirectHash;
57
+ }
53
58
  }
54
59
  })
55
- .catch((errors) => {
56
- if (userParams?.body) {
57
- window.location.href = '/';
58
- } else if(errors?.password){
59
- // this could be used for any kind of errors but we only need it for password errors right now
60
- errors.password?.forEach((error) => notify(error, 'error', { duration: 8000 }))
61
- } else {
62
- notify(
63
- 'Sorry, something went wrong. You may already have an account with this email address, if so, please try signing in instead of registering.',
64
- 'error',
65
- { duration: 8000 },
66
- );
67
- }
60
+ .catch((err) => {
61
+ notify(
62
+ err?.message || ERROR_MESSAGE,
63
+ 'error',
64
+ { duration: 8000 },
65
+ );
66
+ })
67
+ .finally(() => {
68
+ setIsRequestCompleted((prevState) => !prevState);
69
+ setLoading({ userRegister: false });
68
70
  })
69
- .finally(setIsRequestCompleted((prevState) => !prevState))
70
71
  }
71
72
 
72
73
  const { onRecaptchaRender, onResetRecaptcha } = useRecaptcha({
@@ -78,11 +79,9 @@ const UserRegisterForm = (props) => {
78
79
  const handleSubmit = (e) => {
79
80
  e.preventDefault();
80
81
 
81
- if (validateForm()) {
82
- setLoading({ userRegister: true });
83
- setIsRequestCompleted(false);
84
- withRecaptcha ? onRecaptchaRender() : onRecaptchaSubmit()
85
- }
82
+ setLoading({ userRegister: true });
83
+ setIsRequestCompleted(false);
84
+ withRecaptcha ? onRecaptchaRender() : onRecaptchaSubmit()
86
85
  };
87
86
 
88
87
  useEffect(() => {
@@ -96,8 +95,14 @@ const UserRegisterForm = (props) => {
96
95
  <form
97
96
  onSubmit={handleSubmit}
98
97
  ref={formRef}
98
+ className={`registration-form ${className ? className : ''}}`}
99
99
  {...otherProps}
100
100
  >
101
+ {(validationErrors && validationErrors?.length > 0) && (
102
+ <ul className="reset-password-form__errors-list">
103
+ {validationErrors.map(err => <li key={err}>{err}</li>)}
104
+ </ul>
105
+ )}
101
106
  <input
102
107
  type="text"
103
108
  name="body"
@@ -12,13 +12,14 @@ const UserSignInForm = ({ children, signInUser, userCredentials, setLoading, ...
12
12
 
13
13
  signInUser(userCredentials)
14
14
  .then(({ success }) => {
15
- setLoading({ userSignIn: false });
16
15
  if (success) {
17
16
  notify('You have successfully signed in', 'success');
18
17
  } else {
19
18
  notify('Sign in failed, please try again', 'error');
20
19
  }
21
- });
20
+ })
21
+ .catch(() => notify('Sign in failed, please try again', 'error'))
22
+ .finally(() => setLoading({ userSignIn: false }));
22
23
  };
23
24
 
24
25
  return (
@@ -0,0 +1,78 @@
1
+ const ERROR_MESSAGE_SERVICE_NOT_AVAILABLE = 'Sorry, the service is temporarily unavailable. Please try again later.';
2
+ const ERROR_MESSAGE_NOT_FOUND_POSTCODE = 'Sorry, no postcode was found for the searched address.';
3
+ const ERROR_DEFAULT_MESSAGE = 'Sorry, something went wrong. Please try again later.';
4
+
5
+ const handleRequest = async(response) => {
6
+ if (response.ok) {
7
+ const responseData = await response.json();
8
+ return responseData;
9
+ } else {
10
+ throw new Error(ERROR_MESSAGE_SERVICE_NOT_AVAILABLE);
11
+ }
12
+ }
13
+
14
+ const fetchAddressSuggestions = ({ searchedAddress }) => fetch(`/address_lookup?autocomplete=${searchedAddress}`)
15
+ .then(handleRequest)
16
+ .then((data) => data?.suggestions
17
+ ? data?.suggestions?.filter((_item, index) => index < 15)
18
+ : []
19
+ )
20
+ .catch((err) => {
21
+ console.log(err);
22
+ return err;
23
+ })
24
+
25
+ const fetchBranchDataByPostcode = ({
26
+ postcode,
27
+ usingPolygon = false,
28
+ channel,
29
+ isDepartmentSearch
30
+ }) => {
31
+ const polygonPrm = (usingPolygon && channel) ? '&using_polygon=true' : '';
32
+ const channelPrm = channel ? `&channel=${channel}` : '';
33
+ const departmentPrm = (isDepartmentSearch && channel) ? `&department=true` : '';
34
+
35
+ return fetch(`/postcode_branch_lookup?postcode=${postcode}${departmentPrm}${channelPrm}${polygonPrm}`)
36
+ .then(handleRequest)
37
+ .catch((err) => {
38
+ console.log(err);
39
+ return err;
40
+ })
41
+ }
42
+
43
+ const fetchDetailedAddressData = ({ addressID }) => fetch(`/address_lookup?id=${addressID}`)
44
+ .then(handleRequest)
45
+ .catch((err) => {
46
+ console.log(err);
47
+ return err;
48
+ });
49
+
50
+ const fetchBranchDataBySearchedAddress = ({
51
+ addressID,
52
+ usingPolygon,
53
+ channel,
54
+ isDepartmentSearch,
55
+ }) => fetchDetailedAddressData({ addressID })
56
+ .then((data) => {
57
+ if (data?.postcode) {
58
+ return fetchBranchDataByPostcode({
59
+ postcode: data.postcode,
60
+ usingPolygon,
61
+ channel,
62
+ isDepartmentSearch
63
+ })
64
+ } else {
65
+ throw new Error(ERROR_MESSAGE_NOT_FOUND_POSTCODE);
66
+ }
67
+ })
68
+ .then((data) => data?.branch && data?.branch?.id ? [data?.branch?.id.toString()] : [])
69
+ .catch((err) => {
70
+ console.log(err);
71
+ return err;
72
+ });
73
+
74
+ export default {
75
+ fetchAddressSuggestions,
76
+ fetchBranchDataBySearchedAddress,
77
+ ERROR_DEFAULT_MESSAGE,
78
+ };