From 851658ad43616ea5c65c6b941ec2908019493416 Mon Sep 17 00:00:00 2001 From: Michael Thomas <9821783+MichaelT372@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:06:12 +0200 Subject: [PATCH 1/8] New Component: Sticky NSG --- packages/scripts/dist/app.js | 4 +- packages/scripts/dist/index.d.ts | 1 + packages/scripts/dist/index.js | 1 + packages/scripts/dist/interfaces/options.d.ts | 1 + packages/scripts/dist/interfaces/options.js | 1 + packages/scripts/dist/sticky-nsg.d.ts | 10 ++ packages/scripts/dist/sticky-nsg.js | 83 +++++++++++++++ packages/scripts/src/app.ts | 4 +- packages/scripts/src/index.ts | 1 + packages/scripts/src/interfaces/options.ts | 2 + packages/scripts/src/sticky-nsg.ts | 100 ++++++++++++++++++ 11 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 packages/scripts/dist/sticky-nsg.d.ts create mode 100644 packages/scripts/dist/sticky-nsg.js create mode 100644 packages/scripts/src/sticky-nsg.ts diff --git a/packages/scripts/dist/app.js b/packages/scripts/dist/app.js index 8f457b87..db249fad 100644 --- a/packages/scripts/dist/app.js +++ b/packages/scripts/dist/app.js @@ -1,5 +1,5 @@ import { DonationAmount, DonationFrequency, EnForm, ProcessingFees, Country, } from "./events"; -import { AmountLabel, Loader, ProgressBar, UpsellLightbox, ENGrid, OptionsDefaults, setRecurrFreq, PageBackground, MediaAttribution, ApplePay, A11y, CapitalizeFields, Ecard, ClickToExpand, Advocacy, DataAttributes, LiveVariables, iFrame, InputPlaceholders, InputHasValueAndFocus, ShowHideRadioCheckboxes, AutoCountrySelect, SkipToMainContentLink, SrcDefer, NeverBounce, AutoYear, Autocomplete, RememberMe, TranslateFields, ShowIfAmount, EngridLogger, OtherAmount, MinMaxAmount, Ticker, DataReplace, DataHide, AddNameToMessage, ExpandRegionName, AppVersion, UrlToForm, RequiredIfVisible, TidyContact, DataLayer, LiveCurrency, Autosubmit, EventTickets, SwapAmounts, DebugPanel, DebugHiddenFields, FreshAddress, BrandingHtml, CountryDisable, PremiumGift, DigitalWallets, MobileCTA, LiveFrequency, UniversalOptIn, Plaid, GiveBySelect, UrlParamsToBodyAttrs, ExitIntentLightbox, SupporterHub, FastFormFill, SetAttr, ShowIfPresent, ENValidators, CustomCurrency, VGS, PostalCodeValidator, CountryRedirect, WelcomeBack, EcardToTarget, UsOnlyForm, ThankYouPageConditionalContent, EmbeddedEcard, CheckboxLabel, UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, CustomPremium, } from "."; +import { AmountLabel, Loader, ProgressBar, UpsellLightbox, ENGrid, OptionsDefaults, setRecurrFreq, PageBackground, MediaAttribution, ApplePay, A11y, CapitalizeFields, Ecard, ClickToExpand, Advocacy, DataAttributes, LiveVariables, iFrame, InputPlaceholders, InputHasValueAndFocus, ShowHideRadioCheckboxes, AutoCountrySelect, SkipToMainContentLink, SrcDefer, NeverBounce, AutoYear, Autocomplete, RememberMe, TranslateFields, ShowIfAmount, EngridLogger, OtherAmount, MinMaxAmount, Ticker, DataReplace, DataHide, AddNameToMessage, ExpandRegionName, AppVersion, UrlToForm, RequiredIfVisible, TidyContact, DataLayer, LiveCurrency, Autosubmit, EventTickets, SwapAmounts, DebugPanel, DebugHiddenFields, FreshAddress, BrandingHtml, CountryDisable, PremiumGift, DigitalWallets, MobileCTA, LiveFrequency, UniversalOptIn, Plaid, GiveBySelect, UrlParamsToBodyAttrs, ExitIntentLightbox, SupporterHub, FastFormFill, SetAttr, ShowIfPresent, ENValidators, CustomCurrency, VGS, PostalCodeValidator, CountryRedirect, WelcomeBack, EcardToTarget, UsOnlyForm, ThankYouPageConditionalContent, EmbeddedEcard, CheckboxLabel, UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, CustomPremium, StickyNSG, } from "."; export class App extends ENGrid { constructor(options) { super(); @@ -159,6 +159,8 @@ export class App extends ENGrid { new Autosubmit(); // Adjust display of event tickets. new EventTickets(); + // StickyNSG - Must load before SwapAmounts + new StickyNSG(); // Swap Amounts new SwapAmounts(); // On the end of the script, after all subscribers defined, let's load the current frequency diff --git a/packages/scripts/dist/index.d.ts b/packages/scripts/dist/index.d.ts index bdf78c9f..a68c2a2a 100644 --- a/packages/scripts/dist/index.d.ts +++ b/packages/scripts/dist/index.d.ts @@ -86,5 +86,6 @@ export * from "./optin-ladder"; export * from "./post-donation-embed"; export * from "./frequency-upsell-modal"; export * from "./frequency-upsell"; +export * from "./sticky-nsg"; export * from "./events"; export * from "./version"; diff --git a/packages/scripts/dist/index.js b/packages/scripts/dist/index.js index 45780b53..c99997cc 100644 --- a/packages/scripts/dist/index.js +++ b/packages/scripts/dist/index.js @@ -86,6 +86,7 @@ export * from "./optin-ladder"; export * from "./post-donation-embed"; export * from "./frequency-upsell-modal"; export * from "./frequency-upsell"; +export * from "./sticky-nsg"; // Events export * from "./events"; // Version diff --git a/packages/scripts/dist/interfaces/options.d.ts b/packages/scripts/dist/interfaces/options.d.ts index c25c8e1c..7193bf2f 100644 --- a/packages/scripts/dist/interfaces/options.d.ts +++ b/packages/scripts/dist/interfaces/options.d.ts @@ -48,6 +48,7 @@ export interface Options { fieldClearSelectorTargetLocation?: string; checked?: boolean; }; + StickyNSG?: boolean; TidyContact?: false | { cid?: string; record_field?: string; diff --git a/packages/scripts/dist/interfaces/options.js b/packages/scripts/dist/interfaces/options.js index 6df5aead..a5d585ff 100644 --- a/packages/scripts/dist/interfaces/options.js +++ b/packages/scripts/dist/interfaces/options.js @@ -41,6 +41,7 @@ export const OptionsDefaults = { CountryRedirect: false, WelcomeBack: false, OptInLadder: false, + StickyNSG: false, PageLayouts: [ "leftleft1col", "centerleft1col", diff --git a/packages/scripts/dist/sticky-nsg.d.ts b/packages/scripts/dist/sticky-nsg.d.ts new file mode 100644 index 00000000..3c787c90 --- /dev/null +++ b/packages/scripts/dist/sticky-nsg.d.ts @@ -0,0 +1,10 @@ +export declare class StickyNSG { + private logger; + private cookieName; + constructor(); + private shouldRun; + private nsgActiveOnPage; + private deleteCookieIfGiftProcessComplete; + private createStickyNSGCookie; + private applyStickyNSGCookie; +} diff --git a/packages/scripts/dist/sticky-nsg.js b/packages/scripts/dist/sticky-nsg.js new file mode 100644 index 00000000..ccf729bc --- /dev/null +++ b/packages/scripts/dist/sticky-nsg.js @@ -0,0 +1,83 @@ +import { EngridLogger } from "./logger"; +import { ENGrid } from "./engrid"; +import * as cookie from "./cookie"; +export class StickyNSG { + constructor() { + this.logger = new EngridLogger("StickyNSG", "teal", "white", "📌"); + this.cookieName = "engrid-sticky-nsg"; + if (!this.shouldRun()) + return; + this.logger.log("Sticky NSG is enabled"); + this.deleteCookieIfGiftProcessComplete(); + this.createStickyNSGCookie(); + this.applyStickyNSGCookie(); + } + shouldRun() { + return ENGrid.getOption("StickyNSG") === true; + } + nsgActiveOnPage() { + return (window.EngagingNetworks && + window.EngagingNetworks.suggestedGift && + typeof window.EngagingNetworks.suggestedGift === "object" && + Object.keys(window.EngagingNetworks.suggestedGift).length > 0); + } + deleteCookieIfGiftProcessComplete() { + if (ENGrid.getGiftProcess()) { + this.logger.log("Gift process complete, removing sticky NSG cookie if it exists"); + cookie.remove(this.cookieName); + } + } + createStickyNSGCookie() { + var _a, _b, _c, _d; + if (!this.nsgActiveOnPage()) { + this.logger.log("No NSG active on page, not creating sticky NSG cookie"); + return; + } + const url = new URL(window.location.href); + if (url.searchParams.get("skipstickynsg") === "true") { + this.logger.log("'skipstickynsg' param present, not creating sticky NSG cookie"); + return; + } + const nsg = window.EngagingNetworks.suggestedGift; + this.logger.log("Creating sticky NSG cookie", nsg); + const cookieValue = JSON.stringify({ + onetime: { + amounts: (_a = nsg.single) === null || _a === void 0 ? void 0 : _a.reduce((acc, curr) => { + acc[curr.value] = curr.value; + return acc; + }, {}), + default: (_b = nsg.single) === null || _b === void 0 ? void 0 : _b.find((gift) => gift.nextSuggestedGift).value, + stickyDefault: false, + }, + monthly: { + amounts: (_c = nsg.recurring) === null || _c === void 0 ? void 0 : _c.reduce((acc, curr) => { + acc[curr.value] = curr.value; + return acc; + }, {}), + default: (_d = nsg.recurring) === null || _d === void 0 ? void 0 : _d.find((gift) => gift.nextSuggestedGift).value, + stickyDefault: false, + }, + }); + cookie.set(this.cookieName, cookieValue, { path: "/", expires: 30 }); + this.logger.log("Sticky NSG cookie created", cookieValue); + } + applyStickyNSGCookie() { + if (this.nsgActiveOnPage()) { + this.logger.log("NSG active on page, not applying sticky NSG cookie, leaving the EN NSG values."); + return; + } + const cookieValue = cookie.get(this.cookieName); + if (!cookieValue) { + this.logger.log("No sticky NSG cookie found, nothing to apply"); + return; + } + try { + const nsg = JSON.parse(cookieValue); + this.logger.log("Applying sticky NSG cookie values", nsg); + window.EngridAmounts = nsg; + } + catch (e) { + this.logger.error("Error parsing sticky NSG cookie, not applying", e); + } + } +} diff --git a/packages/scripts/src/app.ts b/packages/scripts/src/app.ts index 5365dc7e..7acd4246 100644 --- a/packages/scripts/src/app.ts +++ b/packages/scripts/src/app.ts @@ -86,7 +86,7 @@ import { UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, - CustomPremium, + CustomPremium, StickyNSG, } from "."; export class App extends ENGrid { @@ -307,6 +307,8 @@ export class App extends ENGrid { // Adjust display of event tickets. new EventTickets(); + // StickyNSG - Must load before SwapAmounts + new StickyNSG(); // Swap Amounts new SwapAmounts(); diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts index 651dcb5a..46c0a674 100644 --- a/packages/scripts/src/index.ts +++ b/packages/scripts/src/index.ts @@ -102,6 +102,7 @@ export * from "./optin-ladder"; export * from "./post-donation-embed"; export * from "./frequency-upsell-modal"; export * from "./frequency-upsell"; +export * from "./sticky-nsg"; // Events export * from "./events"; diff --git a/packages/scripts/src/interfaces/options.ts b/packages/scripts/src/interfaces/options.ts index 76e22b83..4e1d4b5d 100644 --- a/packages/scripts/src/interfaces/options.ts +++ b/packages/scripts/src/interfaces/options.ts @@ -52,6 +52,7 @@ export interface Options { fieldClearSelectorTargetLocation?: string; checked?: boolean; }; + StickyNSG?: boolean; TidyContact?: | false | { @@ -219,6 +220,7 @@ export const OptionsDefaults: Options = { CountryRedirect: false, WelcomeBack: false, OptInLadder: false, + StickyNSG: false, PageLayouts: [ "leftleft1col", "centerleft1col", diff --git a/packages/scripts/src/sticky-nsg.ts b/packages/scripts/src/sticky-nsg.ts new file mode 100644 index 00000000..9e912bd0 --- /dev/null +++ b/packages/scripts/src/sticky-nsg.ts @@ -0,0 +1,100 @@ +import { EngridLogger } from "./logger"; +import { ENGrid } from "./engrid"; +import * as cookie from "./cookie"; + +export class StickyNSG { + private logger: EngridLogger = new EngridLogger( + "StickyNSG", + "teal", + "white", + "📌" + ); + private cookieName: string = "engrid-sticky-nsg"; + + constructor() { + if (!this.shouldRun()) return; + this.logger.log("Sticky NSG is enabled"); + this.deleteCookieIfGiftProcessComplete(); + this.createStickyNSGCookie(); + this.applyStickyNSGCookie(); + } + + private shouldRun(): boolean { + return ENGrid.getOption("StickyNSG") === true; + } + + private nsgActiveOnPage(): boolean { + return ( + window.EngagingNetworks && + window.EngagingNetworks.suggestedGift && + typeof window.EngagingNetworks.suggestedGift === "object" && + Object.keys(window.EngagingNetworks.suggestedGift).length > 0 + ); + } + + private deleteCookieIfGiftProcessComplete() { + if (ENGrid.getGiftProcess()) { + this.logger.log( + "Gift process complete, removing sticky NSG cookie if it exists" + ); + cookie.remove(this.cookieName); + } + } + + private createStickyNSGCookie() { + if (!this.nsgActiveOnPage()) { + this.logger.log("No NSG active on page, not creating sticky NSG cookie"); + return; + } + + const url = new URL(window.location.href); + if (url.searchParams.get("skipstickynsg") === "true") { + this.logger.log("'skipstickynsg' param present, not creating sticky NSG cookie"); + return; + } + + const nsg = window.EngagingNetworks.suggestedGift; + this.logger.log("Creating sticky NSG cookie", nsg); + const cookieValue = JSON.stringify({ + onetime: { + amounts: nsg.single?.reduce((acc: any, curr: any) => { + acc[curr.value] = curr.value; + return acc; + }, {}), + default: nsg.single?.find((gift: any) => gift.nextSuggestedGift).value, + stickyDefault: false, + }, + monthly: { + amounts: nsg.recurring?.reduce((acc: any, curr: any) => { + acc[curr.value] = curr.value; + return acc; + }, {}), + default: nsg.recurring?.find((gift: any) => gift.nextSuggestedGift).value, + stickyDefault: false, + }, + }); + cookie.set(this.cookieName, cookieValue, { path: "/", expires: 30 }); + this.logger.log("Sticky NSG cookie created", cookieValue); + } + + private applyStickyNSGCookie() { + if (this.nsgActiveOnPage()) { + this.logger.log("NSG active on page, not applying sticky NSG cookie, leaving the EN NSG values."); + return; + } + + const cookieValue = cookie.get(this.cookieName); + if (!cookieValue) { + this.logger.log("No sticky NSG cookie found, nothing to apply"); + return; + } + + try { + const nsg = JSON.parse(cookieValue); + this.logger.log("Applying sticky NSG cookie values", nsg); + window.EngridAmounts = nsg; + } catch (e) { + this.logger.error("Error parsing sticky NSG cookie, not applying", e); + } + } +} From 6155d0345995a7c9c7622ad10dc263490298663d Mon Sep 17 00:00:00 2001 From: Michael Thomas <9821783+MichaelT372@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:34:36 +0200 Subject: [PATCH 2/8] Sticky NSG: Always add an 'other' option --- packages/scripts/dist/sticky-nsg.js | 4 ++-- packages/scripts/src/sticky-nsg.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/scripts/dist/sticky-nsg.js b/packages/scripts/dist/sticky-nsg.js index ccf729bc..4265412e 100644 --- a/packages/scripts/dist/sticky-nsg.js +++ b/packages/scripts/dist/sticky-nsg.js @@ -45,7 +45,7 @@ export class StickyNSG { amounts: (_a = nsg.single) === null || _a === void 0 ? void 0 : _a.reduce((acc, curr) => { acc[curr.value] = curr.value; return acc; - }, {}), + }, { "Other": "other" }), default: (_b = nsg.single) === null || _b === void 0 ? void 0 : _b.find((gift) => gift.nextSuggestedGift).value, stickyDefault: false, }, @@ -53,7 +53,7 @@ export class StickyNSG { amounts: (_c = nsg.recurring) === null || _c === void 0 ? void 0 : _c.reduce((acc, curr) => { acc[curr.value] = curr.value; return acc; - }, {}), + }, { "Other": "other" }), default: (_d = nsg.recurring) === null || _d === void 0 ? void 0 : _d.find((gift) => gift.nextSuggestedGift).value, stickyDefault: false, }, diff --git a/packages/scripts/src/sticky-nsg.ts b/packages/scripts/src/sticky-nsg.ts index 9e912bd0..c571d452 100644 --- a/packages/scripts/src/sticky-nsg.ts +++ b/packages/scripts/src/sticky-nsg.ts @@ -60,7 +60,7 @@ export class StickyNSG { amounts: nsg.single?.reduce((acc: any, curr: any) => { acc[curr.value] = curr.value; return acc; - }, {}), + }, {"Other": "other"}), default: nsg.single?.find((gift: any) => gift.nextSuggestedGift).value, stickyDefault: false, }, @@ -68,7 +68,7 @@ export class StickyNSG { amounts: nsg.recurring?.reduce((acc: any, curr: any) => { acc[curr.value] = curr.value; return acc; - }, {}), + }, {"Other": "other"}), default: nsg.recurring?.find((gift: any) => gift.nextSuggestedGift).value, stickyDefault: false, }, From 3e46ea804d976de5a2c2628ec02681dd12b3aed1 Mon Sep 17 00:00:00 2001 From: Michael Thomas <9821783+MichaelT372@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:09:42 +0200 Subject: [PATCH 3/8] New Component: StickyPrepopulation --- packages/scripts/dist/app.js | 3 +- packages/scripts/dist/index.d.ts | 1 + packages/scripts/dist/index.js | 1 + packages/scripts/dist/interfaces/options.d.ts | 3 + packages/scripts/dist/interfaces/options.js | 1 + packages/scripts/dist/sticky-nsg.js | 14 + .../scripts/dist/sticky-prepopulation.d.ts | 17 ++ packages/scripts/dist/sticky-prepopulation.js | 207 ++++++++++++++ packages/scripts/src/app.ts | 6 +- packages/scripts/src/index.ts | 1 + packages/scripts/src/interfaces/options.ts | 2 + packages/scripts/src/sticky-nsg.ts | 14 + packages/scripts/src/sticky-prepopulation.ts | 259 ++++++++++++++++++ 13 files changed, 527 insertions(+), 2 deletions(-) create mode 100644 packages/scripts/dist/sticky-prepopulation.d.ts create mode 100644 packages/scripts/dist/sticky-prepopulation.js create mode 100644 packages/scripts/src/sticky-prepopulation.ts diff --git a/packages/scripts/dist/app.js b/packages/scripts/dist/app.js index db249fad..3d2cf1e2 100644 --- a/packages/scripts/dist/app.js +++ b/packages/scripts/dist/app.js @@ -1,5 +1,5 @@ import { DonationAmount, DonationFrequency, EnForm, ProcessingFees, Country, } from "./events"; -import { AmountLabel, Loader, ProgressBar, UpsellLightbox, ENGrid, OptionsDefaults, setRecurrFreq, PageBackground, MediaAttribution, ApplePay, A11y, CapitalizeFields, Ecard, ClickToExpand, Advocacy, DataAttributes, LiveVariables, iFrame, InputPlaceholders, InputHasValueAndFocus, ShowHideRadioCheckboxes, AutoCountrySelect, SkipToMainContentLink, SrcDefer, NeverBounce, AutoYear, Autocomplete, RememberMe, TranslateFields, ShowIfAmount, EngridLogger, OtherAmount, MinMaxAmount, Ticker, DataReplace, DataHide, AddNameToMessage, ExpandRegionName, AppVersion, UrlToForm, RequiredIfVisible, TidyContact, DataLayer, LiveCurrency, Autosubmit, EventTickets, SwapAmounts, DebugPanel, DebugHiddenFields, FreshAddress, BrandingHtml, CountryDisable, PremiumGift, DigitalWallets, MobileCTA, LiveFrequency, UniversalOptIn, Plaid, GiveBySelect, UrlParamsToBodyAttrs, ExitIntentLightbox, SupporterHub, FastFormFill, SetAttr, ShowIfPresent, ENValidators, CustomCurrency, VGS, PostalCodeValidator, CountryRedirect, WelcomeBack, EcardToTarget, UsOnlyForm, ThankYouPageConditionalContent, EmbeddedEcard, CheckboxLabel, UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, CustomPremium, StickyNSG, } from "."; +import { AmountLabel, Loader, ProgressBar, UpsellLightbox, ENGrid, OptionsDefaults, setRecurrFreq, PageBackground, MediaAttribution, ApplePay, A11y, CapitalizeFields, Ecard, ClickToExpand, Advocacy, DataAttributes, LiveVariables, iFrame, InputPlaceholders, InputHasValueAndFocus, ShowHideRadioCheckboxes, AutoCountrySelect, SkipToMainContentLink, SrcDefer, NeverBounce, AutoYear, Autocomplete, RememberMe, TranslateFields, ShowIfAmount, EngridLogger, OtherAmount, MinMaxAmount, Ticker, DataReplace, DataHide, AddNameToMessage, ExpandRegionName, AppVersion, UrlToForm, RequiredIfVisible, TidyContact, DataLayer, LiveCurrency, Autosubmit, EventTickets, SwapAmounts, DebugPanel, DebugHiddenFields, FreshAddress, BrandingHtml, CountryDisable, PremiumGift, DigitalWallets, MobileCTA, LiveFrequency, UniversalOptIn, Plaid, GiveBySelect, UrlParamsToBodyAttrs, ExitIntentLightbox, SupporterHub, FastFormFill, SetAttr, ShowIfPresent, ENValidators, CustomCurrency, VGS, PostalCodeValidator, CountryRedirect, WelcomeBack, EcardToTarget, UsOnlyForm, ThankYouPageConditionalContent, EmbeddedEcard, CheckboxLabel, UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, CustomPremium, StickyNSG, StickyPrepopulation, } from "."; export class App extends ENGrid { constructor(options) { super(); @@ -281,6 +281,7 @@ export class App extends ENGrid { new CheckboxLabel(); new PostDonationEmbed(); new FrequencyUpsell(); + new StickyPrepopulation(); //Debug panel let showDebugPanel = this.options.Debug; try { diff --git a/packages/scripts/dist/index.d.ts b/packages/scripts/dist/index.d.ts index a68c2a2a..ac58e689 100644 --- a/packages/scripts/dist/index.d.ts +++ b/packages/scripts/dist/index.d.ts @@ -87,5 +87,6 @@ export * from "./post-donation-embed"; export * from "./frequency-upsell-modal"; export * from "./frequency-upsell"; export * from "./sticky-nsg"; +export * from "./sticky-prepopulation"; export * from "./events"; export * from "./version"; diff --git a/packages/scripts/dist/index.js b/packages/scripts/dist/index.js index c99997cc..d3217e7a 100644 --- a/packages/scripts/dist/index.js +++ b/packages/scripts/dist/index.js @@ -87,6 +87,7 @@ export * from "./post-donation-embed"; export * from "./frequency-upsell-modal"; export * from "./frequency-upsell"; export * from "./sticky-nsg"; +export * from "./sticky-prepopulation"; // Events export * from "./events"; // Version diff --git a/packages/scripts/dist/interfaces/options.d.ts b/packages/scripts/dist/interfaces/options.d.ts index 7193bf2f..5387f643 100644 --- a/packages/scripts/dist/interfaces/options.d.ts +++ b/packages/scripts/dist/interfaces/options.d.ts @@ -49,6 +49,9 @@ export interface Options { checked?: boolean; }; StickyNSG?: boolean; + StickyPrepopulation?: false | { + fields: string[]; + }; TidyContact?: false | { cid?: string; record_field?: string; diff --git a/packages/scripts/dist/interfaces/options.js b/packages/scripts/dist/interfaces/options.js index a5d585ff..a491495a 100644 --- a/packages/scripts/dist/interfaces/options.js +++ b/packages/scripts/dist/interfaces/options.js @@ -42,6 +42,7 @@ export const OptionsDefaults = { WelcomeBack: false, OptInLadder: false, StickyNSG: false, + StickyPrepopulation: false, PageLayouts: [ "leftleft1col", "centerleft1col", diff --git a/packages/scripts/dist/sticky-nsg.js b/packages/scripts/dist/sticky-nsg.js index 4265412e..165f74e5 100644 --- a/packages/scripts/dist/sticky-nsg.js +++ b/packages/scripts/dist/sticky-nsg.js @@ -15,18 +15,27 @@ export class StickyNSG { shouldRun() { return ENGrid.getOption("StickyNSG") === true; } + /* + * Determine if NSG provided by EN is active on the page + */ nsgActiveOnPage() { return (window.EngagingNetworks && window.EngagingNetworks.suggestedGift && typeof window.EngagingNetworks.suggestedGift === "object" && Object.keys(window.EngagingNetworks.suggestedGift).length > 0); } + /* + * Delete the cookie if the gift process is complete + */ deleteCookieIfGiftProcessComplete() { if (ENGrid.getGiftProcess()) { this.logger.log("Gift process complete, removing sticky NSG cookie if it exists"); cookie.remove(this.cookieName); } } + /* + * Create the sticky NSG cookie if NSG is active on the page + */ createStickyNSGCookie() { var _a, _b, _c, _d; if (!this.nsgActiveOnPage()) { @@ -38,6 +47,8 @@ export class StickyNSG { this.logger.log("'skipstickynsg' param present, not creating sticky NSG cookie"); return; } + // We do some reformating to match the EngridAmounts format + // We also add "Other" to the amounts list const nsg = window.EngagingNetworks.suggestedGift; this.logger.log("Creating sticky NSG cookie", nsg); const cookieValue = JSON.stringify({ @@ -61,6 +72,9 @@ export class StickyNSG { cookie.set(this.cookieName, cookieValue, { path: "/", expires: 30 }); this.logger.log("Sticky NSG cookie created", cookieValue); } + /* + * Apply the sticky NSG cookie values to window.EngridAmounts if NSG is not active on the page + */ applyStickyNSGCookie() { if (this.nsgActiveOnPage()) { this.logger.log("NSG active on page, not applying sticky NSG cookie, leaving the EN NSG values."); diff --git a/packages/scripts/dist/sticky-prepopulation.d.ts b/packages/scripts/dist/sticky-prepopulation.d.ts new file mode 100644 index 00000000..a5c35879 --- /dev/null +++ b/packages/scripts/dist/sticky-prepopulation.d.ts @@ -0,0 +1,17 @@ +export declare class StickyPrepopulation { + private logger; + private options; + private cookieName; + constructor(); + private shouldRun; + private deleteCookieIfGiftProcessComplete; + private createCookie; + private applyPrepopulation; + private getSupporterDetailsFromFields; + private encryptSupporterDetails; + private decryptSupporterDetails; + private createEncryptionKey; + private arrayBufferToBase64; + private base64ToArrayBuffer; + private getSeed; +} diff --git a/packages/scripts/dist/sticky-prepopulation.js b/packages/scripts/dist/sticky-prepopulation.js new file mode 100644 index 00000000..da2e830f --- /dev/null +++ b/packages/scripts/dist/sticky-prepopulation.js @@ -0,0 +1,207 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { ENGrid } from "./engrid"; +import { EngridLogger } from "./logger"; +import * as cookie from "./cookie"; +export class StickyPrepopulation { + constructor() { + this.logger = new EngridLogger("StickyPrepopulation", "teal", "white", "📌"); + this.options = { fields: [] }; + this.cookieName = "engrid-sticky-prepop"; + if (!this.shouldRun()) { + return; + } + this.logger.log("StickyPrepopulation initialized"); + this.deleteCookieIfGiftProcessComplete(); + this.createCookie(); + this.applyPrepopulation(); + } + /* + * Determine if we should run the script + * Do not run if RememberMe is active + * Only run if StickyPrepopulation option is set with fields + */ + shouldRun() { + if (ENGrid.getOption("RememberMe")) { + return false; + } + const options = ENGrid.getOption("StickyPrepopulation"); + if (options && (options === null || options === void 0 ? void 0 : options.fields.length) > 0) { + this.options = options; + return true; + } + else { + return false; + } + } + /* + * Delete the cookie if the gift process is complete + */ + deleteCookieIfGiftProcessComplete() { + if (ENGrid.getGiftProcess()) { + this.logger.log("Gift process complete, removing sticky prepopulation cookie if it exists"); + cookie.remove(this.cookieName); + } + } + /* + * Create the cookie if we're coming from a campaign link and supporterId is present + */ + createCookie() { + var _a; + return __awaiter(this, void 0, void 0, function* () { + // If we're not coming from a campaign link, don't create the cookie + if (!((_a = window.pageJson) === null || _a === void 0 ? void 0 : _a.supporterId)) { + this.logger.log("No supporterId present, not creating sticky prepopulation cookie"); + return; + } + try { + const encryptedSupporterDetails = yield this.encryptSupporterDetails(this.getSupporterDetailsFromFields()); + cookie.set(this.cookieName, window.btoa(JSON.stringify({ + encryptedData: encryptedSupporterDetails.encryptedData, + iv: encryptedSupporterDetails.iv, + pageId: ENGrid.getPageID() + })), { path: "/", expires: 7 }); + } + catch (e) { + this.logger.log("Error creating sticky prepopulation cookie"); + return; + } + this.logger.log("Sticky prepopulation cookie created"); + }); + } + /* + * If the cookie is present and supporterId is not (it's not a campaign link prefilled by EN), + * then apply the prepopulation + */ + applyPrepopulation() { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const cookieData = cookie.get(this.cookieName); + if (!cookieData) { + this.logger.log("No sticky prepopulation cookie found, not prepopulating fields"); + return; + } + if ((_a = window.pageJson) === null || _a === void 0 ? void 0 : _a.supporterId) { + this.logger.log("SupporterId present, not applying sticky prepopulation"); + return; + } + const encryptedSupporterDetails = JSON.parse(window.atob(cookieData)); + if (!encryptedSupporterDetails || (encryptedSupporterDetails === null || encryptedSupporterDetails === void 0 ? void 0 : encryptedSupporterDetails.pageId) !== ENGrid.getPageID()) { + this.logger.log("No encrypted supporter details found in cookie, or page ID does not match"); + return; + } + let supporterDetails = {}; + try { + supporterDetails = JSON.parse(yield this.decryptSupporterDetails(this.base64ToArrayBuffer(encryptedSupporterDetails.encryptedData), new Uint8Array(this.base64ToArrayBuffer(encryptedSupporterDetails.iv)))); + } + catch (e) { + this.logger.log("Error decrypting supporter details from cookie"); + return; + } + this.options.fields.forEach((fieldName) => { + if (!supporterDetails[fieldName]) + return; + ENGrid.setFieldValue(fieldName, decodeURIComponent(supporterDetails[fieldName])); + this.logger.log(`Setting "${fieldName}" to "${decodeURIComponent(supporterDetails[fieldName])}"`); + }); + }); + } + /* + * Get the supporter details from the form fields + */ + getSupporterDetailsFromFields() { + const supporterDetails = {}; + this.options.fields.forEach((fieldName) => { + let field = document.querySelector(`[name="${fieldName}"]`); + // If it is a radio or checkbox, get the checked value + if (field) { + if (field.type === "radio" || field.type === "checkbox") { + field = document.querySelector(`[name="${fieldName}"]:checked`); + } + supporterDetails[fieldName] = encodeURIComponent(field.value); + } + }); + return supporterDetails; + } + /* + * Encrypt the supporter details + */ + encryptSupporterDetails(supporterDetails) { + return __awaiter(this, void 0, void 0, function* () { + const encryptionKey = yield this.createEncryptionKey(this.getSeed()); + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const supporterDetailsString = JSON.stringify(supporterDetails); + const encryptedData = yield window.crypto.subtle.encrypt({ + name: "AES-GCM", + iv: iv, + }, encryptionKey, new TextEncoder().encode(supporterDetailsString)); + return { + encryptedData: this.arrayBufferToBase64(encryptedData), + iv: this.arrayBufferToBase64(iv), + }; + }); + } + /* + * Decrypt the supporter details + */ + decryptSupporterDetails(encryptedSupporterDetails, iv) { + return __awaiter(this, void 0, void 0, function* () { + const encryptionKey = yield this.createEncryptionKey(this.getSeed()); + const decryptedData = yield window.crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, encryptionKey, encryptedSupporterDetails); + return new TextDecoder().decode(decryptedData); + }); + } + /* + * Create the encryption key + */ + createEncryptionKey(seed) { + return __awaiter(this, void 0, void 0, function* () { + const encoder = new TextEncoder(); + const keyMaterial = yield window.crypto.subtle.importKey("raw", encoder.encode(seed), { name: "PBKDF2" }, false, ["deriveKey"]); + return yield window.crypto.subtle.deriveKey({ + name: "PBKDF2", + salt: encoder.encode(seed), + iterations: 100000, + hash: "SHA-256", + }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); + }); + } + /* + * Convert an ArrayBuffer to a base64 string + */ + arrayBufferToBase64(buffer) { + let binary = ""; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + /* + * Create an Array Buffer from a base64 string + */ + base64ToArrayBuffer(base64) { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; + } + /* + * Derive a seed from the page URL + */ + getSeed() { + const url = new URL(window.location.href); + return url.origin + url.pathname + (url.searchParams.get("ea.tracking.id") ? `?ea.tracking.id=${url.searchParams.get("ea.tracking.id")}` : ''); + } +} diff --git a/packages/scripts/src/app.ts b/packages/scripts/src/app.ts index 7acd4246..2b64490b 100644 --- a/packages/scripts/src/app.ts +++ b/packages/scripts/src/app.ts @@ -86,7 +86,9 @@ import { UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, - CustomPremium, StickyNSG, + CustomPremium, + StickyNSG, + StickyPrepopulation, } from "."; export class App extends ENGrid { @@ -465,6 +467,8 @@ export class App extends ENGrid { new FrequencyUpsell(); + new StickyPrepopulation(); + //Debug panel let showDebugPanel = this.options.Debug; try { diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts index 46c0a674..ad713455 100644 --- a/packages/scripts/src/index.ts +++ b/packages/scripts/src/index.ts @@ -103,6 +103,7 @@ export * from "./post-donation-embed"; export * from "./frequency-upsell-modal"; export * from "./frequency-upsell"; export * from "./sticky-nsg"; +export * from "./sticky-prepopulation"; // Events export * from "./events"; diff --git a/packages/scripts/src/interfaces/options.ts b/packages/scripts/src/interfaces/options.ts index 4e1d4b5d..b7d36513 100644 --- a/packages/scripts/src/interfaces/options.ts +++ b/packages/scripts/src/interfaces/options.ts @@ -53,6 +53,7 @@ export interface Options { checked?: boolean; }; StickyNSG?: boolean; + StickyPrepopulation?: false | { fields: string[] }; TidyContact?: | false | { @@ -221,6 +222,7 @@ export const OptionsDefaults: Options = { WelcomeBack: false, OptInLadder: false, StickyNSG: false, + StickyPrepopulation: false, PageLayouts: [ "leftleft1col", "centerleft1col", diff --git a/packages/scripts/src/sticky-nsg.ts b/packages/scripts/src/sticky-nsg.ts index c571d452..35d28341 100644 --- a/packages/scripts/src/sticky-nsg.ts +++ b/packages/scripts/src/sticky-nsg.ts @@ -23,6 +23,9 @@ export class StickyNSG { return ENGrid.getOption("StickyNSG") === true; } + /* + * Determine if NSG provided by EN is active on the page + */ private nsgActiveOnPage(): boolean { return ( window.EngagingNetworks && @@ -32,6 +35,9 @@ export class StickyNSG { ); } + /* + * Delete the cookie if the gift process is complete + */ private deleteCookieIfGiftProcessComplete() { if (ENGrid.getGiftProcess()) { this.logger.log( @@ -41,6 +47,9 @@ export class StickyNSG { } } + /* + * Create the sticky NSG cookie if NSG is active on the page + */ private createStickyNSGCookie() { if (!this.nsgActiveOnPage()) { this.logger.log("No NSG active on page, not creating sticky NSG cookie"); @@ -53,6 +62,8 @@ export class StickyNSG { return; } + // We do some reformating to match the EngridAmounts format + // We also add "Other" to the amounts list const nsg = window.EngagingNetworks.suggestedGift; this.logger.log("Creating sticky NSG cookie", nsg); const cookieValue = JSON.stringify({ @@ -77,6 +88,9 @@ export class StickyNSG { this.logger.log("Sticky NSG cookie created", cookieValue); } + /* + * Apply the sticky NSG cookie values to window.EngridAmounts if NSG is not active on the page + */ private applyStickyNSGCookie() { if (this.nsgActiveOnPage()) { this.logger.log("NSG active on page, not applying sticky NSG cookie, leaving the EN NSG values."); diff --git a/packages/scripts/src/sticky-prepopulation.ts b/packages/scripts/src/sticky-prepopulation.ts new file mode 100644 index 00000000..ddca656b --- /dev/null +++ b/packages/scripts/src/sticky-prepopulation.ts @@ -0,0 +1,259 @@ +import { ENGrid } from "./engrid"; +import { EngridLogger } from "./logger"; +import * as cookie from "./cookie"; + +export class StickyPrepopulation { + private logger: EngridLogger = new EngridLogger( + "StickyPrepopulation", + "teal", + "white", + "📌" + ); + private options: { fields: string[] } = { fields: [] }; + private cookieName: string = "engrid-sticky-prepop"; + + constructor() { + if (!this.shouldRun()) { + return; + } + this.logger.log("StickyPrepopulation initialized"); + this.deleteCookieIfGiftProcessComplete(); + this.createCookie(); + this.applyPrepopulation(); + } + + /* + * Determine if we should run the script + * Do not run if RememberMe is active + * Only run if StickyPrepopulation option is set with fields + */ + private shouldRun(): boolean { + if (ENGrid.getOption("RememberMe")) { + return false; + } + const options = ENGrid.getOption("StickyPrepopulation"); + if (options && options?.fields.length > 0) { + this.options = options; + return true; + } else { + return false; + } + } + + /* + * Delete the cookie if the gift process is complete + */ + private deleteCookieIfGiftProcessComplete() { + if (ENGrid.getGiftProcess()) { + this.logger.log( + "Gift process complete, removing sticky prepopulation cookie if it exists" + ); + cookie.remove(this.cookieName); + } + } + + /* + * Create the cookie if we're coming from a campaign link and supporterId is present + */ + private async createCookie() { + // If we're not coming from a campaign link, don't create the cookie + if (! window.pageJson?.supporterId) { + this.logger.log("No supporterId present, not creating sticky prepopulation cookie"); + return; + } + + try { + const encryptedSupporterDetails = await this.encryptSupporterDetails( + this.getSupporterDetailsFromFields() + ); + + cookie.set(this.cookieName, window.btoa(JSON.stringify({ + encryptedData: encryptedSupporterDetails.encryptedData, + iv: encryptedSupporterDetails.iv, + pageId: ENGrid.getPageID() + })), { path: "/", expires: 7 }); + } catch (e) { + this.logger.log("Error creating sticky prepopulation cookie"); + return; + } + + this.logger.log("Sticky prepopulation cookie created"); + } + + /* + * If the cookie is present and supporterId is not (it's not a campaign link prefilled by EN), + * then apply the prepopulation + */ + private async applyPrepopulation() { + const cookieData = cookie.get(this.cookieName); + + if (!cookieData) { + this.logger.log("No sticky prepopulation cookie found, not prepopulating fields"); + return; + } + + if (window.pageJson?.supporterId) { + this.logger.log("SupporterId present, not applying sticky prepopulation"); + return; + } + + const encryptedSupporterDetails = JSON.parse( + window.atob(cookieData) + ); + + if (!encryptedSupporterDetails || encryptedSupporterDetails?.pageId !== ENGrid.getPageID()) { + this.logger.log("No encrypted supporter details found in cookie, or page ID does not match"); + return; + } + + let supporterDetails: { [key: string]: string } = {}; + + try { + supporterDetails = JSON.parse( + await this.decryptSupporterDetails( + this.base64ToArrayBuffer(encryptedSupporterDetails.encryptedData), + new Uint8Array(this.base64ToArrayBuffer(encryptedSupporterDetails.iv)) + ) + ); + } catch (e) { + this.logger.log("Error decrypting supporter details from cookie"); + return; + } + + this.options.fields.forEach((fieldName) => { + if (!supporterDetails[fieldName]) return; + ENGrid.setFieldValue( + fieldName, + decodeURIComponent(supporterDetails[fieldName]) + ); + this.logger.log(`Setting "${fieldName}" to "${decodeURIComponent(supporterDetails[fieldName])}"`); + }); + } + + /* + * Get the supporter details from the form fields + */ + private getSupporterDetailsFromFields(): { [key: string]: string } { + const supporterDetails: { [key: string]: string } = {}; + + this.options.fields.forEach((fieldName) => { + let field = document.querySelector( + `[name="${fieldName}"]` + ) as HTMLInputElement; + // If it is a radio or checkbox, get the checked value + if (field) { + if (field.type === "radio" || field.type === "checkbox") { + field = document.querySelector( + `[name="${fieldName}"]:checked` + ) as HTMLInputElement; + } + supporterDetails[fieldName] = encodeURIComponent(field.value); + } + }); + + return supporterDetails; + } + + /* + * Encrypt the supporter details + */ + private async encryptSupporterDetails(supporterDetails: { + [key: string]: string; + }): Promise<{ encryptedData: string; iv: string }> { + const encryptionKey = await this.createEncryptionKey(this.getSeed()); + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + + const supporterDetailsString = JSON.stringify(supporterDetails); + const encryptedData = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + encryptionKey, + new TextEncoder().encode(supporterDetailsString) + ); + + return { + encryptedData: this.arrayBufferToBase64(encryptedData), + iv: this.arrayBufferToBase64(iv), + }; + } + + /* + * Decrypt the supporter details + */ + private async decryptSupporterDetails( + encryptedSupporterDetails: ArrayBuffer, + iv: ArrayBuffer + ): Promise { + const encryptionKey = await this.createEncryptionKey(this.getSeed()); + + const decryptedData = await window.crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv }, + encryptionKey, + encryptedSupporterDetails + ); + + return new TextDecoder().decode(decryptedData); + } + + /* + * Create the encryption key + */ + private async createEncryptionKey(seed: string): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await window.crypto.subtle.importKey( + "raw", + encoder.encode(seed), + { name: "PBKDF2" }, + false, + ["deriveKey"] + ); + return await window.crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encoder.encode(seed), + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"] + ); + } + + /* + * Convert an ArrayBuffer to a base64 string + */ + private arrayBufferToBase64(buffer: ArrayBuffer): string { + let binary = ""; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + /* + * Create an Array Buffer from a base64 string + */ + private base64ToArrayBuffer(base64: string): ArrayBuffer { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; + } + + /* + * Derive a seed from the page URL + */ + private getSeed() { + const url = new URL(window.location.href); + return url.origin + url.pathname + (url.searchParams.get("ea.tracking.id") ? `?ea.tracking.id=${url.searchParams.get("ea.tracking.id")}` : ''); + } +} From d3bfd877bf944290dff03334ad21a87cb7845afc Mon Sep 17 00:00:00 2001 From: Michael Thomas <9821783+MichaelT372@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:21:28 +0200 Subject: [PATCH 4/8] add a few extra checks to be sure Sticky Prepop is only running where it should --- .../scripts/dist/sticky-prepopulation.d.ts | 2 +- packages/scripts/dist/sticky-prepopulation.js | 17 +++++++++------ packages/scripts/src/sticky-prepopulation.ts | 21 ++++++++++++------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/scripts/dist/sticky-prepopulation.d.ts b/packages/scripts/dist/sticky-prepopulation.d.ts index a5c35879..25aa9a66 100644 --- a/packages/scripts/dist/sticky-prepopulation.d.ts +++ b/packages/scripts/dist/sticky-prepopulation.d.ts @@ -4,7 +4,7 @@ export declare class StickyPrepopulation { private cookieName; constructor(); private shouldRun; - private deleteCookieIfGiftProcessComplete; + private deleteCookie; private createCookie; private applyPrepopulation; private getSupporterDetailsFromFields; diff --git a/packages/scripts/dist/sticky-prepopulation.js b/packages/scripts/dist/sticky-prepopulation.js index da2e830f..82751c80 100644 --- a/packages/scripts/dist/sticky-prepopulation.js +++ b/packages/scripts/dist/sticky-prepopulation.js @@ -19,7 +19,14 @@ export class StickyPrepopulation { return; } this.logger.log("StickyPrepopulation initialized"); - this.deleteCookieIfGiftProcessComplete(); + if (ENGrid.getGiftProcess()) { + this.deleteCookie(); + return; + } + if (ENGrid.getPageNumber() !== 1 || !ENGrid.getField("supporter.emailAddress")) { + this.logger.log("Not on page 1 or email field not present, not creating cookie or applying pre-population."); + return; + } this.createCookie(); this.applyPrepopulation(); } @@ -44,11 +51,9 @@ export class StickyPrepopulation { /* * Delete the cookie if the gift process is complete */ - deleteCookieIfGiftProcessComplete() { - if (ENGrid.getGiftProcess()) { - this.logger.log("Gift process complete, removing sticky prepopulation cookie if it exists"); - cookie.remove(this.cookieName); - } + deleteCookie() { + this.logger.log("Gift process complete, removing sticky prepopulation cookie if it exists"); + cookie.remove(this.cookieName); } /* * Create the cookie if we're coming from a campaign link and supporterId is present diff --git a/packages/scripts/src/sticky-prepopulation.ts b/packages/scripts/src/sticky-prepopulation.ts index ddca656b..3ead8a23 100644 --- a/packages/scripts/src/sticky-prepopulation.ts +++ b/packages/scripts/src/sticky-prepopulation.ts @@ -17,7 +17,14 @@ export class StickyPrepopulation { return; } this.logger.log("StickyPrepopulation initialized"); - this.deleteCookieIfGiftProcessComplete(); + if (ENGrid.getGiftProcess()) { + this.deleteCookie(); + return; + } + if (ENGrid.getPageNumber() !== 1 || !ENGrid.getField("supporter.emailAddress")) { + this.logger.log("Not on page 1 or email field not present, not creating cookie or applying pre-population."); + return; + } this.createCookie(); this.applyPrepopulation(); } @@ -43,13 +50,11 @@ export class StickyPrepopulation { /* * Delete the cookie if the gift process is complete */ - private deleteCookieIfGiftProcessComplete() { - if (ENGrid.getGiftProcess()) { - this.logger.log( - "Gift process complete, removing sticky prepopulation cookie if it exists" - ); - cookie.remove(this.cookieName); - } + private deleteCookie() { + this.logger.log( + "Gift process complete, removing sticky prepopulation cookie if it exists" + ); + cookie.remove(this.cookieName); } /* From 59fb36f4ddf3d3f171c7c8a35c529bdb14166c1e Mon Sep 17 00:00:00 2001 From: Michael Thomas <9821783+MichaelT372@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:25:28 +0200 Subject: [PATCH 5/8] Sticky pre-population: do not run on chained pages & move cookie parsing into try/catch block --- packages/scripts/dist/sticky-prepopulation.js | 14 +++++++----- packages/scripts/src/sticky-prepopulation.ts | 22 ++++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/scripts/dist/sticky-prepopulation.js b/packages/scripts/dist/sticky-prepopulation.js index 82751c80..8489a195 100644 --- a/packages/scripts/dist/sticky-prepopulation.js +++ b/packages/scripts/dist/sticky-prepopulation.js @@ -33,14 +33,16 @@ export class StickyPrepopulation { /* * Determine if we should run the script * Do not run if RememberMe is active + * Do not run if on a chain link * Only run if StickyPrepopulation option is set with fields */ shouldRun() { if (ENGrid.getOption("RememberMe")) { return false; } + const url = new URL(window.location.href); const options = ENGrid.getOption("StickyPrepopulation"); - if (options && (options === null || options === void 0 ? void 0 : options.fields.length) > 0) { + if (options && (options === null || options === void 0 ? void 0 : options.fields.length) > 0 && !url.searchParams.has("chain")) { this.options = options; return true; } @@ -97,13 +99,13 @@ export class StickyPrepopulation { this.logger.log("SupporterId present, not applying sticky prepopulation"); return; } - const encryptedSupporterDetails = JSON.parse(window.atob(cookieData)); - if (!encryptedSupporterDetails || (encryptedSupporterDetails === null || encryptedSupporterDetails === void 0 ? void 0 : encryptedSupporterDetails.pageId) !== ENGrid.getPageID()) { - this.logger.log("No encrypted supporter details found in cookie, or page ID does not match"); - return; - } let supporterDetails = {}; try { + const encryptedSupporterDetails = JSON.parse(window.atob(cookieData)); + if (!encryptedSupporterDetails || (encryptedSupporterDetails === null || encryptedSupporterDetails === void 0 ? void 0 : encryptedSupporterDetails.pageId) !== ENGrid.getPageID()) { + this.logger.log("No encrypted supporter details found in cookie, or page ID does not match"); + return; + } supporterDetails = JSON.parse(yield this.decryptSupporterDetails(this.base64ToArrayBuffer(encryptedSupporterDetails.encryptedData), new Uint8Array(this.base64ToArrayBuffer(encryptedSupporterDetails.iv)))); } catch (e) { diff --git a/packages/scripts/src/sticky-prepopulation.ts b/packages/scripts/src/sticky-prepopulation.ts index 3ead8a23..c105f743 100644 --- a/packages/scripts/src/sticky-prepopulation.ts +++ b/packages/scripts/src/sticky-prepopulation.ts @@ -32,14 +32,16 @@ export class StickyPrepopulation { /* * Determine if we should run the script * Do not run if RememberMe is active + * Do not run if on a chain link * Only run if StickyPrepopulation option is set with fields */ private shouldRun(): boolean { if (ENGrid.getOption("RememberMe")) { return false; } + const url = new URL(window.location.href); const options = ENGrid.getOption("StickyPrepopulation"); - if (options && options?.fields.length > 0) { + if (options && options?.fields.length > 0 && !url.searchParams.has("chain")) { this.options = options; return true; } else { @@ -102,18 +104,18 @@ export class StickyPrepopulation { return; } - const encryptedSupporterDetails = JSON.parse( - window.atob(cookieData) - ); - - if (!encryptedSupporterDetails || encryptedSupporterDetails?.pageId !== ENGrid.getPageID()) { - this.logger.log("No encrypted supporter details found in cookie, or page ID does not match"); - return; - } - let supporterDetails: { [key: string]: string } = {}; try { + const encryptedSupporterDetails = JSON.parse( + window.atob(cookieData) + ); + + if (!encryptedSupporterDetails || encryptedSupporterDetails?.pageId !== ENGrid.getPageID()) { + this.logger.log("No encrypted supporter details found in cookie, or page ID does not match"); + return; + } + supporterDetails = JSON.parse( await this.decryptSupporterDetails( this.base64ToArrayBuffer(encryptedSupporterDetails.encryptedData), From 67e77f0ffb1bf10d6c684be7ab7020cc7269a7b4 Mon Sep 17 00:00:00 2001 From: Michael Thomas <9821783+MichaelT372@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:47:33 +0100 Subject: [PATCH 6/8] remove 'other' value from sticky nsg - this is covered by EN --- packages/scripts/dist/sticky-nsg.js | 8 ++++---- packages/scripts/src/sticky-nsg.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/scripts/dist/sticky-nsg.js b/packages/scripts/dist/sticky-nsg.js index 165f74e5..2a4fa03f 100644 --- a/packages/scripts/dist/sticky-nsg.js +++ b/packages/scripts/dist/sticky-nsg.js @@ -16,7 +16,7 @@ export class StickyNSG { return ENGrid.getOption("StickyNSG") === true; } /* - * Determine if NSG provided by EN is active on the page + * Determine if NSG provided by EN is active on the page */ nsgActiveOnPage() { return (window.EngagingNetworks && @@ -34,7 +34,7 @@ export class StickyNSG { } } /* - * Create the sticky NSG cookie if NSG is active on the page + * Create the sticky NSG cookie if NSG is active on the page */ createStickyNSGCookie() { var _a, _b, _c, _d; @@ -56,7 +56,7 @@ export class StickyNSG { amounts: (_a = nsg.single) === null || _a === void 0 ? void 0 : _a.reduce((acc, curr) => { acc[curr.value] = curr.value; return acc; - }, { "Other": "other" }), + }, {}), default: (_b = nsg.single) === null || _b === void 0 ? void 0 : _b.find((gift) => gift.nextSuggestedGift).value, stickyDefault: false, }, @@ -64,7 +64,7 @@ export class StickyNSG { amounts: (_c = nsg.recurring) === null || _c === void 0 ? void 0 : _c.reduce((acc, curr) => { acc[curr.value] = curr.value; return acc; - }, { "Other": "other" }), + }, {}), default: (_d = nsg.recurring) === null || _d === void 0 ? void 0 : _d.find((gift) => gift.nextSuggestedGift).value, stickyDefault: false, }, diff --git a/packages/scripts/src/sticky-nsg.ts b/packages/scripts/src/sticky-nsg.ts index 35d28341..9982675e 100644 --- a/packages/scripts/src/sticky-nsg.ts +++ b/packages/scripts/src/sticky-nsg.ts @@ -24,7 +24,7 @@ export class StickyNSG { } /* - * Determine if NSG provided by EN is active on the page + * Determine if NSG provided by EN is active on the page */ private nsgActiveOnPage(): boolean { return ( @@ -48,7 +48,7 @@ export class StickyNSG { } /* - * Create the sticky NSG cookie if NSG is active on the page + * Create the sticky NSG cookie if NSG is active on the page */ private createStickyNSGCookie() { if (!this.nsgActiveOnPage()) { @@ -58,7 +58,9 @@ export class StickyNSG { const url = new URL(window.location.href); if (url.searchParams.get("skipstickynsg") === "true") { - this.logger.log("'skipstickynsg' param present, not creating sticky NSG cookie"); + this.logger.log( + "'skipstickynsg' param present, not creating sticky NSG cookie" + ); return; } @@ -71,7 +73,7 @@ export class StickyNSG { amounts: nsg.single?.reduce((acc: any, curr: any) => { acc[curr.value] = curr.value; return acc; - }, {"Other": "other"}), + }, {}), default: nsg.single?.find((gift: any) => gift.nextSuggestedGift).value, stickyDefault: false, }, @@ -79,8 +81,9 @@ export class StickyNSG { amounts: nsg.recurring?.reduce((acc: any, curr: any) => { acc[curr.value] = curr.value; return acc; - }, {"Other": "other"}), - default: nsg.recurring?.find((gift: any) => gift.nextSuggestedGift).value, + }, {}), + default: nsg.recurring?.find((gift: any) => gift.nextSuggestedGift) + .value, stickyDefault: false, }, }); @@ -93,7 +96,9 @@ export class StickyNSG { */ private applyStickyNSGCookie() { if (this.nsgActiveOnPage()) { - this.logger.log("NSG active on page, not applying sticky NSG cookie, leaving the EN NSG values."); + this.logger.log( + "NSG active on page, not applying sticky NSG cookie, leaving the EN NSG values." + ); return; } From 2462608340fb3fde34e03725d8985b8ba3dc08bb Mon Sep 17 00:00:00 2001 From: Fernando Santos Date: Fri, 12 Dec 2025 14:44:59 -0500 Subject: [PATCH 7/8] SwapAmounts: Do not run if NSG is found on the page --- packages/scripts/dist/swap-amounts.js | 7 ++++++- packages/scripts/src/swap-amounts.ts | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/scripts/dist/swap-amounts.js b/packages/scripts/dist/swap-amounts.js index 64ca674d..d6973cae 100644 --- a/packages/scripts/dist/swap-amounts.js +++ b/packages/scripts/dist/swap-amounts.js @@ -117,7 +117,12 @@ export class SwapAmounts { })); } shouldRun() { - return !!window.EngridAmounts; + const hasNSG = window.EngagingNetworks.suggestedGift !== undefined && + Object.keys(window.EngagingNetworks.suggestedGift).length > 0; + if (!!window.EngridAmounts && hasNSG) { + this.logger.log("Not swapping amounts because Next Suggested Gifts are present"); + } + return !!window.EngridAmounts && !hasNSG; } ignoreCurrentValue() { const urlParam = ENGrid.getUrlParameter("transaction.donationAmt"); diff --git a/packages/scripts/src/swap-amounts.ts b/packages/scripts/src/swap-amounts.ts index eeb12989..cca328b0 100644 --- a/packages/scripts/src/swap-amounts.ts +++ b/packages/scripts/src/swap-amounts.ts @@ -142,7 +142,13 @@ export class SwapAmounts { })); } shouldRun() { - return !!window.EngridAmounts; + const hasNSG = + window.EngagingNetworks.suggestedGift !== undefined && + Object.keys(window.EngagingNetworks.suggestedGift).length > 0; + if (!!window.EngridAmounts && hasNSG) { + this.logger.log("Not swapping amounts because NSG is active on page"); + } + return !!window.EngridAmounts && !hasNSG; } ignoreCurrentValue() { const urlParam = ENGrid.getUrlParameter("transaction.donationAmt"); From 6c0b8754592b79f67ea60b7292a78e54aef6f56f Mon Sep 17 00:00:00 2001 From: Michael Thomas <9821783+MichaelT372@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:45:40 +0100 Subject: [PATCH 8/8] Only add frequencies to sticky nsg cookie that are defined in NSG --- packages/scripts/dist/sticky-nsg.js | 46 +++++++++++++++--------- packages/scripts/dist/swap-amounts.js | 2 +- packages/scripts/src/sticky-nsg.ts | 51 ++++++++++++++++++--------- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/packages/scripts/dist/sticky-nsg.js b/packages/scripts/dist/sticky-nsg.js index 2a4fa03f..7fe0e9b7 100644 --- a/packages/scripts/dist/sticky-nsg.js +++ b/packages/scripts/dist/sticky-nsg.js @@ -37,7 +37,7 @@ export class StickyNSG { * Create the sticky NSG cookie if NSG is active on the page */ createStickyNSGCookie() { - var _a, _b, _c, _d; + var _a, _b, _c, _d, _e, _f; if (!this.nsgActiveOnPage()) { this.logger.log("No NSG active on page, not creating sticky NSG cookie"); return; @@ -51,24 +51,36 @@ export class StickyNSG { // We also add "Other" to the amounts list const nsg = window.EngagingNetworks.suggestedGift; this.logger.log("Creating sticky NSG cookie", nsg); - const cookieValue = JSON.stringify({ - onetime: { - amounts: (_a = nsg.single) === null || _a === void 0 ? void 0 : _a.reduce((acc, curr) => { - acc[curr.value] = curr.value; - return acc; - }, {}), - default: (_b = nsg.single) === null || _b === void 0 ? void 0 : _b.find((gift) => gift.nextSuggestedGift).value, + const oneTimeNsg = (_a = nsg.single) === null || _a === void 0 ? void 0 : _a.reduce((acc, curr) => { + acc[curr.value] = curr.value; + return acc; + }, {}); + const oneTimeDefault = (_c = (_b = nsg.single) === null || _b === void 0 ? void 0 : _b.find((gift) => gift.nextSuggestedGift)) === null || _c === void 0 ? void 0 : _c.value; + const recurringNsg = (_d = nsg.recurring) === null || _d === void 0 ? void 0 : _d.reduce((acc, curr) => { + acc[curr.value] = curr.value; + return acc; + }, {}); + const recurringDefault = (_f = (_e = nsg.recurring) === null || _e === void 0 ? void 0 : _e.find((gift) => gift.nextSuggestedGift)) === null || _f === void 0 ? void 0 : _f.value; + const nsgCookieData = {}; + if (oneTimeNsg && oneTimeDefault) { + nsgCookieData.onetime = { + amounts: oneTimeNsg, + default: oneTimeDefault, stickyDefault: false, - }, - monthly: { - amounts: (_c = nsg.recurring) === null || _c === void 0 ? void 0 : _c.reduce((acc, curr) => { - acc[curr.value] = curr.value; - return acc; - }, {}), - default: (_d = nsg.recurring) === null || _d === void 0 ? void 0 : _d.find((gift) => gift.nextSuggestedGift).value, + }; + } + if (recurringNsg && recurringDefault) { + nsgCookieData.monthly = { + amounts: recurringNsg, + default: recurringDefault, stickyDefault: false, - }, - }); + }; + } + if (Object.keys(nsgCookieData).length === 0) { + this.logger.log("No valid NSG data found to create sticky NSG cookie"); + return; + } + const cookieValue = JSON.stringify(nsgCookieData); cookie.set(this.cookieName, cookieValue, { path: "/", expires: 30 }); this.logger.log("Sticky NSG cookie created", cookieValue); } diff --git a/packages/scripts/dist/swap-amounts.js b/packages/scripts/dist/swap-amounts.js index d6973cae..fcc1a516 100644 --- a/packages/scripts/dist/swap-amounts.js +++ b/packages/scripts/dist/swap-amounts.js @@ -120,7 +120,7 @@ export class SwapAmounts { const hasNSG = window.EngagingNetworks.suggestedGift !== undefined && Object.keys(window.EngagingNetworks.suggestedGift).length > 0; if (!!window.EngridAmounts && hasNSG) { - this.logger.log("Not swapping amounts because Next Suggested Gifts are present"); + this.logger.log("Not swapping amounts because NSG is active on page"); } return !!window.EngridAmounts && !hasNSG; } diff --git a/packages/scripts/src/sticky-nsg.ts b/packages/scripts/src/sticky-nsg.ts index 9982675e..ad634923 100644 --- a/packages/scripts/src/sticky-nsg.ts +++ b/packages/scripts/src/sticky-nsg.ts @@ -68,25 +68,42 @@ export class StickyNSG { // We also add "Other" to the amounts list const nsg = window.EngagingNetworks.suggestedGift; this.logger.log("Creating sticky NSG cookie", nsg); - const cookieValue = JSON.stringify({ - onetime: { - amounts: nsg.single?.reduce((acc: any, curr: any) => { - acc[curr.value] = curr.value; - return acc; - }, {}), - default: nsg.single?.find((gift: any) => gift.nextSuggestedGift).value, + + const oneTimeNsg = nsg.single?.reduce((acc: any, curr: any) => { + acc[curr.value] = curr.value; + return acc; + }, {}); + const oneTimeDefault = nsg.single?.find((gift: any) => gift.nextSuggestedGift)?.value; + const recurringNsg = nsg.recurring?.reduce((acc: any, curr: any) => { + acc[curr.value] = curr.value; + return acc; + }, {}); + const recurringDefault = nsg.recurring?.find((gift: any) => gift.nextSuggestedGift)?.value; + + const nsgCookieData: any = {}; + + if (oneTimeNsg && oneTimeDefault) { + nsgCookieData.onetime = { + amounts: oneTimeNsg, + default: oneTimeDefault, stickyDefault: false, - }, - monthly: { - amounts: nsg.recurring?.reduce((acc: any, curr: any) => { - acc[curr.value] = curr.value; - return acc; - }, {}), - default: nsg.recurring?.find((gift: any) => gift.nextSuggestedGift) - .value, + }; + } + + if (recurringNsg && recurringDefault) { + nsgCookieData.monthly = { + amounts: recurringNsg, + default: recurringDefault, stickyDefault: false, - }, - }); + } + } + + if (Object.keys(nsgCookieData).length === 0) { + this.logger.log("No valid NSG data found to create sticky NSG cookie"); + return; + } + + const cookieValue = JSON.stringify(nsgCookieData); cookie.set(this.cookieName, cookieValue, { path: "/", expires: 30 }); this.logger.log("Sticky NSG cookie created", cookieValue); }