diff --git a/.circleci/config.yml b/.circleci/config.yml index 8929c58dd6..8424b30be9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -350,14 +350,14 @@ workflows: filters: branches: only: - - gig-apply-new-options-dropdown + - free # This is alternate dev env for parallel testing - "build-qa": context : org-global filters: branches: only: - - member-path-component + - ref-email-tracking # This is beta env for production soft releases - "build-prod-beta": context : org-global diff --git a/src/server/routes/growsurf.js b/src/server/routes/growsurf.js index 7e7fa2c1a8..298464e44e 100644 --- a/src/server/routes/growsurf.js +++ b/src/server/routes/growsurf.js @@ -2,11 +2,16 @@ * The routes related to Growsurf integration */ +import _ from 'lodash'; import express from 'express'; +import { middleware } from 'tc-core-library-js'; +import config from 'config'; import GrowsurfService from '../services/growsurf'; const cors = require('cors'); +const authenticator = middleware.jwtAuthenticator; +const authenticatorOptions = _.pick(config.SECRET.JWT_AUTH, ['AUTH_SECRET', 'VALID_ISSUERS']); const routes = express.Router(); // Enables CORS on those routes according config above @@ -14,7 +19,9 @@ const routes = express.Router(); routes.use(cors()); routes.options('*', cors()); -routes.get('/participants', (req, res) => new GrowsurfService().getParticipant(req, res).then(res.send.bind(res))); -routes.post('/participants', (req, res) => new GrowsurfService().getOrCreateParticipant(req, res).then(res.send.bind(res))); +routes.get('/participant/:emailOrId', (req, res) => new GrowsurfService().getParticipantController(req, res).then(res.send.bind(res))); +routes.get('/participants', (req, res) => new GrowsurfService().getParticipantsController(req, res).then(res.send.bind(res))); +routes.post('/participants', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res) => new GrowsurfService().getOrCreateParticipantController(req, res).then(res.send.bind(res))); +routes.patch('/participant/:emailOrId', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res) => new GrowsurfService().updateParticipantController(req, res).then(res.send.bind(res))); export default routes; diff --git a/src/server/services/growsurf.js b/src/server/services/growsurf.js index 1ede169b40..b9850e002c 100644 --- a/src/server/services/growsurf.js +++ b/src/server/services/growsurf.js @@ -21,7 +21,7 @@ export default class GrowsurfService { } /** - * Gets get participant. + * Gets participant by email or id * @return {Promise} * @param {String} idOrEmail growsurf id or email */ @@ -45,14 +45,28 @@ export default class GrowsurfService { } /** - * Gets get participant by email or id. + * Controller - Gets get participant by email or id * @return {Promise} * @param {Object} req the request. * @param {Object} res the response. */ - async getParticipant(req, res) { - const { participantId } = req.query; - const response = await fetch(`${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${participantId}`, { + async getParticipantController(req, res) { + const { emailOrId } = req.params; + const growSurfData = await this.getParticipantByIdOREmail(emailOrId); + if (growSurfData.error) { + res.status(growSurfData.code); + } + return growSurfData; + } + + /** + * Controller - Gets get participants in the campaign + * @return {Promise} + * @param {Object} req the request. + * @param {Object} res the response. + */ + async getParticipantsController(req, res) { + const response = await fetch(`${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participants`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -64,7 +78,7 @@ export default class GrowsurfService { return { error: await response.json(), code: response.status, - url: `${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${participantId}`, + url: `${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participants`, }; } const data = await response.json(); @@ -98,13 +112,13 @@ export default class GrowsurfService { } /** - * Gets get participant by email or id + * Controller - Gets get participant by email or id * if not exxists create it * @return {Promise} * @param {Object} req the request. * @param {Object} res the response. */ - async getOrCreateParticipant(req, res) { + async getOrCreateParticipantController(req, res) { const { body } = req; const result = await this.addParticipant(JSON.stringify({ email: body.email, @@ -133,7 +147,7 @@ export default class GrowsurfService { 'Content-Type': 'application/json', Authorization: this.private.authorization, }, - body, + body: JSON.stringify(body), }); if (response.status >= 300) { return { @@ -146,4 +160,18 @@ export default class GrowsurfService { const data = await response.json(); return data; } + + /** + * Controller - update participant in the system + * @param {Object} req request + * @param {Object} res respons + */ + async updateParticipantController(req, res) { + const { emailOrId } = req.params; + const updateGrowRes = await this.updateParticipant(emailOrId, req.body); + if (updateGrowRes.error) { + res.status(updateGrowRes.code); + } + return updateGrowRes; + } } diff --git a/src/shared/actions/growSurf.js b/src/shared/actions/growSurf.js index b3cbf3571d..38f7920de7 100644 --- a/src/shared/actions/growSurf.js +++ b/src/shared/actions/growSurf.js @@ -18,8 +18,10 @@ function getReferralIdInit() { /** * Get referral id for the logged user * if this member does not exist in growsurf it creates it in the system + * @param {Object} profile the member auth profile + * @param {String} token the auth token */ -async function getReferralIdDone(profile) { +async function getReferralIdDone(profile, tokenV3) { if (profile.email) { const res = await fetch(`${PROXY_ENDPOINT}/growsurf/participants?participantId=${profile.email}`, { method: 'POST', @@ -31,6 +33,7 @@ async function getReferralIdDone(profile) { }), headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${tokenV3}`, }, }); if (res.status >= 300) { diff --git a/src/shared/components/Gigs/GigDetails/index.jsx b/src/shared/components/Gigs/GigDetails/index.jsx index 074cd79ab0..cffab15131 100644 --- a/src/shared/components/Gigs/GigDetails/index.jsx +++ b/src/shared/components/Gigs/GigDetails/index.jsx @@ -262,12 +262,15 @@ function GigDetails(props) { && ( setModalOpen(false)} + onCloseButton={() => { + onReferralDone(); + setModalOpen(false); + }} isReferrSucess={isReferrSucess} isReferrError={isReferrError} referralId={growSurf && growSurf.data ? growSurf.data.id : null} onReferralDone={() => { - onReferralDone(); + onReferralDone(true); setModalOpen(false); }} /> diff --git a/src/shared/components/Gigs/ReferralModal/index.jsx b/src/shared/components/Gigs/ReferralModal/index.jsx index 4c9e8f39f4..f4af505232 100644 --- a/src/shared/components/Gigs/ReferralModal/index.jsx +++ b/src/shared/components/Gigs/ReferralModal/index.jsx @@ -73,7 +73,13 @@ function ReferralModal({

