diff --git a/client/app/components/subscription-form.js b/client/app/components/subscription-form.js index 3737dd8b..0b1259a6 100644 --- a/client/app/components/subscription-form.js +++ b/client/app/components/subscription-form.js @@ -4,6 +4,7 @@ import { action, computed, set } from '@ember/object'; import fetch from 'fetch'; import ENV from 'labs-zap-search/config/environment'; import { getCommunityDistrictsByBorough } from '../helpers/lookup-community-district'; +import { validateEmail } from '../helpers/validate-email'; export default class SubscriptionFormComponent extends Component { communityDistrictsByBorough = {}; @@ -43,7 +44,7 @@ export default class SubscriptionFormComponent extends Component { } // eslint-disable-next-line ember/use-brace-expansion - @computed('isCommunityDistrict', 'args.subscriptions', 'args.email') + @computed('isCommunityDistrict', 'args.subscriptions', 'args.email', 'args.invalidEmailForSignup') get canBeSubmitted() { // If it's an update, subscriptions must be different, or they must have unchecked CD Updates if (this.args.isUpdate) { @@ -51,6 +52,9 @@ export default class SubscriptionFormComponent extends Component { if (!(Object.entries(this.args.subscriptions).find(([key, value]) => (this.previousSubscriptions[key] !== value)))) return false; } + // Disable signup with existing email addresses + if (this.args.invalidEmailForSignup) return false; + if ((this.isCommunityDistrict && !this.isAtLeastOneCommunityDistrictSelected)) return false; return this.isEmailValid && (this.args.subscriptions.CW @@ -59,23 +63,7 @@ export default class SubscriptionFormComponent extends Component { @computed('args.email') get isEmailValid() { - // eslint-disable-next-line no-useless-escape - const tester = /^[-!#$%&'*+\/0-9=?A-Z^_a-z{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; - if (!this.args.email) return false; - - if (this.args.email.length > 254) return false; - - const valid = tester.test(this.args.email); - if (!valid) return false; - - // Further checking of some things regex can't handle - const parts = this.args.email.split('@'); - if (parts[0].length > 64) return false; - - const domainParts = parts[1].split('.'); - if (domainParts.some(function(part) { return part.length > 63; })) return false; - - return true; + return validateEmail(this.args.email); } @computed('args.subscriptions') diff --git a/client/app/controllers/subscribe.js b/client/app/controllers/subscribe.js new file mode 100644 index 00000000..a1f195bd --- /dev/null +++ b/client/app/controllers/subscribe.js @@ -0,0 +1,95 @@ +import Controller from '@ember/controller'; +import { action, computed } from '@ember/object'; +import fetch from 'fetch'; +import ENV from 'labs-zap-search/config/environment'; +import { validateEmail } from '../helpers/validate-email'; + +export default class SubscribeController extends Controller { + lastEmailChecked = ''; + + emailAlreadyExists = false; + + emailNeedsConfirmation = false; + + startContinuouslyChecking = false; + + emailSent = false; + + @computed('emailAlreadyExists', 'emailNeedsConfirmation') + get invalidEmailForSignup() { + return (this.emailAlreadyExists || this.emailNeedsConfirmation); + } + + @action + async checkExistingEmail(event) { + const email = event.target.value; + if (email === this.lastEmailChecked) { return; } + this.set('lastEmailChecked', email); + this.set('startContinuouslyChecking', true); + + try { + const response = await fetch(`${ENV.host}/subscribers/email/${email}`); + const userData = await response.json(); + + if (userData.error) { + this.set('emailAlreadyExists', false); + this.set('emailNeedsConfirmation', false); + this.set('emailSent', false); + return; + } + + if (userData.confirmed === true) { + this.set('emailAlreadyExists', true); + this.set('emailNeedsConfirmation', false); + } else if (userData.confirmed === false) { + this.set('emailNeedsConfirmation', true); + this.set('emailAlreadyExists', false); + } + return; + } catch (error) { + // We will receive an error if: + // a) the user does not exist in Sendgrid, or + // b) their confirmed field is null. + // Either way, we don't need to log to console + this.set('emailAlreadyExists', false); + this.set('emailNeedsConfirmation', false); + this.set('emailSent', false); + } + } + + @action + continuouslyCheckEmail(event) { + if ((this.startContinuouslyChecking) || (validateEmail(event.target.value))) { this.checkExistingEmail(event); } + } + + @action + async sendEmail() { + if (this.emailAlreadyExists) { + // Run the script to update the email + try { + await fetch(`${ENV.host}/subscribers/${this.model.email}/modify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + this.set('emailSent', true); + } catch (error) { + console.error(error); // eslint-disable-line + } + } else if (this.emailNeedsConfirmation) { + // Run the script to confirm the email + try { + await fetch(`${ENV.host}/subscribers/${this.model.email}/resend-confirmation`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + this.set('emailSent', true); + } catch (error) { + console.error(error); // eslint-disable-line + } + } + } +} diff --git a/client/app/helpers/validate-email.js b/client/app/helpers/validate-email.js new file mode 100644 index 00000000..470cb762 --- /dev/null +++ b/client/app/helpers/validate-email.js @@ -0,0 +1,24 @@ +import { helper } from '@ember/component/helper'; + + +export function validateEmail(email) { + // eslint-disable-next-line no-useless-escape + const tester = /^[-!#$%&'*+\/0-9=?A-Z^_a-z{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; + if (!email) return false; + + if (email.length > 254) return false; + + const valid = tester.test(email); + if (!valid) return false; + + // Further checking of some things regex can't handle + const parts = email.split('@'); + if (parts[0].length > 64) return false; + + const domainParts = parts[1].split('.'); + if (domainParts.some(function(part) { return part.length > 63; })) return false; + + return true; +} + +export default helper(validateEmail); diff --git a/client/app/styles/app.scss b/client/app/styles/app.scss index 0ab8f05f..5b15b623 100644 --- a/client/app/styles/app.scss +++ b/client/app/styles/app.scss @@ -69,6 +69,7 @@ $completed-color: #a6cee3; @import 'modules/_m-search'; @import 'modules/_m-site-header'; @import 'modules/_m-statuses'; +@import 'modules/_m-subscribe'; @import 'modules/_m-subscribed'; @import 'modules/_m-subscribers-confirm'; @import 'modules/_m-subscription-form'; diff --git a/client/app/styles/modules/_m-subscribe.scss b/client/app/styles/modules/_m-subscribe.scss new file mode 100644 index 00000000..ca67ac83 --- /dev/null +++ b/client/app/styles/modules/_m-subscribe.scss @@ -0,0 +1,13 @@ +// -------------------------------------------------- +// Module: Subscribe Page +// -------------------------------------------------- + +.email-exists, .email-needs-confirmation { + padding-top: 1.5rem; + font-weight: 700; +} + +.email-sent { + color: $success-color; + font-weight: 700; +} \ No newline at end of file diff --git a/client/app/templates/subscribe.hbs b/client/app/templates/subscribe.hbs index e54240fe..c4bec57a 100644 --- a/client/app/templates/subscribe.hbs +++ b/client/app/templates/subscribe.hbs @@ -7,12 +7,21 @@ Get updated on milestones for all projects in any community district (CD) by signing up to receive emails on Zoning Application Portal. - +