diff --git a/app/client/app.js b/app/client/app.js index f300bedf5..96f9b49ae 100644 --- a/app/client/app.js +++ b/app/client/app.js @@ -9,7 +9,7 @@ import "../../app/assets/stylesheets/app.scss"; const opts = { enableServiceWorker: process.env.NODE_ENV === "production", appVersion: require("../isomorphic/app-version"), - preRenderApplication + preRenderApplication, }; function enableHotReload(store) { @@ -20,13 +20,11 @@ function enableHotReload(store) { }); } } - if (window.OneSignal) { Object.assign(opts, { - serviceWorkerLocation: "/OneSignalSDKWorker.js" + serviceWorkerLocation: "/OneSignalSDKWorker.js", }); } - global.wretch = wretch; startApp(renderApplication, REDUCERS, opts).then(enableHotReload); diff --git a/app/server/app.js b/app/server/app.js index 8172e73cd..6a1254c51 100644 --- a/app/server/app.js +++ b/app/server/app.js @@ -1,4 +1,5 @@ /* eslint-disable no-console, no-unused-vars, import/extensions, object-shorthand, global-require */ +import bodyParser from "body-parser"; import createApp from "@quintype/framework/server/create-app"; import { getClient, Collection } from "@quintype/framework/server/api-client"; import logger from "@quintype/framework/server/logger"; @@ -14,6 +15,8 @@ import { renderLayout } from "./handlers/render-layout"; import { loadData, loadErrorData } from "./load-data"; import { pickComponent } from "../isomorphic/pick-component"; import { generateStaticData, generateStructuredData, SEO } from "@quintype/seo"; +import { enableWebengage } from "../../config/webengage-config"; +import { handleWebEngageNotifications } from "./webengage/webengage-handler"; export const app = createApp(); @@ -72,7 +75,12 @@ app.get("*", (req, res, next) => { return next(); } }); +// Begin webengage integration route +if (enableWebengage) { + app.post("/integrations/webengage/trigger-notification", bodyParser.json(), handleWebEngageNotifications); +} +// End webengage integration route function generateSeo(config, pageType) { return new SEO({ staticTags: Object.assign(generateStaticData(config)), @@ -133,6 +141,6 @@ isomorphicRoutes(app, { staticRoutes: STATIC_ROUTES, seo: generateSeo, preloadJs: true, - oneSignalServiceWorkers: true, + oneSignalServiceWorkers: false, prerenderServiceUrl: "https://prerender.quintype.io", }); diff --git a/app/server/handlers/render-layout.js b/app/server/handlers/render-layout.js index ec32c2f86..ac52ea0b1 100644 --- a/app/server/handlers/render-layout.js +++ b/app/server/handlers/render-layout.js @@ -17,6 +17,7 @@ const fontJsContent = assetPath("font.js") ? readAsset("font.js") : ""; const allChunks = getAllChunks("list", "story", "home"); export async function renderLayout(res, params) { + const storeState = params.store.getState(); const { gtmId, gaId, @@ -28,17 +29,15 @@ export async function renderLayout(res, params) { loadAdsSynchronously, pageType, enableMetype, - } = getConfig(params.store.getState()); + } = getConfig(storeState); const chunk = params.shell ? null : allChunks[getChunkName(params.pageType)]; const criticalCss = await getCriticalCss(); const styleTags = await getStyleTags(); - const arrowCss = await getArrowCss(params.store.getState()); + const arrowCss = await getArrowCss(storeState); const isProduction = process.env.NODE_ENV === "production"; - const placeholderDelay = parseInt( - get(params.store.getState(), ["qt", "config", "publisher-attributes", "placeholder_delay"]) - ); - + const placeholderDelay = parseInt(get(storeState, ["qt", "config", "publisher-attributes", "placeholder_delay"])); + const webengageLicenseCode = get(storeState, ["qt", "config", "webengage-config", "licenseCode"], ""); // Need to change this condition after static page api is fixed const metadataHeader = (pageType === "static-page" && params.metadata?.header && params.metadata?.header === true) || @@ -51,6 +50,7 @@ export async function renderLayout(res, params) { "pages/layout", Object.assign( { + webengageLicenseCode, isProduction, assetPath: assetPath, metadata: params.metadata, diff --git a/app/server/load-data.js b/app/server/load-data.js index 6ddc0410d..a7b0ba0ec 100644 --- a/app/server/load-data.js +++ b/app/server/load-data.js @@ -15,6 +15,7 @@ import { getNavigationMenuArray } from "./data-loaders/menu-data"; import { loadCollectionPageData } from "./data-loaders/collection-page-data"; import { loadAuthorPageData } from "./data-loaders/author-page-data"; import { PAGE_TYPE } from "../isomorphic/constants"; +import webengageConfig from "../../config/webengage-config"; const { ads } = require("@quintype/framework/server/static-configuration"); @@ -27,10 +28,10 @@ const WHITELIST_CONFIG_KEYS = [ "publisher-name", "public-integrations", "sketches-host", - "publisher-settings" + "publisher-settings", ]; -const svgSpritePath = Array.from(getAssetFiles()).find(asset => asset.includes("sprite")); +const svgSpritePath = Array.from(getAssetFiles()).find((asset) => asset.includes("sprite")); export function getPublisherAttributes(publisherYml = publisher) { const publisherAttributes = get(publisherYml, ["publisher"], {}); @@ -42,15 +43,16 @@ export function loadErrorData(error, config) { const errorComponents = { 404: "not-found" }; return Promise.resolve({ data: { - navigationMenu: getNavigationMenuArray(config.layout.menu, config.sections) + navigationMenu: getNavigationMenuArray(config.layout.menu, config.sections), }, config: Object.assign(pick(config.asJson(), WHITELIST_CONFIG_KEYS), { "publisher-attributes": publisherAttributes, "ads-config": ads, - svgSpritePath + svgSpritePath, + "webengage-config": webengageConfig, }), pageType: errorComponents[error.httpStatusCode], - httpStatusCode: error.httpStatusCode || 500 + httpStatusCode: error.httpStatusCode || 500, }); } @@ -89,13 +91,13 @@ export function loadData(pageType, params, config, client, { host, next, domainS } } - return _loadData().then(data => { + return _loadData().then((data) => { return { httpStatusCode: data.httpStatusCode || 200, pageType: data.pageType || pageType, data: Object.assign({}, data, { navigationMenu: getNavigationMenuArray(config.layout.menu, config.sections), - timezone: publisherAttributes.timezone || null + timezone: publisherAttributes.timezone || null, }), config: Object.assign(pick(config.asJson(), WHITELIST_CONFIG_KEYS), { "publisher-attributes": publisherAttributes, @@ -103,8 +105,9 @@ export function loadData(pageType, params, config, client, { host, next, domainS "ads-config": ads, svgSpritePath, domainSlug, - showPlaceholder: publisherAttributes.enable_placeholder - }) + showPlaceholder: publisherAttributes.enable_placeholder, + "webengage-config": webengageConfig, + }), }; }); } diff --git a/app/server/webengage/createCampaign.js b/app/server/webengage/createCampaign.js new file mode 100644 index 000000000..a6e34c2f5 --- /dev/null +++ b/app/server/webengage/createCampaign.js @@ -0,0 +1,39 @@ +import get from "lodash/get"; +import fetch from "node-fetch"; + +async function createCampaign({ res, webhookContent, platform, url, webengageHeaders, logger }) { + const headline = get(webhookContent, ["headline"], ""); + const title = get(webhookContent, ["title"], headline); + const TAGS = ["storypublish"]; + + const webRequestPayload = { + title, + sdks: null, + container: "ONETIME", + tags: TAGS, + experimentMetaData: { applyUCG: true }, + applyUCG: true, + }; + + const appRequestPayload = { + title, + sdks: [2, 3], + container: "ONETIME", + tags: TAGS, + appIds: {}, + }; + const requestPayload = platform === "push-notifications" ? appRequestPayload : webRequestPayload; + try { + const apiResponse = await fetch(url, { + method: "POST", + body: JSON.stringify(requestPayload), + headers: webengageHeaders, + }); + const audienceCreationResponse = await apiResponse.json(); + return get(audienceCreationResponse, ["response", "data", "id"]); + } catch (e) { + logger.error("Error handling Audience/Campaign Creation : " + e); + res.status(503).send({ error: { message: "Audience/Campaign creation failure" } }); + } +} +export default createCampaign; diff --git a/app/server/webengage/createConversion.js b/app/server/webengage/createConversion.js new file mode 100644 index 000000000..f59e65c64 --- /dev/null +++ b/app/server/webengage/createConversion.js @@ -0,0 +1,46 @@ +import fetch from "node-fetch"; +import get from "lodash/get"; +import { licenseCode } from "../../../config/webengage-config"; + +async function createConversion({ res, webhookContent, url, campaignId, webengageHeaders, logger }) { + const headline = get(webhookContent, ["headline"], ""); + const title = get(webhookContent, ["title"], headline); + const requestPayload = { + deadline: "+7d", + experiment: `${campaignId}`, + licenseCode: `${licenseCode}`, + controlGroup: 0, + name: title, + triggerSet: { + triggers: [ + { + name: "Trigger ", + category: "application", + type: "EVENT", + timeDifference: "", + timeAttribute: { + name: "event_time", + category: "system", + }, + filters: null, + }, + ], + }, + version: 2, + status: "ACTIVE", + }; + + try { + const response = await fetch(url, { + method: "POST", + body: JSON.stringify(requestPayload), + headers: webengageHeaders, + }); + await response.json(); + } catch (e) { + logger.error("Error handling createConversion Creation : " + e); + res.status(503).send({ error: { message: "Conversion creation failure" } }); + } +} + +export default createConversion; diff --git a/app/server/webengage/createVariation.js b/app/server/webengage/createVariation.js new file mode 100644 index 000000000..3a70c228b --- /dev/null +++ b/app/server/webengage/createVariation.js @@ -0,0 +1,90 @@ +import get from "lodash/get"; +import { + webPushTextLayoutId, + appPushTextLayoutId, + appPushBannerLayoutId, + icon, +} from "../../../config/webengage-config"; +import fetch from "node-fetch"; + +async function createVariation({ + res, + webhookContent, + platform, + url, + sketchesHost, + eventType, + cdnName, + webengageHeaders, + logger, +}) { + const headline = get(webhookContent, ["headline"], ""); + const title = get(webhookContent, ["title"], headline); + const message = get(webhookContent, ["message"], ""); + const subheadline = get(webhookContent, ["subheadline"], ""); + const storyUrl = get(webhookContent, ["story-url"], ""); + const slug = `${sketchesHost}/${get(webhookContent, ["slug"], "")}`; + const STORY_PUBLISH_EVENT = "story-publish"; + const heroImage = + eventType === STORY_PUBLISH_EVENT + ? `${cdnName}${get(webhookContent, ["v1", "data", "hero-image-s3-key"])}` + : get(webhookContent, ["hero-image-url"], ""); + + const webRequestPayload = [ + { + layoutEId: webPushTextLayoutId, + title, + description: message || subheadline, + sampling: 100, + icon, + requireInteraction: true, + cta: { actionText: "NA", actionLink: storyUrl || slug, type: "EXTERNAL_URL", isPrime: true }, + }, + ]; + const appPushRequestPayload = [ + { + sampling: 50, + layoutEId: appPushTextLayoutId, + title, + message: message || subheadline, + androidDetails: { + expandableDetails: {}, + }, + iosDetails: { + expandableDetails: {}, + }, + }, + { + sampling: 50, + layoutEId: appPushBannerLayoutId, + experimentVariationStatus: "ACTIVE", + title, + message: message || subheadline, + androidDetails: { + expandableDetails: { + image: heroImage, + }, + }, + iosDetails: { + expandableDetails: { + image: heroImage, + }, + }, + }, + ]; + const requestPayload = platform === "push-notifications" ? appPushRequestPayload : webRequestPayload; + + try { + const response = await fetch(url, { + method: "PUT", + body: JSON.stringify(requestPayload), + headers: webengageHeaders, + }); + await response.json(); + } catch (e) { + logger.error("Error handling Variation Creation : " + e); + res.status(503).send({ error: { message: "Variation creation failure" } }); + } +} + +export default createVariation; diff --git a/app/server/webengage/launchCampaign.js b/app/server/webengage/launchCampaign.js new file mode 100644 index 000000000..2be50e34d --- /dev/null +++ b/app/server/webengage/launchCampaign.js @@ -0,0 +1,12 @@ +import fetch from "node-fetch"; +async function launchCampaign({ res, url, webengageHeaders, logger }) { + try { + const response = await fetch(url, { method: "PUT", headers: webengageHeaders }); + await response.json(); + } catch (e) { + logger.error("Error handling launch Campaign:", +e); + res.status(503).send({ error: { message: "Campaign activation failure" } }); + } +} + +export default launchCampaign; diff --git a/app/server/webengage/scheduleCampaign.js b/app/server/webengage/scheduleCampaign.js new file mode 100644 index 000000000..0ba3d38cd --- /dev/null +++ b/app/server/webengage/scheduleCampaign.js @@ -0,0 +1,35 @@ +import fetch from "node-fetch"; +async function scheduleCampaign({ res, url, webengageHeaders, logger }) { + const requestPayload = { + trafficSegmentDto: {}, + scheduler: { + sendNow: true, + sendInTz: "ACCOUNT", + sendIntelligently: false, + time: null, + }, + triggerSet: null, + oneTime: true, + queueMessage: true, + ttl: 86400, + applyFrequencyCapping: true, + incrementFrequencyCappingCount: true, + applyDnd: true, + startDate: null, + endDate: null, + }; + + try { + const response = await fetch(url, { + method: "POST", + body: JSON.stringify(requestPayload), + headers: webengageHeaders, + }); + await response.json(); + } catch (e) { + logger.error("Error handling ScheduleCampaign : " + e); + res.status(503).send({ error: { message: "ScheduleCampaign failure" } }); + } +} + +export default scheduleCampaign; diff --git a/app/server/webengage/webengage-handler.js b/app/server/webengage/webengage-handler.js new file mode 100644 index 000000000..c30366a57 --- /dev/null +++ b/app/server/webengage/webengage-handler.js @@ -0,0 +1,153 @@ +import logger from "@quintype/framework/server/logger"; +import { getClient } from "@quintype/framework/server/api-client"; +import createCampaign from "./createCampaign"; +import scheduleCampaign from "./scheduleCampaign"; +import createVariation from "./createVariation"; +import launchCampaign from "./launchCampaign"; +import get from "lodash/get"; +import { licenseCode, apiKey } from "../../../config/webengage-config"; +import createConversion from "./createConversion"; + +const BASE_URL = "https://api.webengage.com"; + +const WEB_PUSH_PLATFORM = "web-push"; +const APP_PUSH_PLATFORM = "push-notifications"; + +const webengageHeaders = { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, +}; +const getUrl = (url, platform, path) => { + switch (path) { + case "": + return `${url}/${licenseCode}/${platform}`; + case "conversions": + return `${url}/${licenseCode}/${path}`; + default: + return `${url}/${licenseCode}/${platform}/${path}`; + } +}; + +const sendWebPushNotification = async ({ res, webhookContent, cdnName, sketchesHost, eventType }) => { + // Step 1 : AUDIENCE Selection + const campaignId = await createCampaign({ + res, + webhookContent, + platform: WEB_PUSH_PLATFORM, + url: getUrl(`${BASE_URL}/api/v2/accounts`, WEB_PUSH_PLATFORM, ""), + webengageHeaders, + logger, + }); + // Step 2: Schedule campaign -- WHEN + await scheduleCampaign({ + res, + url: getUrl(`${BASE_URL}/api/v1/accounts`, WEB_PUSH_PLATFORM, `${campaignId}/targetingRule/schedule`), + webengageHeaders, + logger, + }); + // Step 3: Create variation either Text / Banner -- MESSAGE + await createVariation({ + res, + webhookContent, + platform: WEB_PUSH_PLATFORM, + url: getUrl(`${BASE_URL}/api/v1/accounts`, WEB_PUSH_PLATFORM, `${campaignId}/variations`), + sketchesHost, + eventType, + cdnName, + webengageHeaders, + logger, + }); + + // Step 4: Conversion Tracking + await createConversion({ + res, + webhookContent, + url: getUrl(`${BASE_URL}/api/v1/accounts`, WEB_PUSH_PLATFORM, `conversions`), + campaignId, + webengageHeaders, + logger, + }); + // Step 5: Activate / Launch + await launchCampaign({ + res, + url: getUrl(`${BASE_URL}/api/v1/accounts`, WEB_PUSH_PLATFORM, `${campaignId}/activate`), + webengageHeaders, + logger, + }); +}; + +const sendAppPushNotification = async ({ res, webhookContent, cdnName, sketchesHost, eventType }) => { + // Step 1 : AUDIENCE Selection + const campaignId = await createCampaign({ + res, + webhookContent, + platform: APP_PUSH_PLATFORM, + url: getUrl(`${BASE_URL}/v2/accounts`, APP_PUSH_PLATFORM, ""), + webengageHeaders, + logger, + }); + // Step 2: Schedule campaign -- WHEN + await scheduleCampaign({ + res, + url: getUrl(`${BASE_URL}/v1/accounts`, APP_PUSH_PLATFORM, `${campaignId}/targetingRule/schedule`), + webengageHeaders, + logger, + }); + + // Step 3: Create variation either Text / Banner -- MESSAGE + await createVariation({ + res, + webhookContent, + platform: APP_PUSH_PLATFORM, + url: getUrl(`${BASE_URL}/v1/accounts`, APP_PUSH_PLATFORM, `${campaignId}/variations`), + sketchesHost, + eventType, + cdnName, + webengageHeaders, + logger, + }); + + // Step 4: Conversion Tracking + await createConversion({ + res, + webhookContent, + url: getUrl(`${BASE_URL}/v1/accounts`, APP_PUSH_PLATFORM, "conversions"), + webengageHeaders, + logger, + }); + + // Step 5: Activate / Launch + await launchCampaign({ + res, + url: getUrl(`${BASE_URL}/v1/accounts`, APP_PUSH_PLATFORM, `${campaignId}/activate`), + webengageHeaders, + logger, + }); +}; + +export const handleWebEngageNotifications = async (req, res, next) => { + const config = await getClient(req.hostname).getConfig(); + const sketchesHost = get(config, ["sketches-host"]); + const cdnName = get(config, ["cdn-name"]); + const webhookContent = get(req, ["body"], {}); + const eventType = get(webhookContent, "type"); + + const targetHandlers = { + web: sendWebPushNotification, + mobile: sendAppPushNotification, + }; + + const targets = get(webhookContent, "targets", []); + + const targetMapping = targets.map((target) => + targetHandlers[target]({ res, webhookContent, cdnName, sketchesHost, eventType }) + ); + + try { + await Promise.all(targetMapping); + res.status(201).send({ status: "success" }); + } catch (e) { + logger.error("Error handling Push Notification: ", +e); + res.status(503).send({ error: { message: "Notification failure" } }); + } +}; diff --git a/config/webengage-config.js b/config/webengage-config.js new file mode 100644 index 000000000..a96ccd2ca --- /dev/null +++ b/config/webengage-config.js @@ -0,0 +1,9 @@ +module.exports = { + enableWebengage: true, + licenseCode: "", + apiKey: "", + webPushTextLayoutId: "i78egae", + appPushTextLayoutId: "2341ifc5", + appPushBannerLayoutId: "~20cc49c5", + icon: "https://afiles.webengage.com/11b564a4b/7553be1b-da70-44e5-b55a-7699021b896e.jpg", +}; diff --git a/views/js/service-worker.ejs b/views/js/service-worker.ejs index c15c30a51..ef37e598e 100644 --- a/views/js/service-worker.ejs +++ b/views/js/service-worker.ejs @@ -1,24 +1,26 @@ <%- serviceWorkerHelper %>; + const shellUrl = "/shell.html?revision=<%= assetHash("app.js") %>-<%= configVersion %>"; -const shellUrl = "/shell.html?revision=<%= assetHash("app.js") %>-<%= configVersion %>"; + const REQUIRED_ASSETS = [ + <%_ getFilesForChunks("app", "list", "story").map(x => { _%> + "<%= x %>", + <%_ }) _%> + // Put fonts here + shellUrl + ]; -const REQUIRED_ASSETS = [ - <%_ getFilesForChunks("app", "list", "story").map(x => { _%> - "<%= x %>", - <%_ }) _%> - // Put fonts here - shellUrl -]; + initializeQServiceWorker({ + routes: <%- JSON.stringify(routes) %>, + assets: REQUIRED_ASSETS, + shell: shellUrl, + hostname: <%- JSON.stringify(hostname) %> + }); -initializeQServiceWorker({ - routes: <%- JSON.stringify(routes) %>, - assets: REQUIRED_ASSETS, - shell: shellUrl, - hostname: <%- JSON.stringify(hostname) %> -}); + workbox.routing.registerRoute(new RegExp('/route-data.json*'), new workbox.strategies.NetworkFirst()); -workbox.routing.registerRoute(new RegExp('/route-data.json*'), new workbox.strategies.NetworkFirst()); + if("<%= config["public-integrations"]["one-signal"] && config["public-integrations"]["one-signal"]["app-id"] %>") { + importScripts('https://cdn.onesignal.com/sdks/OneSignalSDKWorker.js'); + } -if("<%= config["public-integrations"]["one-signal"] && config["public-integrations"]["one-signal"]["app-id"] %>") { - importScripts('https://cdn.onesignal.com/sdks/OneSignalSDKWorker.js'); -} +self.skipWaiting(); +importScripts('https://ssl.widgets.webengage.com/js/service-worker.js'); diff --git a/views/pages/layout.ejs b/views/pages/layout.ejs index cd04d534b..9ac201898 100644 --- a/views/pages/layout.ejs +++ b/views/pages/layout.ejs @@ -82,7 +82,7 @@ <%_ if(!isProduction) { _%> <%- styleTags %> <%_ } _%> - + <%- include("./partials/webengage")-%>
diff --git a/views/pages/partials/webengage.ejs b/views/pages/partials/webengage.ejs new file mode 100644 index 000000000..870c5cc65 --- /dev/null +++ b/views/pages/partials/webengage.ejs @@ -0,0 +1,40 @@ +