@4site/engrid-scripts 0.24.4 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class App extends ENGrid {
13
13
  private onLoad;
14
14
  private onResize;
15
15
  private onValidate;
16
+ private onIntentSubmit;
16
17
  private onSubmit;
17
18
  private onError;
18
19
  static log(message: string): void;
package/dist/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DonationAmount, DonationFrequency, EnForm, ProcessingFees, Country, } from "./events";
2
- 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, StripeFinancialConnections, GiveBySelect, UrlParamsToBodyAttrs, ExitIntentLightbox, SupporterHub, FastFormFill, SetAttr, ShowIfPresent, ENValidators, CustomCurrency, VGS, PostalCodeValidator, CountryRedirect, WelcomeBack, EcardToTarget, UsOnlyForm, ThankYouPageConditionalContent, EmbeddedEcard, CheckboxLabel, UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, CustomPremium, PreferredPaymentMethod, } from ".";
2
+ 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, StripeFinancialConnections, GiveBySelect, UrlParamsToBodyAttrs, ExitIntentLightbox, SupporterHub, FastFormFill, SetAttr, ShowIfPresent, ENValidators, CustomCurrency, VGS, PostalCodeValidator, CountryRedirect, WelcomeBack, EcardToTarget, UsOnlyForm, ThankYouPageConditionalContent, EmbeddedEcard, CheckboxLabel, UpsellCheckbox, PostDonationEmbed, FrequencyUpsell, CustomPremium, StickyNSG, StickyPrepopulation, PreferredPaymentMethod, } from ".";
3
3
  export class App extends ENGrid {
4
4
  constructor(options) {
5
5
  super();
@@ -94,6 +94,7 @@ export class App extends ENGrid {
94
94
  }
95
95
  });
96
96
  // Client onSubmit and onError functions
97
+ this._form.onIntentSubmit.subscribe(() => this.onIntentSubmit());
97
98
  this._form.onSubmit.subscribe(() => this.onSubmit());
98
99
  this._form.onError.subscribe(() => this.onError());
99
100
  this._form.onValidate.subscribe(() => this.onValidate());
