Skip to content

Commit 75fe164

Browse files
committed
Fix (hopefully) for the community registration flow
It turned out that community registration flow did not work for communities protected by DocuSign terms, if at a page there were multiple instances of Join Community buttons. The reason was that multiple terms modals opened simultaneously (and invisibly to the end-user), and failed when they tried to load the same DocuSign term.
1 parent aa9a585 commit 75fe164

File tree

7 files changed

+150
-69
lines changed

7 files changed

+150
-69
lines changed

.exchange-rates.cache

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"disclaimer":"Usage subject to terms: https://openexchangerates.org/terms","license":"https://openexchangerates.org/license","timestamp":1523109602,"base":"USD","rates":{"AED":3.673014,"AFN":69.55,"ALL":105.4,"AMD":479.87899,"ANG":1.784605,"AOA":214.3575,"ARS":20.1495,"AUD":1.3019,"AWG":1.784998,"AZN":1.7025,"BAM":1.5934,"BBD":2,"BDT":82.96,"BGN":1.59263,"BHD":0.377025,"BIF":1781,"BMD":1,"BND":1.317363,"BOB":6.910011,"BRL":3.368012,"BSD":1,"BTC":0.000146198031,"BTN":65.156563,"BWP":9.611152,"BYN":1.962861,"BZD":2.00983,"CAD":1.2791,"CDF":1615,"CHF":0.959255,"CLF":0.02218,"CLP":604.8,"CNH":6.311338,"CNY":6.3017,"COP":2817.460317,"CRC":567.097559,"CUC":1,"CUP":25.5,"CVE":90.45,"CZK":20.654991,"DJF":177,"DKK":6.0634,"DOP":49.527617,"DZD":114.36,"EGP":17.6585,"ERN":15.09,"ETB":27.417298,"EUR":0.81382,"FJD":2.043351,"FKP":0.7096,"GBP":0.7096,"GEL":2.425502,"GGP":0.7096,"GHS":4.437767,"GIP":0.7096,"GMD":47.32,"GNF":9026.25,"GTQ":7.415922,"GYD":206.749115,"HKD":7.84895,"HNL":23.681908,"HRK":6.045872,"HTG":64.707141,"HUF":254.415,"IDR":13776.647095,"ILS":3.53138,"IMP":0.7096,"INR":64.9145,"IQD":1189,"IRR":37636.02369,"ISK":98.768306,"JEP":0.7096,"JMD":124.766797,"JOD":0.709503,"JPY":106.93,"KES":101,"KGS":68.300749,"KHR":4008,"KMF":402.498446,"KPW":900,"KRW":1070.44,"KWD":0.300118,"KYD":0.833149,"KZT":319.890241,"LAK":8315,"LBP":1513,"LKR":155.45,"LRD":131.751267,"LSL":11.936136,"LYD":1.332,"MAD":9.215767,"MDL":16.433954,"MGA":3230.2,"MKD":50.1385,"MMK":1332.860206,"MNT":2388.332694,"MOP":8.087069,"MRO":355.5,"MRU":35.4,"MUR":33.5735,"MVR":15.404937,"MWK":726.3,"MXN":18.2883,"MYR":3.870046,"MZN":61.17,"NAD":12.05125,"NGN":360,"NIO":31.162733,"NOK":7.8285,"NPR":104.23432,"NZD":1.37645,"OMR":0.384981,"PAB":1,"PEN":3.232482,"PGK":3.261758,"PHP":52.044,"PKR":115.5,"PLN":3.417343,"PYG":5561.8,"QAR":3.640999,"RON":3.796299,"RSD":96.23474,"RUB":57.989,"RWF":866.275,"SAR":3.7508,"SBD":7.806884,"SCR":13.455,"SDG":18.160225,"SEK":8.39015,"SGD":1.315422,"SHP":0.7096,"SLL":7664.007735,"SOS":578.199728,"SRD":7.468,"SSP":130.2634,"STD":19928.859374,"STN":20.1,"SVC":8.748292,"SYP":514.98999,"SZL":11.932171,"THB":31.231,"TJS":8.824145,"TMT":3.499986,"TND":2.431109,"TOP":2.237882,"TRY":4.045708,"TTD":6.7429,"TWD":29.328,"TZS":2262.9,"UAH":26.2729,"UGX":3692.15,"USD":1,"UYU":28.265527,"UZS":8100.2,"VEF":40241.5117,"VND":22795.76534,"VUV":106.388832,"WST":2.526438,"XAF":533.830926,"XAG":0.06105823,"XAU":0.00075007,"XCD":2.70255,"XDR":0.690534,"XOF":533.830926,"XPD":0.00110637,"XPF":97.114558,"XPT":0.00109154,"YER":250.306642,"ZAR":12.032145,"ZMW":9.384,"ZWL":322.355011}}
1+
{"disclaimer":"Usage subject to terms: https://openexchangerates.org/terms","license":"https://openexchangerates.org/license","timestamp":1523394000,"base":"USD","rates":{"AED":3.673014,"AFN":69.841474,"ALL":104.75,"AMD":480.835755,"ANG":1.78455,"AOA":217.689,"ARS":20.1498,"AUD":1.288598,"AWG":1.784998,"AZN":1.7025,"BAM":1.586412,"BBD":2,"BDT":82.949445,"BGN":1.583075,"BHD":0.376926,"BIF":1781,"BMD":1,"BND":1.30961,"BOB":6.910011,"BRL":3.409706,"BSD":1,"BTC":0.000146310823,"BTN":64.987163,"BWP":9.636446,"BYN":1.980564,"BZD":2.009463,"CAD":1.260319,"CDF":1615,"CHF":0.956855,"CLF":0.02201,"CLP":600.4,"CNH":6.277035,"CNY":6.283404,"COP":2771.5,"CRC":567.465,"CUC":1,"CUP":25.5,"CVE":89.875,"CZK":20.4789,"DJF":176.525,"DKK":6.026823,"DOP":49.42,"DZD":113.880333,"EGP":17.681,"ERN":14.996667,"ETB":27.55,"EUR":0.809323,"FJD":2.029692,"FKP":0.7055,"GBP":0.7055,"GEL":2.426152,"GGP":0.7055,"GHS":4.435,"GIP":0.7055,"GMD":47.26,"GNF":9025.5,"GTQ":7.415666,"GYD":207.14,"HKD":7.84958,"HNL":23.660322,"HRK":6.011264,"HTG":64.706641,"HUF":251.93,"IDR":13761.147095,"ILS":3.501903,"IMP":0.7055,"INR":64.935,"IQD":1187.5,"IRR":37639.02369,"ISK":98.34,"JEP":0.7055,"JMD":124.781797,"JOD":0.709503,"JPY":107.19263007,"KES":101.101,"KGS":68.275351,"KHR":4005,"KMF":398.6,"KPW":900,"KRW":1064.7,"KWD":0.29987,"KYD":0.833134,"KZT":321.06,"LAK":8310,"LBP":1515.55,"LKR":155.55,"LRD":131.715,"LSL":12.055,"LYD":1.330156,"MAD":9.1718,"MDL":16.451521,"MGA":3205,"MKD":49.83175,"MMK":1324.65,"MNT":2389.832694,"MOP":8.083019,"MRO":355.5,"MRU":35.47,"MUR":33.598,"MVR":15.404937,"MWK":722.08,"MXN":18.2659,"MYR":3.871619,"MZN":60.99,"NAD":12.0525,"NGN":360,"NIO":31.19,"NOK":7.79538,"NPR":103.969545,"NZD":1.35846,"OMR":0.384996,"PAB":1,"PEN":3.238763,"PGK":3.234,"PHP":51.815,"PKR":115.78,"PLN":3.389,"PYG":5534.25,"QAR":3.640999,"RON":3.76902,"RSD":95.68,"RUB":63.1649,"RWF":860,"SAR":3.7587,"SBD":7.806884,"SCR":13.455839,"SDG":18.156826,"SEK":8.30604,"SGD":1.309245,"SHP":0.7055,"SLL":7729.007735,"SOS":577,"SRD":7.468,"SSP":130.2634,"STD":19869.909179,"STN":19.97,"SVC":8.747525,"SYP":514.98999,"SZL":12.06,"THB":31.195,"TJS":8.796594,"TMT":3.504988,"TND":2.390312,"TOP":2.225182,"TRY":4.112305,"TTD":6.728287,"TWD":29.192046,"TZS":2262.95,"UAH":25.926,"UGX":3691.5,"USD":1,"UYU":28.293384,"UZS":8115,"VEF":40241.5117,"VND":22788.26534,"VUV":106.192164,"WST":2.516171,"XAF":530.881357,"XAG":0.06034587,"XAU":0.00074582,"XCD":2.70255,"XDR":0.687937,"XOF":530.881357,"XPD":0.00105153,"XPF":96.577973,"XPT":0.00107644,"YER":250.3,"ZAR":12.0381,"ZMW":9.394379,"ZWL":322.355011}}