OOPS!

{isReferrError.message}

-

Looks like there is a problem on our end. Please try again.
If this persists please contact support@topcoder.com.

+ { + isReferrError.userError ? ( +

If you think this is an error please contact support@topcoder.com.

+ ) : ( +

Looks like there is a problem on our end. Please try again.
If this persists please contact support@topcoder.com.

+ ) + }
button { + margin: 0 0 20px !important; + } + } + } } } diff --git a/src/shared/components/MemberPath/PathSelector/index.jsx b/src/shared/components/MemberPath/PathSelector/index.jsx index ae16a7c202..4afafe7a62 100644 --- a/src/shared/components/MemberPath/PathSelector/index.jsx +++ b/src/shared/components/MemberPath/PathSelector/index.jsx @@ -101,7 +101,7 @@ export default function PathSelector({
diff --git a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx index 5594841ec9..ba5aea8091 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx @@ -53,9 +53,52 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`, * Send gig referral invite */ async onSendClick(email) { - const { profile, growSurf } = this.props; + const { + profile, growSurf, tokenV3, + } = this.props; const { formData } = this.state; - // email the invite + // should not be able to send emails to themselves + if (profile.email === email) { + this.setState({ + isReferrError: { + message: 'You are not allowed to send to yourself.', + userError: true, + }, + }); + // exit no email sending + return; + } + // process sent log + let { emailInvitesLog, emailInvitesStatus } = growSurf.data.metadata; + if (!emailInvitesLog) emailInvitesLog = ''; + // check if email is in sent log alredy? + const foundInLog = emailInvitesLog.indexOf(email); + if (foundInLog !== -1) { + this.setState({ + isReferrError: { + message: `${email} was already invited.`, + userError: true, + }, + }); + // exit no email sending + return; + } + // check if email is already referred? + const growCheck = await fetch(`${PROXY_ENDPOINT}/growsurf/participant/${email}`); + if (growCheck.status === 200) { + const growCheckData = await growCheck.json(); + if (growCheckData.referrer) { + this.setState({ + isReferrError: { + message: `${email} has already been referred.`, + userError: true, + }, + }); + // exit no email sending + return; + } + } + // // email the invite const res = await fetch(`${PROXY_ENDPOINT}/mailchimp/email`, { method: 'POST', body: JSON.stringify({ @@ -79,21 +122,66 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`, this.setState({ isReferrError: await res.json(), }); - } else { + // exit no email tracking due to the error + return; + } + // parse the log to array of emails + if (emailInvitesLog.length) { + emailInvitesLog = emailInvitesLog.split(','); + } else emailInvitesLog = []; + // prepare growSurf update payload + // we keep only 10 emails in the log to justify program rules + if (emailInvitesLog.length < 10) { + emailInvitesLog.push(email); + } + // Auto change status when 10 emails sent + if (emailInvitesLog.length === 10 && emailInvitesStatus !== 'Paid' && emailInvitesStatus !== 'Payment Pending') { + emailInvitesStatus = 'Payment Pending'; + } + // put the tracking update in growsurf + const updateRed = await fetch(`${PROXY_ENDPOINT}/growsurf/participant/${growSurf.data.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${tokenV3}`, + }, + body: JSON.stringify({ + ...growSurf.data, + metadata: { + ...growSurf.data.metadata, + emailInvitesSent: Number(growSurf.data.metadata.emailInvitesSent || 0) + 1, + emailInvitesLog: emailInvitesLog.join(), + emailInvitesStatus, + }, + }), + }); + if (updateRed.status >= 300) { this.setState({ - isReferrSucess: true, + isReferrError: await updateRed.json(), }); + // exit no email tracking due to the error + // just notify the user about it + return; } + // finally do: + this.setState({ + isReferrSucess: true, + }); } /** * Reset the form when referral done */ - onReferralDone() { + onReferralDone(refresh = false) { + if (refresh) { + window.location.reload(false); + return; + } const { formData } = this.state; delete formData.email; this.setState({ isReferrSucess: false, + isReferrError: false, formData, }); } @@ -143,6 +231,7 @@ RecruitCRMJobDetailsContainer.defaultProps = { application: null, profile: {}, growSurf: {}, + tokenV3: null, }; RecruitCRMJobDetailsContainer.propTypes = { @@ -154,6 +243,7 @@ RecruitCRMJobDetailsContainer.propTypes = { application: PT.shape(), profile: PT.shape(), growSurf: PT.shape(), + tokenV3: PT.string, }; function mapStateToProps(state, ownProps) { @@ -166,6 +256,7 @@ function mapStateToProps(state, ownProps) { application: data ? data.application : null, profile, growSurf, + tokenV3: state.auth ? state.auth.tokenV3 : null, }; } diff --git a/src/shared/containers/GigsPages.jsx b/src/shared/containers/GigsPages.jsx index ac159c7d81..3418d7ed7a 100644 --- a/src/shared/containers/GigsPages.jsx +++ b/src/shared/containers/GigsPages.jsx @@ -25,7 +25,7 @@ const cookies = require('browser-cookies'); function GigsPagesContainer(props) { const { - match, profile, growSurf, getReferralId, + match, profile, growSurf, getReferralId, tokenV3, } = props; const optProfile = { attributes: {}, @@ -38,7 +38,7 @@ function GigsPagesContainer(props) { // trigger referral id fetching when profile is loaded if (isomorphy.isClientSide()) { if (_.isEmpty(growSurf) || (!growSurf.loading && !growSurf.data && !growSurf.error)) { - getReferralId(profile); + getReferralId(profile, tokenV3); } } } else if (isomorphy.isClientSide()) { @@ -124,6 +124,7 @@ window._chatlio = window._chatlio||[]; GigsPagesContainer.defaultProps = { profile: null, growSurf: null, + tokenV3: null, }; GigsPagesContainer.propTypes = { @@ -131,6 +132,7 @@ GigsPagesContainer.propTypes = { profile: PT.shape(), growSurf: PT.shape(), getReferralId: PT.func.isRequired, + tokenV3: PT.string, }; function mapStateToProps(state) { @@ -139,15 +141,16 @@ function mapStateToProps(state) { return { profile, growSurf, + tokenV3: state.auth ? state.auth.tokenV3 : null, }; } function mapDispatchToActions(dispatch) { const a = actions.growsurf; return { - getReferralId: (profile) => { + getReferralId: (profile, tokenV3) => { dispatch(a.getReferralidInit()); - dispatch(a.getReferralidDone(profile)); + dispatch(a.getReferralidDone(profile, tokenV3)); }, }; }