@@ -111,6 +112,7 @@ export class App extends ENGrid {
111
112
  window.enOnSubmit = () => {
112
113
  this._form.submit = true;
113
114
  this._form.submitPromise = false;
115
+ this._form.dispatchIntentSubmit();
114
116
  this._form.dispatchSubmit();
115
117
  ENGrid.watchForError(ENGrid.enableSubmit);
116
118
  if (!this._form.submit)
@@ -160,6 +162,8 @@ export class App extends ENGrid {
160
162
  new Autosubmit();
161
163
  // Adjust display of event tickets.
162
164
  new EventTickets();
165
+ // StickyNSG - Must load before SwapAmounts
166
+ new StickyNSG();
163
167
  // Swap Amounts
164
168
  new SwapAmounts();
165
169
  // On the end of the script, after all subscribers defined, let's load the current frequency
@@ -279,6 +283,7 @@ export class App extends ENGrid {
279
283
  new CheckboxLabel();
280
284
  new PostDonationEmbed();
281
285
  new FrequencyUpsell();
286
+ new StickyPrepopulation();
282
287
  //Debug panel
283
288
  let showDebugPanel = this.options.Debug;
284
289
  try {
@@ -328,6 +333,12 @@ export class App extends ENGrid {
328
333
  this.options.onValidate();
329
334
  }
330
335
  }
336
+ onIntentSubmit() {
337
+ if (this.options.onIntentSubmit) {
338
+ this.logger.log("Client onIntentSubmit Triggered");
339
+ this.options.onIntentSubmit();
340
+ }
341
+ }
331
342
  onSubmit() {
332
343
  if (this.options.onSubmit) {
333
344
  this.logger.log("Client onSubmit Triggered");
@@ -16,10 +16,15 @@ export class AutoCountrySelect {
16
16
  ENGrid.getUrlParameter("supporter.region") ||
17
17
  (ENGrid.getUrlParameter("ea.url.id") &&
18
18
  !ENGrid.getUrlParameter("forwarded"));
19
+ // If fast form is active, then personal details have already been filled somehow and we should not override the country selection
20
+ // The client is also likely using WelcomeBack.
21
+ const fastFormActive = ENGrid.getBodyData("hide-fast-address-details") ||
22
+ ENGrid.getBodyData("hide-fast-personal-details");
19
23
  if (!engridAutofill &&
20
24
  !submissionFailed &&
21
25
  hasIntlSupport &&
22
- !locationDataInUrl) {
26
+ !locationDataInUrl &&
27
+ !fastFormActive) {
23
28
  fetch(`https://${window.location.hostname}/cdn-cgi/trace`)
24
29
  .then((res) => res.text())
25
30
  .then((t) => {
@@ -5,6 +5,7 @@ export declare class DataLayer {
5
5
  private static instance;
6
6
  private encoder;
7
7
  private endOfGiftProcessStorageKey;
8
+ private giftFields;
8
9
  private excludedFields;
9
10
  private hashedFields;
10
11
  private retainedEmailField;
@@ -24,6 +24,19 @@ export class DataLayer {
24
24
  this._form = EnForm.getInstance();
25
25
  this.encoder = new TextEncoder();
26
26
  this.endOfGiftProcessStorageKey = "ENGRID_END_OF_GIFT_PROCESS_EVENTS";
27
+ // pageJson entries related to the gift process
28
+ this.giftFields = [
29
+ "amount",
30
+ "currency",
31
+ "donationLogId",
32
+ "feeCover",
33
+ "giftProcess",
34
+ "paymentType",
35
+ "receiptNumber",
36
+ "recurring",
37
+ "transactionId",
38
+ "transactionType",
39
+ ];
27
40
  this.excludedFields = [
28
41
  // Credit Card
29
42
  "transaction.ccnumber",
@@ -102,13 +115,27 @@ export class DataLayer {
102
115
  onLoad() {
103
116
  // Collect all data layer variables to push at once
104
117
  const dataLayerData = {};
118
+ const suppressEcardData = ENGrid.getPageType() === "ECARD" &&
119
+ ENGrid.getOption("SuppressPurchaseEcard");
105
120
  if (ENGrid.getGiftProcess()) {
106
- this.logger.log("EN_SUCCESSFUL_DONATION");
107
- this.addEndOfGiftProcessEventsToDataLayer();
121
+ // EN will chain together gift process data on the page json when redirecting from a completed donation to an ecard.
122
+ // Since the ecard page can be embedded on the thank you page of a donation, this can cause confusion in the data layer with events
123
+ // firing for both the donation and the ecard on the same page.
124
+ if (suppressEcardData) {
125
+ this.logger.log("⛔ Gift process was detected BUT suppressing EN_SUCCESSFUL_DONATION event due to SuppressPurchaseEcard option enabled");
126
+ window.sessionStorage.removeItem(this.endOfGiftProcessStorageKey);
127
+ }
128
+ else {
129
+ this.logger.log("EN_SUCCESSFUL_DONATION");
130
+ this.addEndOfGiftProcessEventsToDataLayer();
131
+ }
108
132
  }
109
133
  if (window.pageJson) {
110
134
  const pageJson = window.pageJson;
111
135
  for (const property in pageJson) {
136
+ if (suppressEcardData && this.giftFields.includes(property)) {
137
+ continue;
138
+ }
112
139
  const key = `EN_PAGEJSON_${property.toUpperCase()}`;
113
140
  const value = pageJson[property];
114
141
  dataLayerData[key] = this.transformJSON(value);
@@ -1,8 +1,13 @@
1
1
  export declare class DigitalWallets {
2
+ private logger;
3
+ private _form;
2
4
  constructor();
3
5
  private addStripeDigitalWallets;
4
6
  private addPaypalTouchDigitalWallets;
5
7
  private addDAF;
6
8
  private addOptionToPaymentTypeField;
7
9
  private checkForWalletsBeingAdded;
10
+ private addPaypalOneTouchListener;
11
+ private addStripeDigitalWalletListener;
12
+ private addDAFListener;
8
13
  }
@@ -1,6 +1,10 @@
1
1
  import { ENGrid } from "./engrid";
2
+ import { EnForm } from "./events";
3
+ import { EngridLogger } from "./logger";
2
4
  export class DigitalWallets {
3
5
  constructor() {
6
+ this.logger = new EngridLogger("DigitalWallets", "#fff", "#333", "👛");
7
+ this._form = EnForm.getInstance();
4
8
  //digital wallets not enabled.
5
9
  if (!document.getElementById("en__digitalWallet")) {
6
10
  ENGrid.setBodyData("payment-type-option-stripedigitalwallet", "false");
@@ -9,6 +13,7 @@ export class DigitalWallets {
9
13
  ENGrid.setBodyData("payment-type-option-paypal-one-touch", "false");
10
14
  ENGrid.setBodyData("payment-type-option-venmo", "false");
11
15
  ENGrid.setBodyData("payment-type-option-daf", "false");
16
+ this.logger.log("No digital wallet container found, skipping digital wallet setup.");
12
17
  return;
13
18
  }
14
19
  // Add giveBySelect classes to the separate wallet containers
@@ -86,6 +91,7 @@ export class DigitalWallets {
86
91
  }
87
92
  }
88
93
  addStripeDigitalWallets() {
94
+ this.logger.log("Stripe Digital Wallets detected");
89
95
  this.addOptionToPaymentTypeField("stripedigitalwallet", "GooglePay / ApplePay");
90
96
  // ENGrid.setBodyData(
91
97
  // "payment-type-option-apple-pay",
@@ -99,15 +105,26 @@ export class DigitalWallets {
99
105
  ENGrid.setBodyData("payment-type-option-apple-pay", "true");
100
106
  ENGrid.setBodyData("payment-type-option-google-pay", "true");
101
107
  ENGrid.setBodyData("payment-type-option-stripedigitalwallet", "true");
108
+ this.addStripeDigitalWalletListener()
109
+ ? this.logger.log("Stripe Digital Wallet listener added successfully")
110
+ : this.logger.log("Failed to add Stripe Digital Wallet listener");
102
111
  }
103
112
  addPaypalTouchDigitalWallets() {
113
+ this.logger.log("Paypal Touch Digital Wallets detected");
104
114
  this.addOptionToPaymentTypeField("paypaltouch", "Paypal / Venmo");
105
115
  ENGrid.setBodyData("payment-type-option-paypal-one-touch", "true");
106
116
  ENGrid.setBodyData("payment-type-option-venmo", "true");
117
+ this.addPaypalOneTouchListener()
118
+ ? this.logger.log("Paypal Touch listener added successfully")
119
+ : this.logger.log("Failed to add Paypal Touch listener");
107
120
  }
108
121
  addDAF() {
122
+ this.logger.log("DAF Digital Wallet detected");
109
123
  this.addOptionToPaymentTypeField("daf", "Donor Advised Fund");
110
124
  ENGrid.setBodyData("payment-type-option-daf", "true");
125
+ this.addDAFListener()
126
+ ? this.logger.log("DAF listener added successfully")
127
+ : this.logger.log("Failed to add DAF listener");
111
128
  }
112
129
  addOptionToPaymentTypeField(value, label) {
113
130
  const paymentTypeField = document.querySelector('[name="transaction.paymenttype"]');
@@ -144,12 +161,36 @@ export class DigitalWallets {
144
161
  else if (walletType === "daf") {
145
162
  this.addDAF();
146
163
  }
147
- //Disconnect observer to prevent multiple additions
164
+ //Disconnect observer and break loop to prevent multiple additions
148
165
  observer.disconnect();
166
+ break;
149
167
  }
150
168
  }
151
169
  };
152
170
  const observer = new MutationObserver(callback);
153
171
  observer.observe(node, { childList: true, subtree: true });
154
172
  }
173
+ addPaypalOneTouchListener() {
174
+ var _a, _b, _c, _d, _e;
175
+ const paypalTouch = (_d = (_c = (_b = (_a = window.EngagingNetworks) === null || _a === void 0 ? void 0 : _a.require) === null || _b === void 0 ? void 0 : _b._defined) === null || _c === void 0 ? void 0 : _c.enPaypalTouch) === null || _d === void 0 ? void 0 : _d.paypalTouch;
176
+ if (!((_e = paypalTouch === null || paypalTouch === void 0 ? void 0 : paypalTouch.library) === null || _e === void 0 ? void 0 : _e.Buttons)) {
177
+ this.logger.log("Paypal Touch library not found, cannot add listener");
178
+ return false;
179
+ }
180
+ const buttons = paypalTouch.library.Buttons.bind(paypalTouch.library);
181
+ paypalTouch.library.Buttons = (o) => buttons(Object.assign(Object.assign({}, o), { onClick: (d, a) => (this._form.dispatchIntentSubmit(),
182
+ o.onClick && o.onClick(d, a)) }));
183
+ paypalTouch.unloadButton && paypalTouch.unloadButton();
184
+ paypalTouch.loadButton && paypalTouch.loadButton();
185
+ return true;
186
+ }
187
+ addStripeDigitalWalletListener() {
188
+ var _a, _b, _c, _d, _e, _f;
189
+ return !!((_f = (_e = (_d = (_c = (_b = (_a = window.EngagingNetworks) === null || _a === void 0 ? void 0 : _a.require) === null || _b === void 0 ? void 0 : _b._defined) === null || _c === void 0 ? void 0 : _c.enStripeButtons) === null || _d === void 0 ? void 0 : _d.stripeButtons) === null || _e === void 0 ? void 0 : _e.paymentRequest) === null || _f === void 0 ? void 0 : _f.on("paymentmethod", this._form.dispatchIntentSubmit.bind(this._form)));
190
+ }
191
+ addDAFListener() {
192
+ const chariotButton = document.getElementById("chariot-button");
193
+ chariotButton === null || chariotButton === void 0 ? void 0 : chariotButton.addEventListener("click", this._form.dispatchIntentSubmit.bind(this._form));
194
+ return !!chariotButton;
195
+ }
155
196
  }
@@ -1,5 +1,6 @@
1
1
  export declare class EnForm {
2
2
  private logger;
3
+ private _onIntentSubmit;
3
4
  private _onSubmit;
4
5
  private _onValidate;
5
6
  private _onError;
@@ -10,11 +11,37 @@ export declare class EnForm {
10
11
  private static instance;
11
12
  private constructor();
12
13
  static getInstance(): EnForm;
14
+ dispatchIntentSubmit(): void;
13
15
  dispatchSubmit(): void;
14
16
  dispatchValidate(): void;
15
17
  dispatchError(): void;
16
18
  submitForm(): void;
19
+ /**
20
+ * onIntentSubmit is dispatched when a submit button is clicked,
21
+ * or a digital wallet submission is initiated,
22
+ * but before server-side validation or the actual submit event.
23
+ * This allows you to run code at the moment the user intends to submit,
24
+ * such as triggering data formatting, analytics events, or other pre-submit actions.
25
+ * Actions that rely on fully processed form data or validation results should use the onSubmit event instead.
26
+ * Note: onSubmit will also dispatch onIntentSubmit, so do not repeat actions in both events.
27
+ */
28
+ get onIntentSubmit(): import("strongly-typed-events").ISignal;
29
+ /**
30
+ * onSubmit is dispatched when the form is submitted, after validation has passed.
31
+ * This is the main event to listen to for form submissions, as it indicates that the user has successfully submitted the form and all validation checks have been passed.
32
+ * This event uses window.enOnSubmit, which is called by Engaging Networks' JavaScript when the form is submitted.
33
+ * At the time of writing, enOnSubmit does not trigger when a user submits via a digital wallet, use onIntentSubmit to listen for those submission attempts.
34
+ * Note: onSubmit will also dispatch onIntentSubmit, so do not repeat actions in both events.
35
+ */
17
36
  get onSubmit(): import("strongly-typed-events").ISignal;
18
- get onError(): import("strongly-typed-events").ISignal;
37
+ /**
38
+ * onValidate is dispatched using window.enOnValidate, which is called by Engaging Networks' JavaScript
39
+ * when the form is being validated, before submission. This only occurs after ENgrid's client-side validation has passed, but before server-side validation.
40
+ */
19
41
  get onValidate(): import("strongly-typed-events").ISignal;
42
+ /**
43
+ * onError is dispatched using window.enOnError, which is called by Engaging Networks' JavaScript when a server-side validation error occurs on form submission.
44
+ * This allows you to listen for validation errors and respond accordingly, such as displaying custom error messages or triggering analytics events.
45
+ */
46
+ get onError(): import("strongly-typed-events").ISignal;
20
47
  }
@@ -3,6 +3,7 @@ import { EngridLogger } from "..";
3
3
  export class EnForm {
4
4
  constructor() {
5
5
  this.logger = new EngridLogger("EnForm");
6
+ this._onIntentSubmit = new SignalDispatcher();
6
7
  this._onSubmit = new SignalDispatcher();
7
8
  this._onValidate = new SignalDispatcher();
8
9
  this._onError = new SignalDispatcher();
@@ -17,6 +18,10 @@ export class EnForm {
17
18
  }
18
19
  return EnForm.instance;
19
20
  }
21
+ dispatchIntentSubmit() {
22
+ this._onIntentSubmit.dispatch();
23
+ this.logger.log("dispatchIntentSubmit");
24
+ }
20
25
  dispatchSubmit() {
21
26
  this._onSubmit.dispatch();
22
27
  this.logger.log("dispatchSubmit");
@@ -40,13 +45,40 @@ export class EnForm {
40
45
  this.logger.log("submitForm");
41
46
  }
42
47
  }
48
+ /**
49
+ * onIntentSubmit is dispatched when a submit button is clicked,
50
+ * or a digital wallet submission is initiated,
51
+ * but before server-side validation or the actual submit event.
52
+ * This allows you to run code at the moment the user intends to submit,
53
+ * such as triggering data formatting, analytics events, or other pre-submit actions.
54
+ * Actions that rely on fully processed form data or validation results should use the onSubmit event instead.
55
+ * Note: onSubmit will also dispatch onIntentSubmit, so do not repeat actions in both events.
56
+ */
57
+ get onIntentSubmit() {
58
+ return this._onIntentSubmit.asEvent();
59
+ }
60
+ /**
61
+ * onSubmit is dispatched when the form is submitted, after validation has passed.
62
+ * This is the main event to listen to for form submissions, as it indicates that the user has successfully submitted the form and all validation checks have been passed.
63
+ * This event uses window.enOnSubmit, which is called by Engaging Networks' JavaScript when the form is submitted.
64
+ * At the time of writing, enOnSubmit does not trigger when a user submits via a digital wallet, use onIntentSubmit to listen for those submission attempts.
65
+ * Note: onSubmit will also dispatch onIntentSubmit, so do not repeat actions in both events.
66
+ */
43
67
  get onSubmit() {
44
68
  return this._onSubmit.asEvent();
45
69
  }
46
- get onError() {
47
- return this._onError.asEvent();
48
- }
70
+ /**
71
+ * onValidate is dispatched using window.enOnValidate, which is called by Engaging Networks' JavaScript
72
+ * when the form is being validated, before submission. This only occurs after ENgrid's client-side validation has passed, but before server-side validation.
73
+ */
49
74
  get onValidate() {
50
75
  return this._onValidate.asEvent();
51
76
  }
77
+ /**
78
+ * onError is dispatched using window.enOnError, which is called by Engaging Networks' JavaScript when a server-side validation error occurs on form submission.
79
+ * This allows you to listen for validation errors and respond accordingly, such as displaying custom error messages or triggering analytics events.
80
+ */
81
+ get onError() {
82
+ return this._onError.asEvent();
83
+ }
52
84
  }
package/dist/index.d.ts CHANGED
@@ -86,6 +86,8 @@ export * from "./optin-ladder";
86
86
  export * from "./post-donation-embed";
87
87
  export * from "./frequency-upsell-modal";
88
88
  export * from "./frequency-upsell";
89
+ export * from "./sticky-nsg";
90
+ export * from "./sticky-prepopulation";
89
91
  export * from "./preferred-payment-method";
90
92
  export * from "./events";
91
93
  export * from "./version";
package/dist/index.js CHANGED
@@ -86,6 +86,8 @@ export * from "./optin-ladder";
86
86
  export * from "./post-donation-embed";
87
87
  export * from "./frequency-upsell-modal";
88
88
  export * from "./frequency-upsell";
89
+ export * from "./sticky-nsg";
90
+ export * from "./sticky-prepopulation";
89
91
  export * from "./preferred-payment-method";
90
92
  // Events
91
93
  export * from "./events";
@@ -18,6 +18,7 @@ export interface Options {
18
18
  UseAmountValidatorFromEN?: boolean;
19
19
  SkipToMainContentLink?: boolean;
20
20
  SrcDefer?: boolean;
21
+ SuppressPurchaseEcard?: boolean;
21
22
  NeverBounceAPI?: string | null;
22
23
  NeverBounceDateField?: string | null;
23
24
  NeverBounceDateFormat?: string;
@@ -50,6 +51,10 @@ export interface Options {
50
51
  fieldClearSelectorTargetLocation?: string;
51
52
  checked?: boolean;
52
53
  };
54
+ StickyNSG?: boolean;
55
+ StickyPrepopulation?: false | {
56
+ fields: string[];
57
+ };
53
58
  TidyContact?: false | {
54
59
  cid?: string;
55
60
  page_types?: ("DONATION" | "ECARD" | "SURVEY" | "EMAILTOTARGET" | "ADVOCACY" | "SUBSCRIBEFORM" | "EVENT" | "SUPPORTERHUB" | "UNSUBSCRIBE" | "TWEETPAGE" | "UNKNOWN")[];
@@ -173,6 +178,7 @@ export interface Options {
173
178
  };
174
179
  onLoad?: () => void;
175
180
  onResize?: () => void;
181
+ onIntentSubmit?: () => void;
176
182
  onSubmit?: () => void;
177
183
  onError?: () => void;
178
184
  onValidate?: () => void;
@@ -17,6 +17,7 @@ export const OptionsDefaults = {
17
17
  UseAmountValidatorFromEN: false,
18
18
  SkipToMainContentLink: true,
19
19
  SrcDefer: true,
20
+ SuppressPurchaseEcard: false,
20
21
  NeverBounceAPI: null,
21
22
  NeverBounceDateField: null,
22
23
  NeverBounceStatusField: null,
@@ -41,6 +42,8 @@ export const OptionsDefaults = {
41
42
  CountryRedirect: false,
42
43
  WelcomeBack: false,
43
44
  OptInLadder: false,
45
+ StickyNSG: false,
46
+ StickyPrepopulation: false,
44
47
  PreferredPaymentMethod: false,
45
48
  PageLayouts: [
46
49
  "leftleft1col",
@@ -7,9 +7,10 @@ export declare class OptInLadder {
7
7
  private runAsChildThankYou;
8
8
  private inIframe;
9
9
  private saveStepToSessionStorage;
10
- private getFirstPageUrl;
11
10
  private saveOptInsToSessionStorage;
12
11
  private isEmbeddedThankYouPage;
12
+ private getPageUrl;
13
+ private getFirstPageUrl;
13
14
  private hidePage;
14
15
  private clearSessionStorage;
15
16
  }
@@ -1,8 +1,11 @@
1
- // This component is responsible for showing a ladder of checkboxes, one at a time, to the user.
2
- // If the page is not embedded in an iframe, and there are EN's Opt-In fields on the page, we will store the values to sessionStorage upon Form Submit.
3
- // If the page is embedded in an iframe and on a Thank You Page, we will look for .optin-ladder elements, compare the values to sessionStorage, and show the next checkbox in the ladder, removing all but the first match.
4
- // If the page is embedded in an iframe and on a Thank You Page, and the child iFrame is also a Thank You Page, we will look for a sessionStorage that has the current ladder step and the total number of steps.
5
- // If the current step is less than the total number of steps, we will redirect to the first page. If the current step is equal to the total number of steps, we will show the Thank You Page.
1
+ /**
2
+ * Docs: https://engrid.4sitestudios.com/component/optin-ladder
3
+ * This component is responsible for showing a ladder of checkboxes, one at a time, to the user.
4
+ * If the page is not embedded in an iframe, and there are EN's Opt-In fields on the page, we will store the values to sessionStorage upon Form Submit.
5
+ * If the page is embedded in an iframe and on a Thank You Page, we will look for .optin-ladder elements, compare the values to sessionStorage, and show the next checkbox in the ladder, removing all but the first match.
6
+ * If the page is embedded in an iframe and on a Thank You Page, and the child iFrame is also a Thank You Page, we will look for a sessionStorage that has the current ladder step and the total number of steps.
7
+ * If the current step is less than the total number of steps, we will redirect to the first page. If the current step is equal to the total number of steps, we will show the Thank You Page.
8
+ */
6
9
  import { EngridLogger, ENGrid, EnForm } from ".";
7
10
  export class OptInLadder {
8
11
  constructor() {
@@ -95,7 +98,7 @@ export class OptInLadder {
95
98
  if (!emailField || !emailField.value) {
96
99
  this.logger.log("Email field is empty");
97
100
  // Since this is a OptInLadder page with no e-mail address, hide the page
98
- this.hidePage();
101
+ this.hidePage(true);
99
102
  return;
100
103
  }
101
104
  const sessionStorageCheckboxValues = JSON.parse(sessionStorage.getItem("engrid.supporter.questions") || "{}");
@@ -179,6 +182,12 @@ export class OptInLadder {
179
182
  return;
180
183
  }
181
184
  const hasOptInLadderStop = sessionStorage.getItem("engrid.optin-ladder-stop");
185
+ const hasOptInLadderPersistStop = sessionStorage.getItem("engrid.optin-ladder-persist-stop");
186
+ if (hasOptInLadderPersistStop) {
187
+ this.logger.log("OptInLadder has been stopped with persist flag, showing the thank-you page");
188
+ sessionStorage.removeItem("engrid.optin-ladder-persist-stop");
189
+ return;
190
+ }
182
191
  if (hasOptInLadderStop) {
183
192
  this.logger.log("OptInLadder has been stopped");
184
193
  return;
@@ -186,9 +195,14 @@ export class OptInLadder {
186
195
  const sessionStorageOptInLadder = JSON.parse(sessionStorage.getItem("engrid.optin-ladder") || "{}");
187
196
  const currentStep = sessionStorageOptInLadder.step || 0;
188
197
  const totalSteps = sessionStorageOptInLadder.totalSteps || 0;
189
- if (currentStep <= totalSteps) {
190
- this.logger.log(`Current step ${currentStep} is less or equal to total steps ${totalSteps}`);
198
+ if (totalSteps === 0) {
199
+ this.logger.log("No total steps found in sessionStorage");
191
200
  this.hidePage();
201
+ return;
202
+ }
203
+ else if (currentStep <= totalSteps) {
204
+ this.logger.log(`Current step ${currentStep} is less or equal to total steps ${totalSteps}`);
205
+ this.hidePage(true);
192
206
  // Redirect to the first page
193
207
  window.location.href = this.getFirstPageUrl();
194
208
  return;
@@ -211,14 +225,6 @@ export class OptInLadder {
211
225
  sessionStorage.setItem("engrid.optin-ladder", JSON.stringify({ step, totalSteps }));
212
226
  this.logger.log(`Saved step ${step} of ${totalSteps} to sessionStorage`);
213
227
  }
214
- getFirstPageUrl() {
215
- // Get the current URL and replace the last path with 1?chain
216
- const url = new URL(window.location.href);
217
- const path = url.pathname.split("/");
218
- path.pop();
219
- path.push("1");
220
- return url.origin + path.join("/") + "?chain";
221
- }
222
228
  saveOptInsToSessionStorage(type = "parent") {
223
229
  // Grab all the checkboxes with the name starting with "supporter.questions"
224
230
  const checkboxes = document.querySelectorAll('input[name^="supporter.questions"]');
@@ -248,15 +254,32 @@ export class OptInLadder {
248
254
  isEmbeddedThankYouPage() {
249
255
  return ENGrid.getBodyData("embedded") === "thank-you-page-donation";
250
256
  }
251
- hidePage() {
252
- const engridPage = document.querySelector("#engrid");
253
- if (engridPage) {
254
- engridPage.classList.add("hide");
257
+ getPageUrl(page, chain = false) {
258
+ const url = new URL(window.location.href);
259
+ const path = url.pathname.split("/");
260
+ path[path.length - 1] = String(page);
261
+ return url.origin + path.join("/") + (chain ? "?chain" : "");
262
+ }
263
+ getFirstPageUrl() {
264
+ return this.getPageUrl(1, true);
265
+ }
266
+ hidePage(forceHide = false) {
267
+ if (ENGrid.getBodyData("opt-in-ladder-persist") === "true" && !forceHide) {
268
+ this.logger.log("Hide activated, but opt-in ladder persist is enabled, showing the thank-you page");
269
+ sessionStorage.setItem("engrid.optin-ladder-persist-stop", "Y");
270
+ window.location.href = this.getPageUrl(2);
271
+ }
272
+ else {
273
+ const engridPage = document.querySelector("#engrid");
274
+ if (engridPage) {
275
+ engridPage.classList.add("hide");
276
+ }
255
277
  }
256
278
  }
257
279
  clearSessionStorage() {
258
280
  sessionStorage.removeItem("engrid.supporter.questions");
259
281
  sessionStorage.removeItem("engrid.optin-ladder");
260
282
  sessionStorage.removeItem("engrid.optin-ladder-stop");
283
+ sessionStorage.removeItem("engrid.optin-ladder-persist-stop");
261
284
  }
262
285
  }
@@ -0,0 +1,10 @@
1
+ export declare class StickyNSG {
2
+ private logger;
3
+ private cookieName;
4
+ constructor();
5
+ private shouldRun;
6
+ private nsgActiveOnPage;
7
+ private deleteCookieIfGiftProcessComplete;
8
+ private createStickyNSGCookie;
9
+ private applyStickyNSGCookie;
10
+ }
@@ -0,0 +1,109 @@
1
+ import { EngridLogger } from "./logger";
2
+ import { ENGrid } from "./engrid";
3
+ import * as cookie from "./cookie";
4
+ export class StickyNSG {
5
+ constructor() {
6
+ this.logger = new EngridLogger("StickyNSG", "teal", "white", "📌");
7
+ this.cookieName = "engrid-sticky-nsg";
8
+ if (!this.shouldRun())
9
+ return;
10
+ this.logger.log("Sticky NSG is enabled");
11
+ this.deleteCookieIfGiftProcessComplete();
12
+ this.createStickyNSGCookie();
13
+ this.applyStickyNSGCookie();
14
+ }
15
+ shouldRun() {
16
+ return ENGrid.getOption("StickyNSG") === true;
17
+ }
18
+ /*
19
+ * Determine if NSG provided by EN is active on the page
20
+ */
21
+ nsgActiveOnPage() {
22
+ return (window.EngagingNetworks &&
23
+ window.EngagingNetworks.suggestedGift &&
24
+ typeof window.EngagingNetworks.suggestedGift === "object" &&
25
+ Object.keys(window.EngagingNetworks.suggestedGift).length > 0);
26
+ }
27
+ /*
28
+ * Delete the cookie if the gift process is complete
29
+ */
30
+ deleteCookieIfGiftProcessComplete() {
31
+ if (ENGrid.getGiftProcess()) {
32
+ this.logger.log("Gift process complete, removing sticky NSG cookie if it exists");
33
+ cookie.remove(this.cookieName);
34
+ }
35
+ }
36
+ /*
37
+ * Create the sticky NSG cookie if NSG is active on the page
38
+ */
39
+ createStickyNSGCookie() {
40
+ var _a, _b, _c, _d, _e, _f;
41
+ if (!this.nsgActiveOnPage()) {
42
+ this.logger.log("No NSG active on page, not creating sticky NSG cookie");
43
+ return;
44
+ }
45
+ const url = new URL(window.location.href);
46
+ if (url.searchParams.get("skipstickynsg") === "true") {
47
+ this.logger.log("'skipstickynsg' param present, not creating sticky NSG cookie");
48
+ return;
49
+ }
50
+ // We do some reformating to match the EngridAmounts format
51
+ // We also add "Other" to the amounts list
52
+ const nsg = window.EngagingNetworks.suggestedGift;
53
+ this.logger.log("Creating sticky NSG cookie", nsg);
54
+ const oneTimeNsg = (_a = nsg.single) === null || _a === void 0 ? void 0 : _a.reduce((acc, curr) => {
55
+ acc[curr.value] = curr.value;
56
+ return acc;
57
+ }, {});
58
+ 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;
59
+ const recurringNsg = (_d = nsg.recurring) === null || _d === void 0 ? void 0 : _d.reduce((acc, curr) => {
60
+ acc[curr.value] = curr.value;
61
+ return acc;
62
+ }, {});
63
+ 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;
64
+ const nsgCookieData = {};
65
+ if (oneTimeNsg && oneTimeDefault) {
66
+ nsgCookieData.onetime = {
67
+ amounts: oneTimeNsg,
68
+ default: oneTimeDefault,
69
+ stickyDefault: false,
70
+ };
71
+ }
72
+ if (recurringNsg && recurringDefault) {
73
+ nsgCookieData.monthly = {
74
+ amounts: recurringNsg,
75
+ default: recurringDefault,
76
+ stickyDefault: false,
77
+ };
78
+ }
79
+ if (Object.keys(nsgCookieData).length === 0) {
80
+ this.logger.log("No valid NSG data found to create sticky NSG cookie");
81
+ return;
82
+ }
83
+ const cookieValue = JSON.stringify(nsgCookieData);
84
+ cookie.set(this.cookieName, cookieValue, { path: "/", expires: 30 });
85
+ this.logger.log("Sticky NSG cookie created", cookieValue);
86
+ }
87
+ /*
88
+ * Apply the sticky NSG cookie values to window.EngridAmounts if NSG is not active on the page
89
+ */
90
+ applyStickyNSGCookie() {
91
+ if (this.nsgActiveOnPage()) {
92
+ this.logger.log("NSG active on page, not applying sticky NSG cookie, leaving the EN NSG values.");
93
+ return;
94
+ }
95
+ const cookieValue = cookie.get(this.cookieName);
96
+ if (!cookieValue) {
97
+ this.logger.log("No sticky NSG cookie found, nothing to apply");
98
+ return;
99
+ }
100
+ try {
101
+ const nsg = JSON.parse(cookieValue);
102
+ this.logger.log("Applying sticky NSG cookie values", nsg);
103
+ window.EngridAmounts = nsg;
104
+ }
105
+ catch (e) {
106
+ this.logger.error("Error parsing sticky NSG cookie, not applying", e);
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,17 @@
1
+ export declare class StickyPrepopulation {
2
+ private logger;
3
+ private options;
4
+ private cookieName;
5
+ constructor();
6
+ private shouldRun;
7
+ private deleteCookie;
8
+ private createCookie;
9
+ private applyPrepopulation;
10
+ private getSupporterDetailsFromFields;
11
+ private encryptSupporterDetails;
12
+ private decryptSupporterDetails;
13
+ private createEncryptionKey;
14
+ private arrayBufferToBase64;
15
+ private base64ToArrayBuffer;
16
+ private getSeed;
17
+ }
@@ -0,0 +1,214 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { ENGrid } from "./engrid";
11
+ import { EngridLogger } from "./logger";
12
+ import * as cookie from "./cookie";
13
+ export class StickyPrepopulation {
14
+ constructor() {
15
+ this.logger = new EngridLogger("StickyPrepopulation", "teal", "white", "📌");
16
+ this.options = { fields: [] };
17
+ this.cookieName = "engrid-sticky-prepop";
18
+ if (!this.shouldRun()) {
19
+ return;
20
+ }
21
+ this.logger.log("StickyPrepopulation initialized");
22
+ if (ENGrid.getGiftProcess()) {
23
+ this.deleteCookie();
24
+ return;
25
+ }
26
+ if (ENGrid.getPageNumber() !== 1 || !ENGrid.getField("supporter.emailAddress")) {
27
+ this.logger.log("Not on page 1 or email field not present, not creating cookie or applying pre-population.");
28
+ return;
29
+ }
30
+ this.createCookie();
31
+ this.applyPrepopulation();
32
+ }
33
+ /*
34
+ * Determine if we should run the script
35
+ * Do not run if RememberMe is active
36
+ * Do not run if on a chain link
37
+ * Only run if StickyPrepopulation option is set with fields
38
+ */
39
+ shouldRun() {
40
+ if (ENGrid.getOption("RememberMe")) {
41
+ return false;
42
+ }
43
+ const url = new URL(window.location.href);
44
+ const options = ENGrid.getOption("StickyPrepopulation");
45
+ if (options && (options === null || options === void 0 ? void 0 : options.fields.length) > 0 && !url.searchParams.has("chain")) {
46
+ this.options = options;
47
+ return true;
48
+ }
49
+ else {
50
+ return false;
51
+ }
52
+ }
53
+ /*
54
+ * Delete the cookie if the gift process is complete
55
+ */
56
+ deleteCookie() {
57
+ this.logger.log("Gift process complete, removing sticky prepopulation cookie if it exists");
58
+ cookie.remove(this.cookieName);
59
+ }
60
+ /*
61
+ * Create the cookie if we're coming from a campaign link and supporterId is present
62
+ */
63
+ createCookie() {
64
+ var _a;
65
+ return __awaiter(this, void 0, void 0, function* () {
66
+ // If we're not coming from a campaign link, don't create the cookie
67
+ if (!((_a = window.pageJson) === null || _a === void 0 ? void 0 : _a.supporterId)) {
68
+ this.logger.log("No supporterId present, not creating sticky prepopulation cookie");
69
+ return;
70
+ }
71
+ try {
72
+ const encryptedSupporterDetails = yield this.encryptSupporterDetails(this.getSupporterDetailsFromFields());
73
+ cookie.set(this.cookieName, window.btoa(JSON.stringify({
74
+ encryptedData: encryptedSupporterDetails.encryptedData,
75
+ iv: encryptedSupporterDetails.iv,
76
+ pageId: ENGrid.getPageID()
77
+ })), { path: "/", expires: 7 });
78
+ }
79
+ catch (e) {
80
+ this.logger.log("Error creating sticky prepopulation cookie");
81
+ return;
82
+ }
83
+ this.logger.log("Sticky prepopulation cookie created");
84
+ });
85
+ }
86
+ /*
87
+ * If the cookie is present and supporterId is not (it's not a campaign link prefilled by EN),
88
+ * then apply the prepopulation
89
+ */
90
+ applyPrepopulation() {
91
+ var _a;
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const cookieData = cookie.get(this.cookieName);
94
+ if (!cookieData) {
95
+ this.logger.log("No sticky prepopulation cookie found, not prepopulating fields");
96
+ return;
97
+ }
98
+ if ((_a = window.pageJson) === null || _a === void 0 ? void 0 : _a.supporterId) {
99
+ this.logger.log("SupporterId present, not applying sticky prepopulation");
100
+ return;
101
+ }
102
+ let supporterDetails = {};
103
+ try {
104
+ const encryptedSupporterDetails = JSON.parse(window.atob(cookieData));
105
+ if (!encryptedSupporterDetails || (encryptedSupporterDetails === null || encryptedSupporterDetails === void 0 ? void 0 : encryptedSupporterDetails.pageId) !== ENGrid.getPageID()) {
106
+ this.logger.log("No encrypted supporter details found in cookie, or page ID does not match");
107
+ return;
108
+ }
109
+ supporterDetails = JSON.parse(yield this.decryptSupporterDetails(this.base64ToArrayBuffer(encryptedSupporterDetails.encryptedData), new Uint8Array(this.base64ToArrayBuffer(encryptedSupporterDetails.iv))));
110
+ }
111
+ catch (e) {
112
+ this.logger.log("Error decrypting supporter details from cookie");
113
+ return;
114
+ }
115
+ this.options.fields.forEach((fieldName) => {
116
+ if (!supporterDetails[fieldName])
117
+ return;
118
+ ENGrid.setFieldValue(fieldName, decodeURIComponent(supporterDetails[fieldName]));
119
+ this.logger.log(`Setting "${fieldName}" to "${decodeURIComponent(supporterDetails[fieldName])}"`);
120
+ });
121
+ });
122
+ }
123
+ /*
124
+ * Get the supporter details from the form fields
125
+ */
126
+ getSupporterDetailsFromFields() {
127
+ const supporterDetails = {};
128
+ this.options.fields.forEach((fieldName) => {
129
+ let field = document.querySelector(`[name="${fieldName}"]`);
130
+ // If it is a radio or checkbox, get the checked value
131
+ if (field) {
132
+ if (field.type === "radio" || field.type === "checkbox") {
133
+ field = document.querySelector(`[name="${fieldName}"]:checked`);
134
+ }
135
+ supporterDetails[fieldName] = encodeURIComponent(field.value);
136
+ }
137
+ });
138
+ return supporterDetails;
139
+ }
140
+ /*
141
+ * Encrypt the supporter details
142
+ */
143
+ encryptSupporterDetails(supporterDetails) {
144
+ return __awaiter(this, void 0, void 0, function* () {
145
+ const encryptionKey = yield this.createEncryptionKey(this.getSeed());
146
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
147
+ const supporterDetailsString = JSON.stringify(supporterDetails);
148
+ const encryptedData = yield window.crypto.subtle.encrypt({
149
+ name: "AES-GCM",
150
+ iv: iv,
151
+ }, encryptionKey, new TextEncoder().encode(supporterDetailsString));
152
+ return {
153
+ encryptedData: this.arrayBufferToBase64(encryptedData),
154
+ iv: this.arrayBufferToBase64(iv),
155
+ };
156
+ });
157
+ }
158
+ /*
159
+ * Decrypt the supporter details
160
+ */
161
+ decryptSupporterDetails(encryptedSupporterDetails, iv) {
162
+ return __awaiter(this, void 0, void 0, function* () {
163
+ const encryptionKey = yield this.createEncryptionKey(this.getSeed());
164
+ const decryptedData = yield window.crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, encryptionKey, encryptedSupporterDetails);
165
+ return new TextDecoder().decode(decryptedData);
166
+ });
167
+ }
168
+ /*
169
+ * Create the encryption key
170
+ */
171
+ createEncryptionKey(seed) {
172
+ return __awaiter(this, void 0, void 0, function* () {
173
+ const encoder = new TextEncoder();
174
+ const keyMaterial = yield window.crypto.subtle.importKey("raw", encoder.encode(seed), { name: "PBKDF2" }, false, ["deriveKey"]);
175
+ return yield window.crypto.subtle.deriveKey({
176
+ name: "PBKDF2",
177
+ salt: encoder.encode(seed),
178
+ iterations: 100000,
179
+ hash: "SHA-256",
180
+ }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
181
+ });
182
+ }
183
+ /*
184
+ * Convert an ArrayBuffer to a base64 string
185
+ */
186
+ arrayBufferToBase64(buffer) {
187
+ let binary = "";
188
+ const bytes = new Uint8Array(buffer);
189
+ const len = bytes.byteLength;
190
+ for (let i = 0; i < len; i++) {
191
+ binary += String.fromCharCode(bytes[i]);
192
+ }
193
+ return window.btoa(binary);
194
+ }
195
+ /*
196
+ * Create an Array Buffer from a base64 string
197
+ */
198
+ base64ToArrayBuffer(base64) {
199
+ const binary_string = window.atob(base64);
200
+ const len = binary_string.length;
201
+ const bytes = new Uint8Array(len);
202
+ for (let i = 0; i < len; i++) {
203
+ bytes[i] = binary_string.charCodeAt(i);
204
+ }
205
+ return bytes.buffer;
206
+ }
207
+ /*
208
+ * Derive a seed from the page URL
209
+ */
210
+ getSeed() {
211
+ const url = new URL(window.location.href);
212
+ return url.origin + url.pathname + (url.searchParams.get("ea.tracking.id") ? `?ea.tracking.id=${url.searchParams.get("ea.tracking.id")}` : '');
213
+ }
214
+ }
@@ -117,7 +117,12 @@ export class SwapAmounts {
117
117
  }));
118
118
  }
119
119
  shouldRun() {
120
- return !!window.EngridAmounts;
120
+ const hasNSG = window.EngagingNetworks.suggestedGift !== undefined &&
121
+ Object.keys(window.EngagingNetworks.suggestedGift).length > 0;
122
+ if (!!window.EngridAmounts && hasNSG) {
123
+ this.logger.log("Not swapping amounts because NSG is active on page");
124
+ }
125
+ return !!window.EngridAmounts && !hasNSG;
121
126
  }
122
127
  ignoreCurrentValue() {
123
128
  const urlParam = ENGrid.getUrlParameter("transaction.donationAmt");
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const AppVersion = "0.24.4";
1
+ export declare const AppVersion = "0.25.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const AppVersion = "0.24.4";
1
+ export const AppVersion = "0.25.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4site/engrid-scripts",
3
- "version": "0.24.4",
3
+ "version": "0.25.0",
4
4
  "description": "Scripts for the ENGRID Project",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "strongly-typed-events": "^2.0.9"
26
26
  },
27
27
  "homepage": "https://github.com/4site-interactive-studios/engrid#readme",
28
- "gitHead": "591c9c9c6ae91db22b43352e23f948310a22d120",
28
+ "gitHead": "6f0962a8be8ef7211c383a7d8d7b2c188091408d",
29
29
  "devDependencies": {
30
30
  "nodemon": "^2.0.20"
31
31
  }