src/shared/actions/terms.js

+28-2
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,30 @@ function checkStatusDone(entity, tokens) {
187187
return checkStatus(MAX_ATTEMPTS);
188188
}
189189

190+
/**
191+
* Payload creator for the action that opens the specified terms modal.
192+
* @param {String} modalInstanceUuid ID of the terms modal instance to be
193+
* opened. Any other instances of terms modals present in the page will be
194+
* closed automatically by this action, as it is not safe to open multiple
195+
* modals, (and makes no sense in current implementation).
196+
* @param {???} selectedTerm Optional. Selected term. It was not documented by
197+
* author of related code, thus the exact value is not clear.
198+
* @return {Object} Action payload.
199+
*/
200+
function openTermsModal(modalInstanceUuid, selectedTerm) {
201+
return { modalInstanceUuid, selectedTerm };
202+
}
203+
204+
/**
205+
* Payload creator for the action that closes the specified terms modal.
206+
* @param {String} modalInstanceUuid ID of the terms modal instance to be
207+
* closed. If another terms modal is open, it won't be affected.
208+
* @return {String} Action payload.
209+
*/
210+
function closeTermsModal(modalInstanceUuid) {
211+
return modalInstanceUuid;
212+
}
213+
190214
export default createActions({
191215
TERMS: {
192216
GET_TERMS_INIT: _.identity,
@@ -197,8 +221,10 @@ export default createActions({
197221
GET_DOCU_SIGN_URL_DONE: getDocuSignUrlDone,
198222
AGREE_TERM_INIT: agreeTermInit,
199223
AGREE_TERM_DONE: agreeTermDone,
200-
OPEN_TERMS_MODAL: _.identity,
201-
CLOSE_TERMS_MODAL: _.noop,
224+
225+
OPEN_TERMS_MODAL: openTermsModal,
226+
CLOSE_TERMS_MODAL: closeTermsModal,
227+
202228
SELECT_TERM: _.identity,
203229
SIGN_DOCU: _.identity,
204230
CHECK_STATUS_INIT: _.noop,

src/shared/containers/ReviewOpportunityDetails.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ function mapDispatchToProps(dispatch) {
194194
},
195195
onPhaseExpand: () => dispatch(page.togglePhasesExpand()),
196196
openTermsModal: () => {
197-
dispatch(terms.openTermsModal());
197+
dispatch(terms.openTermsModal('ANY'));
198198
},
199199
selectTab: tab => dispatch(page.selectTab(tab)),
200200
setRoles: roles => dispatch(page.setRoles(roles)),

src/shared/containers/Terms.jsx

+62-32
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import _ from 'lodash';
1111
import Terms from 'components/Terms';
1212
import termsActions from 'actions/terms';
1313

14+
let isAnyTermModalOpen = false;
15+
1416
class TermsPageContainer extends React.Component {
1517
componentDidMount() {
1618
const { loadTerms, authTokens, entity, termsForEntity } = this.props;
@@ -31,37 +33,56 @@ class TermsPageContainer extends React.Component {
3133
}
3234
}
3335

36+
componentWillUnmount() {
37+
if (this.forceOpen) isAnyTermModalOpen = false;
38+
}
39+
3440
render() {
41+
const {
42+
closeTermsModal,
43+
instanceId,
44+
openTermsModalUuid,
45+
} = this.props;
46+
47+
if (openTermsModalUuid === 'ANY' && !isAnyTermModalOpen) {
48+
isAnyTermModalOpen = true;
49+
this.forceOpen = true;
50+
}
51+
const open = (openTermsModalUuid && openTermsModalUuid === instanceId)
52+
|| (openTermsModalUuid === 'ANY' && this.forceOpen);
53+
3554
return (
3655
<div>
37-
{this.props.showTermsModal &&
38-
<Terms
39-
agreeingTerm={this.props.agreeingTerm}
40-
agreeTerm={termId => this.props.agreeTerm(this.props.authTokens, termId)}
41-
canRegister={this.props.canRegister}
42-
checkingStatus={this.props.checkingStatus}
43-
checkStatus={() => this.props.checkStatus(this.props.authTokens, this.props.entity)}
44-
defaultTitle={this.props.defaultTitle}
45-
description={this.props.description}
46-
details={this.props.termDetails}
47-
docuSignUrl={this.props.docuSignUrl}
48-
getDocuSignUrl={(templateId) => {
49-
const base = window ? window.location.href.match('.*://[^/]*')[0] : '';
50-
return this.props.getDocuSignUrl(this.props.authTokens,
51-
templateId, `${base}/community-app-assets/iframe-break`);
52-
}}
53-
isLoadingTerms={this.props.isLoadingTerms}
54-
loadDetails={termId => this.props.loadTermDetails(this.props.authTokens, termId)}
55-
loadingDocuSignUrl={this.props.loadingDocuSignUrl}
56-
loadingTermId={this.props.loadingTermId}
57-
onCancel={this.props.closeTermsModal}
58-
register={this.props.register}
59-
selectedTerm={this.props.selectedTerm}
60-
selectTerm={this.props.selectTerm}
61-
signDocu={this.props.signDocu}
62-
terms={this.props.terms}
63-
viewOnly={this.props.viewOnly}
64-
/>
56+
{
57+
open ? (
58+
<Terms
59+
agreeingTerm={this.props.agreeingTerm}
60+
agreeTerm={termId => this.props.agreeTerm(this.props.authTokens, termId)}
61+
canRegister={this.props.canRegister}
62+
checkingStatus={this.props.checkingStatus}
63+
checkStatus={() => this.props.checkStatus(this.props.authTokens, this.props.entity)}
64+
defaultTitle={this.props.defaultTitle}
65+
description={this.props.description}
66+
details={this.props.termDetails}
67+
docuSignUrl={this.props.docuSignUrl}
68+
getDocuSignUrl={(templateId) => {
69+
const base = window ? window.location.href.match('.*://[^/]*')[0] : '';
70+
return this.props.getDocuSignUrl(this.props.authTokens,
71+
templateId, `${base}/community-app-assets/iframe-break`);
72+
}}
73+
isLoadingTerms={this.props.isLoadingTerms}
74+
loadDetails={termId => this.props.loadTermDetails(this.props.authTokens, termId)}
75+
loadingDocuSignUrl={this.props.loadingDocuSignUrl}
76+
loadingTermId={this.props.loadingTermId}
77+
onCancel={() => closeTermsModal(instanceId)}
78+
register={this.props.register}
79+
selectedTerm={this.props.selectedTerm}
80+
selectTerm={this.props.selectTerm}
81+
signDocu={this.props.signDocu}
82+
terms={this.props.terms}
83+
viewOnly={this.props.viewOnly}
84+
/>
85+
) : null
6586
}
6687
</div>
6788
);
@@ -107,14 +128,23 @@ TermsPageContainer.propTypes = {
107128
entity: enitytType.isRequired,
108129
getDocuSignUrl: PT.func.isRequired,
109130
isLoadingTerms: PT.bool,
131+
132+
/* WARNING: This is very important prop! It is not safe to open multiple terms
133+
* modal simultaneously: DocuSign terms will fail to load, if the same terms
134+
* is shown in multiple modals. To prevent this, parent component should pass
135+
* into terms container an UUID that will be used then in actions to open
136+
* this exact modal. */
137+
instanceId: PT.string.isRequired,
138+
110139
loadingDocuSignUrl: PT.string,
111140
loadingTermId: PT.string,
112141
loadTermDetails: PT.func.isRequired,
113142
loadTerms: PT.func.isRequired,
143+
openTermsModalUuid: PT.string.isRequired,
114144
register: PT.func.isRequired,
115145
selectedTerm: PT.shape(),
116146
selectTerm: PT.func.isRequired,
117-
showTermsModal: PT.bool,
147+
118148
signDocu: PT.func.isRequired,
119149
termDetails: PT.shape(),
120150
terms: PT.arrayOf(PT.shape()),
@@ -131,8 +161,8 @@ const mapStateToProps = (state, props) => ({
131161
isLoadingTerms: _.isEqual(state.terms.loadingTermsForEntity, props.entity),
132162
loadingDocuSignUrl: state.terms.loadingDocuSignUrl,
133163
loadingTermId: state.terms.loadingDetailsForTermId,
164+
openTermsModalUuid: state.terms.openTermsModalUuid,
134165
selectedTerm: state.terms.selectedTerm,
135-
showTermsModal: state.terms.showTermsModal,
136166
termDetails: state.terms.details,
137167
terms: state.terms.terms,
138168
termsForEntity: state.terms.entity,
@@ -142,8 +172,8 @@ const mapStateToProps = (state, props) => ({
142172
const mapDispatchToProps = (dispatch) => {
143173
const t = termsActions.terms;
144174
return {
145-
closeTermsModal: () => {
146-
dispatch(t.closeTermsModal());
175+
closeTermsModal: (uuid) => {
176+
dispatch(t.closeTermsModal(uuid));
147177
},
148178
selectTerm: (term) => {
149179
dispatch(t.selectTerm(term));

src/shared/containers/challenge-detail/index.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ const mapDispatchToProps = (dispatch) => {
500500
dispatch(cl.getChallengeSubtracksDone());
501501
},
502502
openTermsModal: (term) => {
503-
dispatch(t.openTermsModal(term));
503+
dispatch(t.openTermsModal('ANY', term));
504504
},
505505
updateChallenge: (challenge, tokenV3) => {
506506
const uuid = shortId();

src/shared/containers/tc-communities/JoinCommunity.jsx

+29-18
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,42 @@ import React from 'react';
88
import PT from 'prop-types';
99
import _ from 'lodash';
1010
import actions from 'actions/tc-communities';
11+
import shortId from 'shortid';
1112
import Terms from 'containers/Terms';
1213
import termsActions from 'actions/terms';
14+
1315
import JoinCommunity, {
1416
STATE as JOIN_COMMUNITY,
1517
} from 'components/tc-communities/JoinCommunity';
1618
import { connect } from 'react-redux';
1719

18-
const JoinCommunityContainer = (props) => {
19-
const { token, groupIds, userId, terms, openTermsModal,
20-
communityId, join, joinCommunityWrapper } = props;
20+
class JoinCommunityContainer extends React.Component {
21+
constructor(props) {
22+
super(props);
23+
this.instanceId = shortId();
24+
}
2125

22-
const hasNotAgreedTerms = terms && terms.length && !_.every(terms, 'agreed');
23-
const onJoinClick = hasNotAgreedTerms ? openTermsModal : join;
26+
render() {
27+
const { token, groupIds, userId, terms, openTermsModal,
28+
communityId, join, joinCommunityWrapper } = this.props;
2429

25-
return (
26-
<div className={joinCommunityWrapper}>
27-
<Terms
28-
entity={{ type: 'community', id: communityId }}
29-
description="You are seeing these Terms & Conditions because you are going to join a community and you have to respect the terms below in order to be able to be a member."
30-
register={() => join(token, groupIds[0], userId)}
31-
/>
32-
<JoinCommunity {...props} join={onJoinClick} />
33-
</div>
34-
);
35-
};
30+
const hasNotAgreedTerms = terms && terms.length && !_.every(terms, 'agreed');
31+
const onJoinClick = hasNotAgreedTerms ?
32+
() => openTermsModal(this.instanceId) : join;
33+
34+
return (
35+
<div className={joinCommunityWrapper}>
36+
<Terms
37+
entity={{ type: 'community', id: communityId }}
38+
description="You are seeing these Terms & Conditions because you are going to join a community and you have to respect the terms below in order to be able to be a member."
39+
instanceId={this.instanceId}
40+
register={() => join(token, groupIds[0], userId)}
41+
/>
42+
<JoinCommunity {...this.props} join={onJoinClick} />
43+
</div>
44+
);
45+
}
46+
}
3647

3748
JoinCommunityContainer.defaultProps = {
3849
token: '',
@@ -93,9 +104,9 @@ function mapDispatchToProps(dispatch) {
93104
},
94105
resetJoinButton: () => dispatch(a.resetJoinButton()),
95106
showJoinConfirmModal: () => dispatch(a.showJoinConfirmModal()),
96-
openTermsModal: () => {
107+
openTermsModal: (uuid) => {
97108
dispatch(a.resetJoinButton());
98-
dispatch(t.openTermsModal());
109+
dispatch(t.openTermsModal(uuid));
99110
},
100111
};
101112
}

src/shared/reducers/terms.js

+28-14
Original file line numberDiff line numberDiff line change
@@ -174,29 +174,41 @@ function onAgreeTermDone(state, action) {
174174
}
175175

176176
/**
177-
* Handles TERMS/OPEN_TERMS_MODAL action.
177+
* Opens the specified instance of terms modal + selects the terms to show in
178+
* there, although the exact functioning of that functionality was not
179+
* documented, thus has to be tracked.
178180
* @param {Object} state
179181
* @param {Object} action
180182
* @return {Object} New state.
181183
*/
182184
function onOpenTermsModal(state, action) {
183-
if (action.payload) {
184-
return {
185-
...state,
186-
showTermsModal: true,
187-
selectedTerm: action.payload,
188-
viewOnly: true,
189-
};
185+
const { modalInstanceUuid } = action.payload;
186+
187+
let { selectedTerm } = action.payload;
188+
if (!selectedTerm) {
189+
selectedTerm = _.find(state.terms, t => !t.agreed) || state.terms[0];
190190
}
191-
const selectedTerm = _.find(state.terms, t => !t.agreed) || state.terms[0];
191+
192192
return {
193193
...state,
194-
showTermsModal: true,
194+
openTermsModalUuid: modalInstanceUuid,
195195
selectedTerm,
196-
viewOnly: false,
196+
viewOnly: Boolean(action.payload.selectedTerm),
197197
};
198198
}
199199

200+
/**
201+
* Closes the specified terms modal, if necessary.
202+
* @param {Object} state
203+
* @param {Object} action
204+
* @return {Object} New state.
205+
*/
206+
function onCloseTermsModal(state, { payload }) {
207+
if (payload !== state.openTermsModalUuid
208+
&& state.openTermsModalUuid !== 'ANY') return state;
209+
return { ...state, openTermsModalUuid: '' };
210+
}
211+
200212
/**
201213
* Handles TERMS/SIGN_DOCU action.
202214
* @param {Object} state
@@ -275,8 +287,10 @@ function create(initialState) {
275287
agreeingTerm: payload,
276288
}),
277289
[actions.terms.agreeTermDone]: onAgreeTermDone,
290+
278291
[actions.terms.openTermsModal]: onOpenTermsModal,
279-
[actions.terms.closeTermsModal]: state => ({ ...state, showTermsModal: false }),
292+
[actions.terms.closeTermsModal]: onCloseTermsModal,
293+
280294
[actions.terms.selectTerm]: (state, { payload }) => ({ ...state, selectedTerm: payload }),
281295
[actions.terms.signDocu]: onSignDocu,
282296
[actions.terms.checkStatusInit]: state => ({
@@ -287,7 +301,7 @@ function create(initialState) {
287301
}, initialState || {
288302
getTermsFailure: false,
289303
terms: [],
290-
showTermsModal: false,
304+
openTermsModalUuid: '',
291305
selectedTerm: null,
292306
viewOnly: false,
293307
checkingStatus: false,
@@ -335,7 +349,7 @@ export function factory(req) {
335349
// if we try to join community automatically, but not all terms are agreed,
336350
// then we show terms modal (also we make sure is logged in before open)
337351
if (tokens.tokenV3 && req.query.join && !_.every(termsDoneAction.payload.terms, 'agreed')) {
338-
state = onOpenTermsModal(state, actions.terms.openTermsModal());
352+
state = onOpenTermsModal(state, actions.terms.openTermsModal('ANY'));
339353
}
340354

341355
return create(state);

0 commit comments

Comments
 (0)