Skip to content

Commit 4ccf085

Browse files
Merge pull request #5735 from topcoder-platform/ref-email-tracking
Ref email tracking
2 parents e80f6b1 + c1c0dc0 commit 4ccf085

File tree

10 files changed

+183
-27
lines changed

10 files changed

+183
-27
lines changed

.circleci/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,14 +350,14 @@ workflows:
350350
filters:
351351
branches:
352352
only:
353-
- gig-apply-new-options-dropdown
353+
- free
354354
# This is alternate dev env for parallel testing
355355
- "build-qa":
356356
context : org-global
357357
filters:
358358
branches:
359359
only:
360-
- member-path-component
360+
- ref-email-tracking
361361
# This is beta env for production soft releases
362362
- "build-prod-beta":
363363
context : org-global

src/server/routes/growsurf.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,26 @@
22
* The routes related to Growsurf integration
33
*/
44

5+
import _ from 'lodash';
56
import express from 'express';
7+
import { middleware } from 'tc-core-library-js';
8+
import config from 'config';
69
import GrowsurfService from '../services/growsurf';
710

811
const cors = require('cors');
912

13+
const authenticator = middleware.jwtAuthenticator;
14+
const authenticatorOptions = _.pick(config.SECRET.JWT_AUTH, ['AUTH_SECRET', 'VALID_ISSUERS']);
1015
const routes = express.Router();
1116

1217
// Enables CORS on those routes according config above
1318
// ToDo configure CORS for set of our trusted domains
1419
routes.use(cors());
1520
routes.options('*', cors());
1621

17-
routes.get('/participants', (req, res) => new GrowsurfService().getParticipant(req, res).then(res.send.bind(res)));
18-
routes.post('/participants', (req, res) => new GrowsurfService().getOrCreateParticipant(req, res).then(res.send.bind(res)));
22+
routes.get('/participant/:emailOrId', (req, res) => new GrowsurfService().getParticipantController(req, res).then(res.send.bind(res)));
23+
routes.get('/participants', (req, res) => new GrowsurfService().getParticipantsController(req, res).then(res.send.bind(res)));
24+
routes.post('/participants', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res) => new GrowsurfService().getOrCreateParticipantController(req, res).then(res.send.bind(res)));
25+
routes.patch('/participant/:emailOrId', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res) => new GrowsurfService().updateParticipantController(req, res).then(res.send.bind(res)));
1926

2027
export default routes;

src/server/services/growsurf.js

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default class GrowsurfService {
2121
}
2222

2323
/**
24-
* Gets get participant.
24+
* Gets participant by email or id
2525
* @return {Promise}
2626
* @param {String} idOrEmail growsurf id or email
2727
*/
@@ -45,14 +45,28 @@ export default class GrowsurfService {
4545
}
4646

