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.
- package/actions/branches.actions.js +10 -0
- package/actions/branches.types.js +2 -0
- package/actions/user.actions.js +49 -38
- package/branches/branches-search-form/branches-polygon-search-input.component.jsx +216 -0
- package/branches/index.js +2 -0
- package/hooks/use-recaptcha.js +5 -3
- package/package.json +1 -1
- package/reducers/app.reducer.js +1 -0
- package/reducers/branches.reducer.js +12 -0
- package/user/change-password-form/change-password-form.component.jsx +303 -0
- package/user/forgotten-password-form/forgotten-password-form.component.jsx +11 -1
- package/user/index.js +2 -0
- package/user/reset-password-form/reset-password-form.component.jsx +67 -7
- package/user/sign-out-button/sign-out-button.component.jsx +4 -2
- package/user/user-edit-form/user-edit-form.component.jsx +2 -2
- package/user/user-register-form/user-register-form.component.jsx +36 -31
- package/user/user-sign-in-form/user-sign-in-form.component.jsx +3 -2
- package/utils/requests-handler.js +78 -0
|
@@ -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;
|
package/actions/user.actions.js
CHANGED
|
@@ -67,56 +67,67 @@ export const createUser = (payload, recaptchaData = {}) => (dispatch, getState)
|
|
|
67
67
|
}),
|
|
68
68
|
})
|
|
69
69
|
.then(async (response) => {
|
|
70
|
-
const
|
|
71
|
-
|
|
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
|
-
|
|
76
|
+
let promises = [];
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
document.dispatchEvent(event);
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
});
|
|
111
|
+
Promise.all(promises).then(() => dispatch(fetchUser()));
|
|
112
|
+
|
|
113
|
+
return responseData;
|
|
114
|
+
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
const hasValidationErrors = responseData.errors && Array.isArray(responseData.errors);
|
|
117
|
+
if (hasValidationErrors) {
|
|
118
|
+
return { validationErrors: responseData.errors };
|
|
119
|
+
}
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
if (responseData.message) {
|
|
122
|
+
throw new Error(responseData.message);
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error();
|
|
125
|
+
}
|
|
118
126
|
})
|
|
119
|
-
.
|
|
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
|
};
|
package/hooks/use-recaptcha.js
CHANGED
|
@@ -92,12 +92,14 @@ const useRecaptcha = ({
|
|
|
92
92
|
}, [stateIsRecaptchaLoaded]);
|
|
93
93
|
|
|
94
94
|
const onRecaptchaRender = useCallback(() => {
|
|
95
|
-
const
|
|
95
|
+
const recaptchaTokenEl = formRef
|
|
96
96
|
.current
|
|
97
97
|
.querySelector(`[name="${RECAPTCHA_CONFIG.FILED_NAME}"]`);
|
|
98
98
|
|
|
99
|
-
if (
|
|
100
|
-
|
|
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
package/reducers/app.reducer.js
CHANGED
|
@@ -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
|
-
})
|
|
40
|
-
.then((
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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) =>
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
55
|
+
if (redirectHash) {
|
|
56
|
+
window.location.hash = redirectHash;
|
|
57
|
+
}
|
|
53
58
|
}
|
|
54
59
|
})
|
|
55
|
-
.catch((
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
};
|