From aaa9e0579a507366ab88b8d0e17056ab44c8c2a7 Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Fri, 10 Jul 2020 21:08:45 -0400 Subject: [PATCH 001/147] allow default timezone to be settable per-organization --- __test__/test_data/female_scientists.csv | 10 +++++----- docs/REFERENCE-environment_variables.md | 3 ++- src/components/CampaignTextingHoursForm.jsx | 9 ++++----- .../contact-loaders/test-fakedata/index.js | 13 ++++++++++++ src/lib/timezones.js | 20 ++++++++++++------- src/server/api/assignment.js | 8 +++++++- src/server/middleware/render-index.js | 6 +++++- 7 files changed, 49 insertions(+), 20 deletions(-) diff --git a/__test__/test_data/female_scientists.csv b/__test__/test_data/female_scientists.csv index 975cc187f..33bbb3039 100644 --- a/__test__/test_data/female_scientists.csv +++ b/__test__/test_data/female_scientists.csv @@ -276,11 +276,11 @@ Angeliki,Panajiotatou,2125550173,30273 Kathleen,I. Pritchard,2125550174,30274 Frieda,Robscheit-Robbins,2125550175,90275 Ora,Mendelsohn Rosen,2125550176,90276 -Una,Ryan,2125550177,90277 -Una,M. Ryan,2125550178,90278 -Velma,Scantlebury,2125550179,90279 -Lise,Thiry,2125550180,90280 -Helen,Rodríguez Trías,2125550181,90281 +Una,Ryan,2125550177,96704 +Una,M. Ryan,2125550178,96704 +Velma,Scantlebury,2125550179,96704 +Lise,Thiry,2125550180,96704 +Helen,Rodríguez Trías,2125550181,96704 Marie,Stopes,2125550182,90282 Elizabeth,M. Ward,2125550183,30283 Elsie,Widdowson,2125550184,30284 diff --git a/docs/REFERENCE-environment_variables.md b/docs/REFERENCE-environment_variables.md index db37cf5c6..0a628bfcf 100644 --- a/docs/REFERENCE-environment_variables.md +++ b/docs/REFERENCE-environment_variables.md @@ -25,9 +25,10 @@ | DB_TYPE | Database connection type for [Knex](http://knexjs.org/#Installation-client). _Options_: mysql, pg, sqlite3. _Default_: sqlite3. | | DB_USE_SSL | Boolean value to determine whether database connections should use SSL. _Default_: false. | | DEBUG_SCALING | Emit console.log on events related to scaling issues. _Default_: false. | -| DEFAULT_SERVICE | Default SMS service. _Options_: twilio, nexmo, fakeservice. | | DEFAULT_ORG | Set only with FIX_ORGLESS. Set to integer organization.id corresponding to the organization you want orgless users to be assigned to. | +| DEFAULT_SERVICE | Default SMS service. _Options_: twilio, nexmo, fakeservice. | | DEPRECATED_TEXTERUI | For a limited time, you can set DPRECATED_TEXTERUI=`GONE_SOON` to restore the old texter ui. This will be removed in an upcoming release, but is meant to give you time to update training materials and fallback, in case a major regression is found. You can also just add `?old=1` to the end of the url. | +| DEFAULT_TZ | Default timezone region for determining timezone when a contact timezone is unavailable | | DEV_APP_PORT | Port for development Webpack server. Required for development. | | DST_REFERENCE_TIMEZONE | Timezone to use to determine whether DST is in effect. If it's DST in this timezone, we assume it's DST everywhere. _Default_: "America/New_York". (The default will work for any campaign in the US. For example, if the campaign is in Australia, use "Australia/Sydney" or some other timezone in Australia. Note that DST is opposite in the northern and souther hemispheres.) | | EMAIL_FROM | Email from address. _Required to send email from either Mailgun **or** a custom SMTP server_. | diff --git a/src/components/CampaignTextingHoursForm.jsx b/src/components/CampaignTextingHoursForm.jsx index 18a1720e2..952d5488b 100644 --- a/src/components/CampaignTextingHoursForm.jsx +++ b/src/components/CampaignTextingHoursForm.jsx @@ -169,13 +169,12 @@ export default class CampaignTextingHoursForm extends React.Component { "Override organization texting hours?" )} + {this.addToggleFormField( + "textingHoursEnforced", + "Texting hours enforced?" + )} {this.props.formValues.overrideOrganizationTextingHours ? (
- {this.addToggleFormField( - "textingHoursEnforced", - "Texting hours enforced?" - )} - {this.props.formValues.textingHoursEnforced ? (
{this.addAutocompleteFormField( diff --git a/src/integrations/contact-loaders/test-fakedata/index.js b/src/integrations/contact-loaders/test-fakedata/index.js index 43b8ed58c..e6f1d776b 100644 --- a/src/integrations/contact-loaders/test-fakedata/index.js +++ b/src/integrations/contact-loaders/test-fakedata/index.js @@ -113,6 +113,17 @@ export async function processContactLoad(job, maxContacts, organization) { return; // bail early } const areaCodes = ["213", "323", "212", "718", "646", "661"]; + // FUTURE -- maybe based on campaign default use 'surrounding' offsets + const timezones = [ + "-12_1", + "-11_0", + "-5_1", + "-4_1", + "0_0", + "5_0", + "10_0", + "" + ]; const contactCount = Math.min( contactData.requestContactCount || 0, maxContacts ? maxContacts : areaCodes.length * 100, @@ -130,6 +141,8 @@ export async function processContactLoad(job, maxContacts, organization) { cell: `+1${ac}555${suffix}`, zip: "10011", custom_fields: "{}", + timezone_offset: + timezones[parseInt(Math.random() * timezones.length, 10)], message_status: "needsMessage", campaign_id: campaignId }); diff --git a/src/lib/timezones.js b/src/lib/timezones.js index 440bfeaec..a0f1788fe 100644 --- a/src/lib/timezones.js +++ b/src/lib/timezones.js @@ -5,6 +5,7 @@ import { getProcessEnvDstReferenceTimezone } from "../lib/tz-helpers"; import { DstHelper } from "./dst-helper"; +import { getConfig } from "../server/api/lib/config"; const TIMEZONE_CONFIG = { missingTimeZone: { @@ -93,9 +94,12 @@ export const getSendBeforeTimeUtc = ( return null; } - if (getProcessEnvTz()) { + const defaultTimezone = getProcessEnvTz( + getConfig("DEFAULT_TZ", organization) + ); + if (defaultTimezone) { return getUtcFromTimezoneAndHour( - getProcessEnvTz(), + defaultTimezone, organization.textingHoursEnd ); } @@ -160,6 +164,7 @@ export const isBetweenTextingHours = (offsetData, config) => { return true; } } else if (!config.textingHoursEnforced) { + // organization setting return true; } @@ -181,15 +186,16 @@ export const isBetweenTextingHours = (offsetData, config) => { ); } - if (getProcessEnvTz()) { - const today = moment.tz(getProcessEnvTz()).format("YYYY-MM-DD"); + const localTimezone = getProcessEnvTz(config.defaultTimezone); + if (!offsetData && localTimezone) { + const today = moment.tz(localTimezone).format("YYYY-MM-DD"); const start = moment - .tz(`${today}`, getProcessEnvTz()) + .tz(`${today}`, localTimezone) .add(config.textingHoursStart, "hours"); const stop = moment - .tz(`${today}`, getProcessEnvTz()) + .tz(`${today}`, localTimezone) .add(config.textingHoursEnd, "hours"); - return moment.tz(getProcessEnvTz()).isBetween(start, stop, null, "[]"); + return moment.tz(localTimezone).isBetween(start, stop, null, "[]"); } return isOffsetBetweenTextingHours( diff --git a/src/server/api/assignment.js b/src/server/api/assignment.js index 4e766381e..8e02302a4 100644 --- a/src/server/api/assignment.js +++ b/src/server/api/assignment.js @@ -1,5 +1,6 @@ import { mapFieldsToModel } from "./lib/utils"; import { Assignment, r, cacheableData } from "../models"; +import { getConfig } from "./lib/config"; import { getOffsets, defaultTimezoneIsBetweenTextingHours } from "../../lib"; export function addWhereClauseForContactsFilterMessageStatusIrrespectiveOfPastDue( @@ -38,7 +39,12 @@ export function getContacts( const pastDue = campaign.due_by && Number(campaign.due_by) + 24 * 60 * 60 * 1000 < Number(new Date()); - const config = { textingHoursStart, textingHoursEnd, textingHoursEnforced }; + const config = { + textingHoursStart, + textingHoursEnd, + textingHoursEnforced, + defaultTimezone: getConfig("DEFAULT_TZ", organization) + }; if (campaign.override_organization_texting_hours) { const textingHoursStart = campaign.texting_hours_start; diff --git a/src/server/middleware/render-index.js b/src/server/middleware/render-index.js index fe3049d8d..7dcb71272 100644 --- a/src/server/middleware/render-index.js +++ b/src/server/middleware/render-index.js @@ -81,7 +81,11 @@ export default function renderIndex(html, css, assetMap) { window.TERMS_REQUIRE=${getConfig("TERMS_REQUIRE", null, { truthy: 1 }) || false} - window.TZ="${process.env.TZ || ""}" + window.TZ="${ + "DEFAULT_TZ" in process.env + ? process.env.DEFAULT_TZ + : process.env.TZ || "" + }" window.CONTACT_LOADERS="${process.env.CONTACT_LOADERS || "csv-upload,test-fakedata,datawarehouse"}" window.DST_REFERENCE_TIMEZONE="${process.env.DST_REFERENCE_TIMEZONE || From e3035f047cfa7c2d033dd17410c6a057d2a0b5b3 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 6 Aug 2020 10:21:10 -0500 Subject: [PATCH 002/147] My Campaign Integration --- .gitignore | 1 + src/integrations/contact-loaders/ngpvan/util.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 47ae2dd66..6a5b1fd58 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ CONFIG_FILE.json scratch/ cypress/screenshots cypress/videos +/webpack/bundle.js diff --git a/src/integrations/contact-loaders/ngpvan/util.js b/src/integrations/contact-loaders/ngpvan/util.js index 96f3e5e78..b11cb100f 100644 --- a/src/integrations/contact-loaders/ngpvan/util.js +++ b/src/integrations/contact-loaders/ngpvan/util.js @@ -13,7 +13,7 @@ export default class Van { ); } - const buffer = Buffer.from(`${appName}:${apiKey}|0`); + const buffer = Buffer.from(`${appName}:${apiKey}|1`); return `Basic ${buffer.toString("base64")}`; }; From 33ecd76438edcba4df002ef872cb1b8a60e80118 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 6 Aug 2020 13:24:50 -0500 Subject: [PATCH 003/147] Fix logging --- src/lib/log.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/log.js b/src/lib/log.js index baf85be44..0ef98d32f 100644 --- a/src/lib/log.js +++ b/src/lib/log.js @@ -1,6 +1,6 @@ import minilog from "minilog"; import { isClient } from "./is-client"; -const rollbar = require("rollbar"); +const Rollbar = require("rollbar"); let logInstance = null; if (isClient()) { @@ -20,8 +20,11 @@ if (isClient()) { process.env.NODE_ENV === "production" && process.env.ROLLBAR_ACCESS_TOKEN ) { - enableRollbar = true; - rollbar.init(process.env.ROLLBAR_ACCESS_TOKEN); + rollbar = new Rollbar({ + accessToken: process.env.ROLLBAR_ACCESS_TOKEN, + captureUncaught: true, + captureUnhandledRejections: true + }); } minilog.suggest.deny( @@ -39,11 +42,11 @@ if (isClient()) { logInstance.error = err => { if (enableRollbar) { if (typeof err === "object") { - rollbar.handleError(err); + rollbar.error(err); } else if (typeof err === "string") { - rollbar.reportMessage(err); + rollbar.log(err); } else { - rollbar.reportMessage("Got backend error with no error message"); + rollbar.log("Got backend error with no error message"); } } From d050fa0b90fe869326a3ab8e8044635851e1aa19 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 6 Aug 2020 13:59:13 -0500 Subject: [PATCH 004/147] fixing logs again --- src/lib/log.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/log.js b/src/lib/log.js index 0ef98d32f..a0670ada6 100644 --- a/src/lib/log.js +++ b/src/lib/log.js @@ -15,7 +15,8 @@ if (isClient()) { existingErrorLogger.call(...errObj); }; } else { - let enableRollbar = false; + let rollbar = null; + if ( process.env.NODE_ENV === "production" && process.env.ROLLBAR_ACCESS_TOKEN @@ -40,7 +41,7 @@ if (isClient()) { logInstance = minilog("backend"); const existingErrorLogger = logInstance.error; logInstance.error = err => { - if (enableRollbar) { + if (rollbar) { if (typeof err === "object") { rollbar.error(err); } else if (typeof err === "string") { From 0c51a7caa4f9152ff70f32962ed9597587cd72f5 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Thu, 6 Aug 2020 14:34:36 -0500 Subject: [PATCH 005/147] Express middleware --- src/lib/log.js | 3 ++- src/server/index.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/lib/log.js b/src/lib/log.js index a0670ada6..e9eaaf882 100644 --- a/src/lib/log.js +++ b/src/lib/log.js @@ -24,7 +24,8 @@ if (isClient()) { rollbar = new Rollbar({ accessToken: process.env.ROLLBAR_ACCESS_TOKEN, captureUncaught: true, - captureUnhandledRejections: true + captureUnhandledRejections: true, + endpoint: "https://api.rollbar.com/api/1/item" }); } diff --git a/src/server/index.js b/src/server/index.js index 7268b4e1e..e33250411 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -20,6 +20,7 @@ import { setupUserNotificationObservers } from "./notifications"; import { twiml } from "twilio"; import { existsSync } from "fs"; import { rawAllMethods } from "../integrations/contact-loaders"; +import Rollbar from "rollbar"; process.on("uncaughtException", ex => { log.error(ex); @@ -96,6 +97,16 @@ if (process.env.SIMULATE_DELAY_MILLIS) { }); } +if (process.env.NODE_ENV === "production" && process.env.ROLLBAR_ACCESS_TOKEN) { + const rollbar = new Rollbar({ + accessToken: process.env.ROLLBAR_ACCESS_TOKEN, + captureUncaught: true, + captureUnhandledRejections: true + }); + + app.use(rollbar.errorHandler()); +} + // give contact loaders a chance const configuredIngestMethods = rawAllMethods(); Object.keys(configuredIngestMethods).forEach(ingestMethodName => { From 18098caf21c0764297359dbd08e19397a6c8bce8 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Fri, 21 Aug 2020 20:59:46 -0500 Subject: [PATCH 006/147] Timezone fixes --- .env.example | 2 +- __test__/e2e/.env.e2e | 2 +- __test__/lib/dst-helper.test.js | 28 ++++------- __test__/lib/timezones.test.js | 64 ++++++++++++++----------- __test__/lib/tz-helpers.test.js | 2 +- app.json | 2 +- deploy/lambda-env.json | 4 +- deploy/spoke-pm2.config.js.template | 2 +- docs/REFERENCE-environment_variables.md | 2 +- docs/TEXTING-HOURS-ENFORCEMENT.md | 2 +- jest.config.js | 2 +- src/lib/__mocks__/tz-helpers.js | 2 +- src/lib/tz-helpers.js | 2 +- src/server/api/schema.js | 3 +- src/server/middleware/render-index.js | 2 +- 15 files changed, 61 insertions(+), 60 deletions(-) diff --git a/.env.example b/.env.example index 58c01cdb6..0f70f0059 100644 --- a/.env.example +++ b/.env.example @@ -47,7 +47,7 @@ EMAIL_HOST_USER= EMAIL_HOST_PORT= EMAIL_FROM= TWILIO_MESSAGE_VALIDITY_PERIOD= -DST_REFERENCE_TIMEZONE='America/New_York' +DST_REFERENCE_TIMEZONE='US/Eastern' PASSPORT_STRATEGY=local EXPERIMENTAL_TAGS=1 TEXTER_SIDEBOXES=celebration-gif,default-dynamicassignment,default-releasecontacts,contact-reference,tag-contact diff --git a/__test__/e2e/.env.e2e b/__test__/e2e/.env.e2e index d379ce5d5..8e52123ea 100644 --- a/__test__/e2e/.env.e2e +++ b/__test__/e2e/.env.e2e @@ -40,4 +40,4 @@ EMAIL_HOST_USER= EMAIL_HOST_PORT= EMAIL_FROM= TWILIO_MESSAGE_VALIDITY_PERIOD= -DST_REFERENCE_TIMEZONE='America/New_York' +DST_REFERENCE_TIMEZONE='US/Eastern' diff --git a/__test__/lib/dst-helper.test.js b/__test__/lib/dst-helper.test.js index f505d5b63..32ebb02b1 100644 --- a/__test__/lib/dst-helper.test.js +++ b/__test__/lib/dst-helper.test.js @@ -10,26 +10,18 @@ describe("test DstHelper", () => { it("helps us figure out if we're in DST in February in New York", () => { MockDate.set("2018-02-01T15:00:00Z"); - let d = new DateTime( - new Date(), - DateFunctions.Get, - zone("America/New_York") - ); - expect(DstHelper.isOffsetDst(d.offset(), "America/New_York")).toBeFalsy(); - expect(DstHelper.isDateTimeDst(d, "America/New_York")).toBeFalsy(); - expect(DstHelper.isDateDst(new Date(), "America/New_York")).toBeFalsy(); + let d = new DateTime(new Date(), DateFunctions.Get, zone("US/Eastern")); + expect(DstHelper.isOffsetDst(d.offset(), "US/Eastern")).toBeFalsy(); + expect(DstHelper.isDateTimeDst(d, "US/Eastern")).toBeFalsy(); + expect(DstHelper.isDateDst(new Date(), "US/Eastern")).toBeFalsy(); }); it("helps us figure out if we're in DST in July in New York", () => { MockDate.set("2018-07-21T15:00:00Z"); - let d = new DateTime( - new Date(), - DateFunctions.Get, - zone("America/New_York") - ); - expect(DstHelper.isOffsetDst(d.offset(), "America/New_York")).toBeTruthy(); - expect(DstHelper.isDateTimeDst(d, "America/New_York")).toBeTruthy(); - expect(DstHelper.isDateDst(new Date(), "America/New_York")).toBeTruthy(); + let d = new DateTime(new Date(), DateFunctions.Get, zone("US/Eastern")); + expect(DstHelper.isOffsetDst(d.offset(), "US/Eastern")).toBeTruthy(); + expect(DstHelper.isDateTimeDst(d, "US/Eastern")).toBeTruthy(); + expect(DstHelper.isDateDst(new Date(), "US/Eastern")).toBeTruthy(); }); it("helps us figure out if we're in DST in February in Sydney", () => { @@ -89,8 +81,8 @@ describe("test DstHelper", () => { }); it("correctly reports a timezone's offset and whether it has DST", () => { - expect(DstHelper.getTimezoneOffsetHours("America/New_York")).toEqual(-5); - expect(DstHelper.timezoneHasDst("America/New_York")).toBeTruthy(); + expect(DstHelper.getTimezoneOffsetHours("US/Eastern")).toEqual(-5); + expect(DstHelper.timezoneHasDst("US/Eastern")).toBeTruthy(); expect(DstHelper.getTimezoneOffsetHours("US/Arizona")).toEqual(-7); expect(DstHelper.timezoneHasDst("US/Arizona")).toBeFalsy(); expect(DstHelper.getTimezoneOffsetHours("Europe/Paris")).toEqual(1); diff --git a/__test__/lib/timezones.test.js b/__test__/lib/timezones.test.js index 895a2bab5..43dff5f62 100644 --- a/__test__/lib/timezones.test.js +++ b/__test__/lib/timezones.test.js @@ -101,7 +101,7 @@ const buildIsBetweenTextingHoursExpectWithNoOffset = (start, end) => { 0, 0, false, - makeCampignTextingHoursConfig(true, start, end, "America/New_York") + makeCampignTextingHoursConfig(true, start, end, "US/Eastern") ) ) ); @@ -566,7 +566,11 @@ describe("test defaultTimezoneIsBetweenTextingHours", () => { describe("test convertOffsetsToStrings", () => { it("works", () => { - let test_offsets = [[1, true], [2, false], [-1, true]]; + let test_offsets = [ + [1, true], + [2, false], + [-1, true] + ]; let strings_returned = convertOffsetsToStrings(test_offsets); expect(strings_returned).toHaveLength(3); expect(strings_returned[0]).toBe("1_1"); @@ -664,7 +668,7 @@ describe("test getContactTimezone", () => { true, 14, 16, - "America/New_York" + "US/Eastern" ), {} ) @@ -885,7 +889,11 @@ describe("test defaultTimezoneIsBetweenTextingHours", () => { describe("test convertOffsetsToStrings", () => { it("works", () => { - let test_offsets = [[1, true], [2, false], [-1, true]]; + let test_offsets = [ + [1, true], + [2, false], + [-1, true] + ]; let strings_returned = convertOffsetsToStrings(test_offsets); expect(strings_returned).toHaveLength(3); expect(strings_returned[0]).toBe("1_1"); @@ -983,7 +991,7 @@ describe("test getContactTimezone", () => { true, 14, 16, - "America/New_York" + "US/Eastern" ), {} ) @@ -1004,7 +1012,7 @@ describe("test getContactTimezone", () => { true, 14, 16, - "America/New_York" + "US/Eastern" ), {} ) @@ -1024,7 +1032,7 @@ describe("test getContactTimezone", () => { true, 14, 16, - "America/New_York" + "US/Eastern" ), {} ) @@ -1044,37 +1052,37 @@ describe("test getUtcFromOffsetAndHour", () => { it("returns the correct UTC during northern hemisphere summer", () => { MockDate.set("2018-07-01T11:00:00.000-05:00"); - expect( - getUtcFromOffsetAndHour(-5, true, 12, "America/New_York").unix() - ).toEqual(moment("2018-07-01T16:00:00.000Z").unix()); + expect(getUtcFromOffsetAndHour(-5, true, 12, "US/Eastern").unix()).toEqual( + moment("2018-07-01T16:00:00.000Z").unix() + ); }); it("returns the correct UTC during northern hemisphere summer with result being next day", () => { MockDate.set("2018-07-01T11:00:00.000-05:00"); - expect( - getUtcFromOffsetAndHour(-5, true, 23, "America/New_York").unix() - ).toEqual(moment("2018-07-02T03:00:00.000Z").unix()); + expect(getUtcFromOffsetAndHour(-5, true, 23, "US/Eastern").unix()).toEqual( + moment("2018-07-02T03:00:00.000Z").unix() + ); }); it("returns the correct UTC during northern hemisphere winter", () => { MockDate.set("2018-02-01T11:00:00.000-05:00"); - expect( - getUtcFromOffsetAndHour(-5, true, 12, "America/New_York").unix() - ).toEqual(moment("2018-02-01T17:00:00.000Z").unix()); + expect(getUtcFromOffsetAndHour(-5, true, 12, "US/Eastern").unix()).toEqual( + moment("2018-02-01T17:00:00.000Z").unix() + ); }); it("returns the correct UTC during northern hemisphere summer if offset doesn't have DST", () => { MockDate.set("2018-07-01T11:00:00.000-05:00"); - expect( - getUtcFromOffsetAndHour(-5, false, 12, "America/New_York").unix() - ).toEqual(moment("2018-07-01T17:00:00.000Z").unix()); + expect(getUtcFromOffsetAndHour(-5, false, 12, "US/Eastern").unix()).toEqual( + moment("2018-07-01T17:00:00.000Z").unix() + ); }); it("returns the correct UTC during northern hemisphere winter if offset doesn't have DST", () => { MockDate.set("2018-02-01T11:00:00.000-05:00"); - expect( - getUtcFromOffsetAndHour(-5, false, 12, "America/New_York").unix() - ).toEqual(moment("2018-02-01T17:00:00.000Z").unix()); + expect(getUtcFromOffsetAndHour(-5, false, 12, "US/Eastern").unix()).toEqual( + moment("2018-02-01T17:00:00.000Z").unix() + ); }); }); @@ -1085,21 +1093,21 @@ describe("test getUtcFromTimezoneAndHour", () => { it("returns the correct UTC during northern hemisphere summer", () => { MockDate.set("2018-07-01T11:00:00.000-05:00"); - expect(getUtcFromTimezoneAndHour("America/New_York", 12).unix()).toEqual( + expect(getUtcFromTimezoneAndHour("US/Eastern", 12).unix()).toEqual( moment("2018-07-01T16:00:00.000Z").unix() ); }); it("returns the correct UTC during northern hemisphere summer with result being next day", () => { MockDate.set("2018-07-01T11:00:00.000-05:00"); - expect(getUtcFromTimezoneAndHour("America/New_York", 23).unix()).toEqual( + expect(getUtcFromTimezoneAndHour("US/Eastern", 23).unix()).toEqual( moment("2018-07-02T03:00:00.000Z").unix() ); }); it("returns the correct UTC during northern hemisphere winter", () => { MockDate.set("2018-02-01T11:00:00.000-05:00"); - expect(getUtcFromTimezoneAndHour("America/New_York", 12).unix()).toEqual( + expect(getUtcFromTimezoneAndHour("US/Eastern", 12).unix()).toEqual( moment("2018-02-01T17:00:00.000Z").unix() ); }); @@ -1171,7 +1179,7 @@ describe("test getSendBeforeTimewUtc", () => { overrideOrganizationTextingHours: true, textingHoursEnforced: true, textingHoursEnd: 21, - timezone: "America/New_York" + timezone: "US/Eastern" } ).unix() ).toEqual(moment("2018-09-04T01:00:00.000Z").unix()); @@ -1190,14 +1198,14 @@ describe("test getSendBeforeTimewUtc", () => { overrideOrganizationTextingHours: true, textingHoursEnforced: true, textingHoursEnd: 21, - timezone: "America/New_York" + timezone: "US/Eastern" } ).unix() ).toEqual(moment("2018-09-04T01:00:00.000Z").unix()); }); it("returns correct time if campaign does not override and TZ is set", () => { - tzHelpers.getProcessEnvTz.mockImplementation(() => "America/New_York"); + tzHelpers.getProcessEnvTz.mockImplementation(() => "US/Eastern"); expect( getSendBeforeTimeUtc( {}, diff --git a/__test__/lib/tz-helpers.test.js b/__test__/lib/tz-helpers.test.js index 89b60a474..40c74e3e3 100644 --- a/__test__/lib/tz-helpers.test.js +++ b/__test__/lib/tz-helpers.test.js @@ -4,6 +4,6 @@ jest.unmock("../../src/lib/tz-helpers"); describe("test getProcessEnvDstReferenceTimezone", () => { it("works", () => { - expect(getProcessEnvDstReferenceTimezone()).toEqual("America/New_York"); + expect(getProcessEnvDstReferenceTimezone()).toEqual("US/Eastern"); }); }); diff --git a/app.json b/app.json index 59b31b6e0..ed138218c 100644 --- a/app.json +++ b/app.json @@ -163,7 +163,7 @@ "DST_REFERENCE_TIMEZONE": { "description": "Timezone to use to determine whether DST is in effect for a date", "required": true, - "value": "America/New_York" + "value": "US/Eastern" }, "HEROKU_APP_NAME": { diff --git a/deploy/lambda-env.json b/deploy/lambda-env.json index d4c401842..56b15faeb 100644 --- a/deploy/lambda-env.json +++ b/deploy/lambda-env.json @@ -38,6 +38,6 @@ "ROLLBAR_CLIENT_TOKEN": "set_this_in_production", "ROLLBAR_ACCESS_TOKEN": "set_this_in_production", "ROLLBAR_ENDPOINT": "https://api.rollbar.com/api/1/item/", - "DST_REFERENCE_TIMEZONE": "America/New_York", - "TZ": "America/New_York" + "DST_REFERENCE_TIMEZONE": "US/Eastern", + "TZ": "US/Eastern" } diff --git a/deploy/spoke-pm2.config.js.template b/deploy/spoke-pm2.config.js.template index ce50c39e1..e19c6db45 100644 --- a/deploy/spoke-pm2.config.js.template +++ b/deploy/spoke-pm2.config.js.template @@ -43,7 +43,7 @@ const env_production = { ROLLBAR_ACCESS_TOKEN:'', ROLLBAR_ENDPOINT:'https://api.rollbar.com/api/1/item/', ALLOW_SEND_ALL: false, - DST_REFERENCE_TIMEZONE: 'America/New_York' + DST_REFERENCE_TIMEZONE: 'US/Eastern' } module.exports = { diff --git a/docs/REFERENCE-environment_variables.md b/docs/REFERENCE-environment_variables.md index 7707ac99c..f2d78fd72 100644 --- a/docs/REFERENCE-environment_variables.md +++ b/docs/REFERENCE-environment_variables.md @@ -31,7 +31,7 @@ | DEFAULT_ORG | Set only with FIX_ORGLESS. Set to integer organization.id corresponding to the organization you want orgless users to be assigned to. | | DEFAULT_RESPONSEWINDOW | Default number of hours after when a campaign's contacts that need a response (after they reply) -- this is changeable per-campaign, but this sets the default. | | DEV_APP_PORT | Port for development Webpack server. Required for development. | -| DST_REFERENCE_TIMEZONE | Timezone to use to determine whether DST is in effect. If it's DST in this timezone, we assume it's DST everywhere. _Default_: "America/New_York". (The default will work for any campaign in the US. For example, if the campaign is in Australia, use "Australia/Sydney" or some other timezone in Australia. Note that DST is opposite in the northern and souther hemispheres.) | +| DST_REFERENCE_TIMEZONE | Timezone to use to determine whether DST is in effect. If it's DST in this timezone, we assume it's DST everywhere. _Default_: "US/Eastern". (The default will work for any campaign in the US. For example, if the campaign is in Australia, use "Australia/Sydney" or some other timezone in Australia. Note that DST is opposite in the northern and souther hemispheres.) | | EMAIL_FROM | Email from address. _Required to send email from either Mailgun **or** a custom SMTP server_. | | EMAIL_HOST | Email server host. _Required for custom SMTP server usage_. | | EMAIL_HOST_PASSWORD | Email server password. _Required for custom SMTP server usage_. | diff --git a/docs/TEXTING-HOURS-ENFORCEMENT.md b/docs/TEXTING-HOURS-ENFORCEMENT.md index 0ac9998f0..bde6615f2 100644 --- a/docs/TEXTING-HOURS-ENFORCEMENT.md +++ b/docs/TEXTING-HOURS-ENFORCEMENT.md @@ -22,7 +22,7 @@ Spoke will not send texts to contacts before the start time or after the end tim If the `TZ` environment variable is set, Spoke will assume that all contacts are located in the time zone specified by the variable. The current time in that time zone -- with Daylight Savings applied if it is summer in that area and the time zone has Daylight Savings Time -- is considered the current time for purposes of deciding whether it is OK to send texts. -The timezone in New York City is specified by the string `America/New_York`. Other time zone names are listed [here.](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) +The timezone in New York City is specified by the string `US/Eastern`. Other time zone names are listed [here.](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) ## Contact ZIP code diff --git a/jest.config.js b/jest.config.js index 03a1e4228..2e106e557 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,7 +19,7 @@ module.exports = { JOBS_SAME_PROCESS: "1", RETHINK_KNEX_NOREFS: "1", // avoids db race conditions DEFAULT_SERVICE: "fakeservice", - DST_REFERENCE_TIMEZONE: "America/New_York", + DST_REFERENCE_TIMEZONE: "US/Eastern", DATABASE_SETUP_TEARDOWN_TIMEOUT: 60000, PASSPORT_STRATEGY: "local", SESSION_SECRET: "it is JUST a test! -- it better be!", diff --git a/src/lib/__mocks__/tz-helpers.js b/src/lib/__mocks__/tz-helpers.js index e3f934cbe..a7ed63207 100644 --- a/src/lib/__mocks__/tz-helpers.js +++ b/src/lib/__mocks__/tz-helpers.js @@ -1,5 +1,5 @@ const tzHelpers = jest.genMockFromModule("../tz-helpers"); -tzHelpers.getProcessEnvDstReferenceTimezone = () => "America/New_York"; +tzHelpers.getProcessEnvDstReferenceTimezone = () => "US/Eastern"; module.exports = tzHelpers; diff --git a/src/lib/tz-helpers.js b/src/lib/tz-helpers.js index dedf354dc..30924419c 100644 --- a/src/lib/tz-helpers.js +++ b/src/lib/tz-helpers.js @@ -18,6 +18,6 @@ export function getProcessEnvDstReferenceTimezone() { return ( process.env.DST_REFERENCE_TIMEZONE || global.DST_REFERENCE_TIMEZONE || - "America/New_York" + "US/Eastern" ); } diff --git a/src/server/api/schema.js b/src/server/api/schema.js index dc96b021d..545e811aa 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -760,7 +760,8 @@ const rootMutations = { response_window: getConfig("DEFAULT_RESPONSEWINDOW", organization, { default: 48 }), - use_own_messaging_service: false + use_own_messaging_service: false, + timezone: getConfig("DST_TIMEZONE_REFERENCE", organization) }); const newCampaign = await campaignInstance.save(); await r.knex("campaign_admin").insert({ diff --git a/src/server/middleware/render-index.js b/src/server/middleware/render-index.js index ba9eb0450..eba5f45b9 100644 --- a/src/server/middleware/render-index.js +++ b/src/server/middleware/render-index.js @@ -87,7 +87,7 @@ export default function renderIndex(html, css, assetMap) { window.CONTACT_LOADERS="${process.env.CONTACT_LOADERS || "csv-upload,test-fakedata,datawarehouse"}" window.DST_REFERENCE_TIMEZONE="${process.env.DST_REFERENCE_TIMEZONE || - "America/New_York"}" + "US/Eastern"}" window.PASSPORT_STRATEGY="${process.env.PASSPORT_STRATEGY || "auth0"}" window.PEOPLE_PAGE_CAMPAIGN_FILTER_SORT = "${process.env .PEOPLE_PAGE_CAMPAIGN_FILTER_SORT || ""}" From 5e560f017a17c66c038d72dbad88b211e6d640d2 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Sat, 22 Aug 2020 12:06:21 -0500 Subject: [PATCH 007/147] More timezone fixes --- app.json | 6 ++++++ src/lib/index.js | 5 ++++- src/server/middleware/render-index.js | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index ed138218c..2cf2cb394 100644 --- a/app.json +++ b/app.json @@ -166,6 +166,12 @@ "value": "US/Eastern" }, + "DEFAULT_TZ": { + "description": "Timezone", + "required": true, + "value": "US/Eastern" + }, + "HEROKU_APP_NAME": { "description": "Name of your Heroku app (not used if BASE_URL is set)", "required": false diff --git a/src/lib/index.js b/src/lib/index.js index 20d59bab5..6095c6ee5 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -16,7 +16,10 @@ export { getUtcFromOffsetAndHour, getSendBeforeTimeUtc } from "./timezones"; -export { getProcessEnvTz } from "./tz-helpers"; +export { + getProcessEnvTz, + getProcessEnvDstReferenceTimezone +} from "./tz-helpers"; export { DstHelper } from "./dst-helper"; export { isClient } from "./is-client"; import { log } from "./log"; diff --git a/src/server/middleware/render-index.js b/src/server/middleware/render-index.js index eba5f45b9..299b806a9 100644 --- a/src/server/middleware/render-index.js +++ b/src/server/middleware/render-index.js @@ -1,4 +1,5 @@ import { hasConfig, getConfig } from "../api/lib/config"; +import { getProcessEnvTz, getProcessEnvDstReferenceTimezone } from "../../lib"; const canGoogleImport = hasConfig("GOOGLE_SECRET"); @@ -83,10 +84,10 @@ export default function renderIndex(html, css, assetMap) { window.TERMS_REQUIRE=${getConfig("TERMS_REQUIRE", null, { truthy: 1 }) || false} - window.TZ="${process.env.TZ || ""}" + window.TZ="${getProcessEnvTz() || ""}" window.CONTACT_LOADERS="${process.env.CONTACT_LOADERS || "csv-upload,test-fakedata,datawarehouse"}" - window.DST_REFERENCE_TIMEZONE="${process.env.DST_REFERENCE_TIMEZONE || + window.DST_REFERENCE_TIMEZONE="${getProcessEnvDstReferenceTimezone() || "US/Eastern"}" window.PASSPORT_STRATEGY="${process.env.PASSPORT_STRATEGY || "auth0"}" window.PEOPLE_PAGE_CAMPAIGN_FILTER_SORT = "${process.env From 844c752772183c6f517dc178c9e10e9ed6ce40fd Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Sat, 22 Aug 2020 12:21:33 -0500 Subject: [PATCH 008/147] example --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 0f70f0059..dcda922cd 100644 --- a/.env.example +++ b/.env.example @@ -48,6 +48,7 @@ EMAIL_HOST_PORT= EMAIL_FROM= TWILIO_MESSAGE_VALIDITY_PERIOD= DST_REFERENCE_TIMEZONE='US/Eastern' +DEFAULT_TZ='US/Eastern' PASSPORT_STRATEGY=local EXPERIMENTAL_TAGS=1 TEXTER_SIDEBOXES=celebration-gif,default-dynamicassignment,default-releasecontacts,contact-reference,tag-contact From 531e307996ce9f302f03393aa0dc505fedc1a973 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Sat, 22 Aug 2020 12:21:55 -0500 Subject: [PATCH 009/147] Fixing main --- src/extensions/contact-loaders/ngpvan/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/contact-loaders/ngpvan/util.js b/src/extensions/contact-loaders/ngpvan/util.js index b11cb100f..96f3e5e78 100644 --- a/src/extensions/contact-loaders/ngpvan/util.js +++ b/src/extensions/contact-loaders/ngpvan/util.js @@ -13,7 +13,7 @@ export default class Van { ); } - const buffer = Buffer.from(`${appName}:${apiKey}|1`); + const buffer = Buffer.from(`${appName}:${apiKey}|0`); return `Basic ${buffer.toString("base64")}`; }; From 940b162d6bd9704c4d024e2455db7214009f4a62 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Sat, 22 Aug 2020 17:20:42 -0500 Subject: [PATCH 010/147] Fix send last past message --- src/components/AssignmentTexter/ContactController.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/AssignmentTexter/ContactController.jsx b/src/components/AssignmentTexter/ContactController.jsx index 20937ac03..3907d3057 100644 --- a/src/components/AssignmentTexter/ContactController.jsx +++ b/src/components/AssignmentTexter/ContactController.jsx @@ -284,6 +284,8 @@ export class ContactController extends React.Component { this.setState({ finishedContactId: contactId }, () => { if (!this.props.reviewContactId) { this.props.refreshData(); + this.clearContactIdOldData(contactId); + this.updateCurrentContactIndex(this.state.currentContactIndex); } }); } From c291596756311d65f3df528455bcd689196067db Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Mon, 24 Aug 2020 19:46:44 -0500 Subject: [PATCH 011/147] Fixing config name --- src/server/api/schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 545e811aa..d86ae8546 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -761,7 +761,7 @@ const rootMutations = { default: 48 }), use_own_messaging_service: false, - timezone: getConfig("DST_TIMEZONE_REFERENCE", organization) + timezone: getConfig("DST_REFERENCE_TIMEZONE", organization) }); const newCampaign = await campaignInstance.save(); await r.knex("campaign_admin").insert({ From b07ac0336baaf5bd44133775f99c852459242eab Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Tue, 25 Aug 2020 13:47:20 -0500 Subject: [PATCH 012/147] Texter Two-Click Message Review --- .../IncomingMessageList/MessageResponse.jsx | 19 ++++++++++---- src/components/SendButton.jsx | 25 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/components/IncomingMessageList/MessageResponse.jsx b/src/components/IncomingMessageList/MessageResponse.jsx index 2129b0fda..f1a1d74c3 100644 --- a/src/components/IncomingMessageList/MessageResponse.jsx +++ b/src/components/IncomingMessageList/MessageResponse.jsx @@ -24,7 +24,8 @@ class MessageResponse extends Component { this.state = { messageText: "", isSending: false, - sendError: "" + sendError: "", + doneFirstClick: false }; this.handleCloseErrorDialog = this.handleCloseErrorDialog.bind(this); @@ -43,14 +44,21 @@ class MessageResponse extends Component { handleMessageFormChange = ({ messageText }) => this.setState({ messageText }); handleMessageFormSubmit = async ({ messageText }) => { - const { campaignContactId } = this.props.conversation; - const message = this.createMessageToContact(messageText); if (this.state.isSending) { return; // stops from multi-send } + + if (window.TEXTER_TWOCLICK && !this.state.doneFirstClick) { + this.setState({ doneFirstClick: true }); // Enforce TEXTER_TWOCLICK + return; + } + + const { campaignContactId } = this.props.conversation; + const message = this.createMessageToContact(messageText); + this.setState({ isSending: true }); - const finalState = { isSending: false }; + const finalState = { isSending: false, doneFirstClick: false }; try { const response = await this.props.mutations.sendMessage( message, @@ -82,7 +90,7 @@ class MessageResponse extends Component { .max(window.MAX_MESSAGE_LENGTH) }); - const { messageText, isSending } = this.state; + const { messageText, isSending, doneFirstClick } = this.state; const isSendDisabled = isSending || messageText.trim() === ""; const errorActions = [ @@ -114,6 +122,7 @@ class MessageResponse extends Component {
diff --git a/src/components/SendButton.jsx b/src/components/SendButton.jsx index 39a7e0a92..47d6afc45 100644 --- a/src/components/SendButton.jsx +++ b/src/components/SendButton.jsx @@ -1,8 +1,10 @@ import PropTypes from "prop-types"; import React, { Component } from "react"; -import RaisedButton from "material-ui/RaisedButton"; +import FlatButton from "material-ui/FlatButton"; import { StyleSheet, css } from "aphrodite"; import { dataTest } from "../lib/attributes"; +import theme from "../styles/theme"; +import { inlineStyles, flexStyles } from "./AssignmentTexter/StyleControls"; // This is because the Toolbar from material-ui seems to only apply the correct margins if the // immediate child is a Button or other type it recognizes. Can get rid of this if we remove material-ui @@ -16,11 +18,27 @@ class SendButton extends Component { render() { return (
-
@@ -30,7 +48,8 @@ class SendButton extends Component { SendButton.propTypes = { onFinalTouchTap: PropTypes.func, - disabled: PropTypes.bool + disabled: PropTypes.bool, + doneFirstClick: PropTypes.bool }; export default SendButton; From 9c39cd2eb25befbcdac305ed3af0e17755d89181 Mon Sep 17 00:00:00 2001 From: Owen Burbank Date: Thu, 3 Sep 2020 12:27:15 -0700 Subject: [PATCH 013/147] My fixes --- src/components/CampaignTextersForm.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CampaignTextersForm.jsx b/src/components/CampaignTextersForm.jsx index 0b027d650..eafb60435 100644 --- a/src/components/CampaignTextersForm.jsx +++ b/src/components/CampaignTextersForm.jsx @@ -374,6 +374,7 @@ export default class CampaignTextersForm extends React.Component { texter.assignment.needsMessageCount} hintText="Contacts" fullWidth onFocus={() => this.setState({ focusedTexterId: texter.id })} From 4eb6a3ba5d8030149e03a7cf02a7477c91aacfcb Mon Sep 17 00:00:00 2001 From: Jeff Mann Date: Fri, 4 Sep 2020 08:46:42 -0400 Subject: [PATCH 014/147] Allow admins to see phone inventory (but still not buy) --- src/components/AdminDashboard.jsx | 3 ++- src/containers/AdminPhoneNumberInventory.js | 18 +++++++++++------- src/server/api/organization.js | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/AdminDashboard.jsx b/src/components/AdminDashboard.jsx index 21b2d809a..3abe585e4 100644 --- a/src/components/AdminDashboard.jsx +++ b/src/components/AdminDashboard.jsx @@ -66,6 +66,7 @@ class AdminDashboard extends React.Component { // HACK: Setting params.adminPerms helps us hide non-supervolunteer functionality params.adminPerms = hasRole("ADMIN", roles || []); + params.ownerPerms = hasRole("OWNER", roles || []); let sections = [ { @@ -96,7 +97,7 @@ class AdminDashboard extends React.Component { { name: "Phone Numbers", path: "phone-numbers", - role: "OWNER" + role: "ADMIN" } ]; diff --git a/src/containers/AdminPhoneNumberInventory.js b/src/containers/AdminPhoneNumberInventory.js index 257be3ec3..f368e1b35 100644 --- a/src/containers/AdminPhoneNumberInventory.js +++ b/src/containers/AdminPhoneNumberInventory.js @@ -39,6 +39,7 @@ const inlineStyles = { class AdminPhoneNumberInventory extends React.Component { static propTypes = { data: PropTypes.object, + params: PropTypes.object, mutations: PropTypes.object }; @@ -249,13 +250,16 @@ class AdminPhoneNumberInventory extends React.Component { initialSort={{column: 'areaCode', order: 'asc'}} onSortOrderChange={handleSortOrderChange} /> - - - + {this.props.params.ownerPerms ? ( + + + + ) : null} + { - await accessRequired(user, organization.id, "OWNER", true); + await accessRequired(user, organization.id, "ADMIN", true); const jobs = await r .knex("job_request") .where({ From 26c3d9b897a6f7d73430145ebbb9928a4aebd6a7 Mon Sep 17 00:00:00 2001 From: Adam Greenspan Date: Wed, 2 Sep 2020 18:04:12 -0500 Subject: [PATCH 015/147] Mobilize Event Shifter Sidebox --- .../mobilize-event-shifter/react-component.js | 227 ++++++++++++++++++ src/server/middleware/render-index.js | 3 + 2 files changed, 230 insertions(+) create mode 100644 src/extensions/texter-sideboxes/mobilize-event-shifter/react-component.js diff --git a/src/extensions/texter-sideboxes/mobilize-event-shifter/react-component.js b/src/extensions/texter-sideboxes/mobilize-event-shifter/react-component.js new file mode 100644 index 000000000..bc5634527 --- /dev/null +++ b/src/extensions/texter-sideboxes/mobilize-event-shifter/react-component.js @@ -0,0 +1,227 @@ +import type from "prop-types"; +import React from "react"; +import yup from "yup"; +import Form from "react-formal"; +import FlatButton from "material-ui/FlatButton"; +import Dialog from "material-ui/Dialog"; +import CircularProgress from "material-ui/CircularProgress"; +import { Tabs, Tab } from "material-ui/Tabs"; +import { css, StyleSheet } from "aphrodite"; +import { + flexStyles, + inlineStyles +} from "../../../components/AssignmentTexter/StyleControls"; + +export const displayName = () => "Mobilize Event Shifter"; + +export const showSidebox = ({ contact, messageStatusFilter }) => + contact && messageStatusFilter !== "needsMessage"; + +const styles = StyleSheet.create({ + dialog: { + paddingTop: 0 + }, + iframe: { + height: "80vh", + width: "100%", + border: "none" + }, + loader: { + paddingTop: 50, + paddingLeft: "calc(50% - 25px)" + } +}); + +export class TexterSidebox extends React.Component { + constructor(props) { + super(props); + + const { settingsData, contact } = props; + + const customFields = contact.customFields || {}; + const eventId = + customFields.event_id || settingsData.mobilizeEventShifterDefaultEventId; + + this.state = { + dialogOpen: false, + eventiFrameLoading: true, + alliFrameLoading: true, + dialogTab: eventId ? "event" : "all" + }; + } + + cleanPhoneNumber = phone => { + // take the last 10 digits + return phone + .replace(/[^\d]/g, "") + .split("") + .reverse() + .slice(0, 10) + .reverse() + .join(""); + }; + + openDialog = () => { + this.setState({ + dialogOpen: true + }); + }; + + iframeLoaded = iframeLoadingName => { + const update = {}; + update[iframeLoadingName] = false; + this.setState(update); + }; + + changeTab = e => { + this.setState({ + dialogTab: e + }); + }; + + closeDialog = () => { + this.setState({ + dialogOpen: false, + eventiFrameLoading: true, + alliFrameLoading: true + }); + }; + + buildUrlParamString = urlParams => { + return _.map( + urlParams, + (val, key) => `${key}=${encodeURIComponent(val)}` + ).join("&"); + }; + + render() { + const { settingsData, contact, campaign } = this.props; + + const customFields = contact.customFields || {}; + + const eventId = + customFields.event_id || settingsData.mobilizeEventShifterDefaultEventId; + const urlParams = { + first_name: contact.firstName || "", + last_name: contact.lastName || "", + phone: this.cleanPhoneNumber(contact.cell || ""), + email: customFields.email || "", + zip: customFields.zip || "", + source: `spoke-${campaign.organization.id}-${campaign.id}` + }; + + const urlParamString = this.buildUrlParamString(urlParams); + const allEventsUrlParams = this.buildUrlParamString({ + zip: customFields.zip || "" + }); + + return ( +
+ this.setState({ dialogOpen: true })} + className={css(flexStyles.flatButton)} + labelStyle={inlineStyles.flatButtonLabel} + /> + + ]} + open={this.state.dialogOpen} + modal={true} + onRequestClose={this.closeDialog} + maxWidth="lg" + className={css(styles.dialog)} + > + {eventId ? ( + + + + + ) : ( + "" + )} + {eventId ? ( +
+ {this.state.eventiFrameLoading ? ( + + ) : ( + "" + )} +