4747
/**
48-
* Gets get participant by email or id.
48+
* Controller - Gets get participant by email or id
4949
* @return {Promise}
5050
* @param {Object} req the request.
5151
* @param {Object} res the response.
5252
*/
53-
async getParticipant(req, res) {
54-
const { participantId } = req.query;
55-
const response = await fetch(`${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${participantId}`, {
53+
async getParticipantController(req, res) {
54+
const { emailOrId } = req.params;
55+
const growSurfData = await this.getParticipantByIdOREmail(emailOrId);
56+
if (growSurfData.error) {
57+
res.status(growSurfData.code);
58+
}
59+
return growSurfData;
60+
}
61+
62+
/**
63+
* Controller - Gets get participants in the campaign
64+
* @return {Promise}
65+
* @param {Object} req the request.
66+
* @param {Object} res the response.
67+
*/
68+
async getParticipantsController(req, res) {
69+
const response = await fetch(`${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participants`, {
5670
method: 'GET',
5771
headers: {
5872
'Content-Type': 'application/json',
@@ -64,7 +78,7 @@ export default class GrowsurfService {
6478
return {
6579
error: await response.json(),
6680
code: response.status,
67-
url: `${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${participantId}`,
81+
url: `${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participants`,
6882
};
6983
}
7084
const data = await response.json();
@@ -98,13 +112,13 @@ export default class GrowsurfService {
98112
}
99113

100114
/**
101-
* Gets get participant by email or id
115+
* Controller - Gets get participant by email or id
102116
* if not exxists create it
103117
* @return {Promise}
104118
* @param {Object} req the request.
105119
* @param {Object} res the response.
106120
*/
107-
async getOrCreateParticipant(req, res) {
121+
async getOrCreateParticipantController(req, res) {
108122
const { body } = req;
109123
const result = await this.addParticipant(JSON.stringify({
110124
email: body.email,
@@ -133,7 +147,7 @@ export default class GrowsurfService {
133147
'Content-Type': 'application/json',
134148
Authorization: this.private.authorization,
135149
},
136-
body,
150+
body: JSON.stringify(body),
137151
});
138152
if (response.status >= 300) {
139153
return {
@@ -146,4 +160,18 @@ export default class GrowsurfService {
146160
const data = await response.json();
147161
return data;
148162
}
163+
164+
/**
165+
* Controller - update participant in the system
166+
* @param {Object} req request
167+
* @param {Object} res respons
168+
*/
169+
async updateParticipantController(req, res) {
170+
const { emailOrId } = req.params;
171+
const updateGrowRes = await this.updateParticipant(emailOrId, req.body);
172+
if (updateGrowRes.error) {
173+
res.status(updateGrowRes.code);
174+
}
175+
return updateGrowRes;
176+
}
149177
}

src/shared/actions/growSurf.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ function getReferralIdInit() {
1818
/**
1919
* Get referral id for the logged user
2020
* if this member does not exist in growsurf it creates it in the system
21+
* @param {Object} profile the member auth profile
22+
* @param {String} token the auth token
2123
*/
22-
async function getReferralIdDone(profile) {
24+
async function getReferralIdDone(profile, tokenV3) {
2325
if (profile.email) {
2426
const res = await fetch(`${PROXY_ENDPOINT}/growsurf/participants?participantId=${profile.email}`, {
2527
method: 'POST',
@@ -31,6 +33,7 @@ async function getReferralIdDone(profile) {
3133
}),
3234
headers: {
3335
'Content-Type': 'application/json',
36+
Authorization: `Bearer ${tokenV3}`,
3437
},
3538
});
3639
if (res.status >= 300) {

src/shared/components/Gigs/GigDetails/index.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,15 @@ function GigDetails(props) {
262262
&& (
263263
<ReferralModal
264264
profile={profile}
265-
onCloseButton={() => setModalOpen(false)}
265+
onCloseButton={() => {
266+
onReferralDone();
267+
setModalOpen(false);
268+
}}
266269
isReferrSucess={isReferrSucess}
267270
isReferrError={isReferrError}
268271
referralId={growSurf && growSurf.data ? growSurf.data.id : null}
269272
onReferralDone={() => {
270-
onReferralDone();
273+
onReferralDone(true);
271274
setModalOpen(false);
272275
}}
273276
/>

src/shared/components/Gigs/ReferralModal/index.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@ function ReferralModal({
7373
<div className={modalStyle.referrSucess}>
7474
<h3 className={modalStyle.title}>OOPS!</h3>
7575
<p className={modalStyle.loginMsg}>{isReferrError.message}</p>
76-
<p>Looks like there is a problem on our end. Please try again.<br />If this persists please contact <a href="mailto:support@topcoder.com">support@topcoder.com</a>.</p>
76+
{
77+
isReferrError.userError ? (
78+
<p>If you think this is an error please contact <a href="mailto:support@topcoder.com">support@topcoder.com</a>.</p>
79+
) : (
80+
<p>Looks like there is a problem on our end. Please try again.<br />If this persists please contact <a href="mailto:support@topcoder.com">support@topcoder.com</a>.</p>
81+
)
82+
}
7783
<div className={modalStyle.ctaButtons}>
7884
<PrimaryButton
7985
onClick={onCloseButton}

src/shared/components/Gigs/ReferralModal/modal.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* stylelint-disable no-descending-specificity */
12
@import "~styles/mixins";
23
@import "~components/Contentful/default";
34

@@ -105,10 +106,24 @@
105106
padding: 50px 40px;
106107
}
107108

109+
.loginMsg {
110+
font-size: 20px;
111+
}
112+
108113
.regTxt {
109114
font-size: 14px;
110115
margin: 10px 0 0;
111116
}
117+
118+
.ctaButtons {
119+
@include xs-to-sm {
120+
flex-direction: column;
121+
122+
> button {
123+
margin: 0 0 20px !important;
124+
}
125+
}
126+
}
112127
}
113128
}
114129

src/shared/components/MemberPath/PathSelector/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export default function PathSelector({
101101
</div>
102102
<a
103103
href={data.items[activeItemIndex].btnURL}
104-
target="_blank"
104+
target={data.items[activeItemIndex].btnNewTab ? '_blank' : '_self'}
105105
rel="noopener noreferrer"
106106
className={styles.contentButton}
107107
>

src/shared/containers/Gigs/RecruitCRMJobDetails.jsx

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,52 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`,
5353
* Send gig referral invite
5454
*/
5555
async onSendClick(email) {
56-
const { profile, growSurf } = this.props;
56+
const {
57+
profile, growSurf, tokenV3,
58+
} = this.props;
5759
const { formData } = this.state;
58-
// email the invite
60+
// should not be able to send emails to themselves
61+
if (profile.email === email) {
62+
this.setState({
63+
isReferrError: {
64+
message: 'You are not allowed to send to yourself.',
65+
userError: true,
66+
},
67+
});
68+
// exit no email sending
69+
return;
70+
}
71+
// process sent log
72+
let { emailInvitesLog, emailInvitesStatus } = growSurf.data.metadata;
73+
if (!emailInvitesLog) emailInvitesLog = '';
74+
// check if email is in sent log alredy?
75+
const foundInLog = emailInvitesLog.indexOf(email);
76+
if (foundInLog !== -1) {
77+
this.setState({
78+
isReferrError: {
79+
message: `${email} was already invited.`,
80+
userError: true,
81+
},
82+
});
83+
// exit no email sending
84+
return;
85+
}
86+
// check if email is already referred?
87+
const growCheck = await fetch(`${PROXY_ENDPOINT}/growsurf/participant/${email}`);
88+
if (growCheck.status === 200) {
89+
const growCheckData = await growCheck.json();
90+
if (growCheckData.referrer) {
91+
this.setState({
92+
isReferrError: {
93+
message: `${email} has already been referred.`,
94+
userError: true,
95+
},
96+
});
97+
// exit no email sending
98+
return;
99+
}
100+
}
101+
// // email the invite
59102
const res = await fetch(`${PROXY_ENDPOINT}/mailchimp/email`, {
60103
method: 'POST',
61104
body: JSON.stringify({
@@ -79,21 +122,66 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`,
79122
this.setState({
80123
isReferrError: await res.json(),
81124
});
82-
} else {
125+
// exit no email tracking due to the error
126+
return;
127+
}
128+
// parse the log to array of emails
129+
if (emailInvitesLog.length) {
130+
emailInvitesLog = emailInvitesLog.split(',');
131+
} else emailInvitesLog = [];
132+
// prepare growSurf update payload
133+
// we keep only 10 emails in the log to justify program rules
134+
if (emailInvitesLog.length < 10) {
135+
emailInvitesLog.push(email);
136+
}
137+
// Auto change status when 10 emails sent
138+
if (emailInvitesLog.length === 10 && emailInvitesStatus !== 'Paid' && emailInvitesStatus !== 'Payment Pending') {
139+
emailInvitesStatus = 'Payment Pending';
140+
}
141+
// put the tracking update in growsurf
142+
const updateRed = await fetch(`${PROXY_ENDPOINT}/growsurf/participant/${growSurf.data.id}`, {
143+
method: 'PATCH',
144+
headers: {
145+
'Content-Type': 'application/json',
146+
Authorization: `Bearer ${tokenV3}`,
147+
},
148+
body: JSON.stringify({
149+
...growSurf.data,
150+
metadata: {
151+
...growSurf.data.metadata,
152+
emailInvitesSent: Number(growSurf.data.metadata.emailInvitesSent || 0) + 1,
153+
emailInvitesLog: emailInvitesLog.join(),
154+
emailInvitesStatus,
155+
},
156+
}),
157+
});
158+
if (updateRed.status >= 300) {
83159
this.setState({
84-
isReferrSucess: true,
160+
isReferrError: await updateRed.json(),
85161
});
162+
// exit no email tracking due to the error
163+
// just notify the user about it
164+
return;
86165
}
166+
// finally do:
167+
this.setState({
168+
isReferrSucess: true,
169+
});
87170
}
88171

89172
/**
90173
* Reset the form when referral done
91174
*/
92-
onReferralDone() {
175+
onReferralDone(refresh = false) {
176+
if (refresh) {
177+
window.location.reload(false);
178+
return;
179+
}
93180
const { formData } = this.state;
94181
delete formData.email;
95182
this.setState({
96183
isReferrSucess: false,
184+
isReferrError: false,
97185
formData,
98186
});
99187
}
@@ -143,6 +231,7 @@ RecruitCRMJobDetailsContainer.defaultProps = {
143231
application: null,
144232
profile: {},
145233
growSurf: {},
234+
tokenV3: null,
146235
};
147236

148237
RecruitCRMJobDetailsContainer.propTypes = {
@@ -154,6 +243,7 @@ RecruitCRMJobDetailsContainer.propTypes = {
154243
application: PT.shape(),
155244
profile: PT.shape(),
156245
growSurf: PT.shape(),
246+
tokenV3: PT.string,
157247
};
158248

159249
function mapStateToProps(state, ownProps) {
@@ -166,6 +256,7 @@ function mapStateToProps(state, ownProps) {
166256
application: data ? data.application : null,
167257
profile,
168258
growSurf,
259+
tokenV3: state.auth ? state.auth.tokenV3 : null,
169260
};
170261
}
171262

0 commit comments

Comments
 (0)