@4site/engrid-scripts 0.25.13 → 0.26.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.js CHANGED
@@ -187,7 +187,7 @@ export class App extends ENGrid {
187
187
  new MediaAttribution();
188
188
  // Apple Pay
189
189
  if (this.options.applePay)
190
- new ApplePay();
190
+ ApplePay.getInstance();
191
191
  // Capitalize Fields
192
192
  if (this.options.CapitalizeFields)
193
193
  new CapitalizeFields();
@@ -1,13 +1,28 @@
1
1
  import { EnForm, DonationAmount, ProcessingFees } from ".";
2
2
  export declare class ApplePay {
3
+ private logger;
3
4
  applePay: HTMLInputElement;
4
5
  _amount: DonationAmount;
5
6
  _fees: ProcessingFees;
6
7
  _form: EnForm;
8
+ beforeSession: (() => boolean) | null;
9
+ private walletFields;
10
+ private errorFields;
11
+ private static instance;
7
12
  constructor();
13
+ static getInstance(): ApplePay;
14
+ private hasApplePayOption;
8
15
  private checkApplePay;
16
+ private writeButtonContainer;
17
+ private onSubmitFallback;
18
+ onPayClicked(): void;
19
+ private preflight;
20
+ private scrollToError;
21
+ private clearErrors;
22
+ private missingMandatoryFields;
23
+ private openSession;
24
+ private onPaymentAuthorized;
25
+ private setField;
26
+ private setRegion;
9
27
  performValidation(url: string): Promise<unknown>;
10
- private log;
11
- sendPaymentToken(token: any): Promise<unknown>;
12
- private onPayClicked;
13
28
  }
package/dist/apple-pay.js CHANGED
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { EnForm, DonationAmount, ENGrid, ProcessingFees } from ".";
10
+ import { EnForm, DonationAmount, ENGrid, ProcessingFees, EngridLogger, } from ".";
11
11
  /*global window */
12
12
  const ApplePaySession = window.ApplePaySession;
13
13
  const merchantIdentifier = window.merchantIdentifier;
@@ -24,50 +24,313 @@ const merchantCapabilities = window.merchantCapabilities;
24
24
  const merchantTotalLabel = window.merchantTotalLabel;
25
25
  export class ApplePay {
26
26
  constructor() {
27
+ this.logger = new EngridLogger("ApplePay", "#000000", "#a6f3a6", "🍎");
27
28
  this.applePay = document.querySelector('.en__field__input.en__field__input--radio[value="applepay"]');
28
29
  this._amount = DonationAmount.getInstance();
29
30
  this._fees = ProcessingFees.getInstance();
30
31
  this._form = EnForm.getInstance();
32
+ // Client hook: runs after the built-in pre-flight, right before the Apple
33
+ // Pay sheet opens. Return false to abort. The hook shows its own errors
34
+ // with ENGrid.setError; the donation amount field error is cleared before
35
+ // every attempt.
36
+ this.beforeSession = null;
37
+ // Fields the wallet supplies via requiredBillingContactFields, so they are
38
+ // excluded from the mandatory-field pre-flight.
39
+ this.walletFields = [
40
+ "supporter.address1",
41
+ "supporter.address2",
42
+ "supporter.city",
43
+ "supporter.region",
44
+ "supporter.postcode",
45
+ "supporter.country",
46
+ "supporter.phoneNumber",
47
+ ];
48
+ // Field containers this component flagged with ENGrid.setError, so they can
49
+ // be cleared on the next attempt.
50
+ this.errorFields = [];
51
+ ApplePay.instance = this;
31
52
  this.checkApplePay();
32
53
  }
54
+ static getInstance() {
55
+ if (!ApplePay.instance) {
56
+ ApplePay.instance = new ApplePay();
57
+ }
58
+ return ApplePay.instance;
59
+ }
60
+ // True when the page offers Apple Pay, either as a giveBySelect radio tile
61
+ // or as an option of the payment type select.
62
+ hasApplePayOption() {
63
+ if (this.applePay)
64
+ return true;
65
+ const paymentTypeField = ENGrid.getField("transaction.paymenttype");
66
+ if (!paymentTypeField || !paymentTypeField.options)
67
+ return false;
68
+ return Array.from(paymentTypeField.options).some((option) => option.value.toLowerCase() === "applepay");
69
+ }
33
70
  checkApplePay() {
34
71
  return __awaiter(this, void 0, void 0, function* () {
35
- const pageform = document.querySelector("form.en__component--page");
36
- if (!this.applePay || !window.hasOwnProperty("ApplePaySession")) {
72
+ if (!this.hasApplePayOption() ||
73
+ !window.hasOwnProperty("ApplePaySession")) {
37
74
  const applePayContainer = document.querySelector(".en__field__item.applepay");
38
75
  if (applePayContainer)
39
76
  applePayContainer.remove();
40
- if (ENGrid.debug)
41
- console.log("Apple Pay DISABLED");
77
+ ENGrid.setBodyData("apple-pay-available", "false");
78
+ this.logger.log("DISABLED: not supported by this browser or page");
79
+ return false;
80
+ }
81
+ if (!merchantIdentifier) {
82
+ ENGrid.setBodyData("apple-pay-available", "false");
83
+ this.logger.log("DISABLED: window.merchantIdentifier is not defined");
42
84
  return false;
43
85
  }
44
- const promise = ApplePaySession.canMakePaymentsWithActiveCard(merchantIdentifier);
45
86
  let applePayEnabled = false;
46
- yield promise.then((canMakePayments) => {
47
- applePayEnabled = canMakePayments;
48
- if (canMakePayments) {
49
- let input = document.createElement("input");
50
- input.setAttribute("type", "hidden");
51
- input.setAttribute("name", "PkPaymentToken");
52
- input.setAttribute("id", "applePayToken");
53
- pageform.appendChild(input);
54
- this._form.onSubmit.subscribe(() => this.onPayClicked());
87
+ try {
88
+ applePayEnabled = yield ApplePaySession.canMakePaymentsWithActiveCard(merchantIdentifier);
89
+ }
90
+ catch (e) {
91
+ applePayEnabled = false;
92
+ }
93
+ ENGrid.setBodyData("apple-pay-available", applePayEnabled ? "true" : "false");
94
+ if (!applePayEnabled) {
95
+ this.logger.log("DISABLED: no provisioned card");
96
+ return false;
97
+ }
98
+ // Hidden field that carries the wallet token to EN. Only create it if it
99
+ // doesn't exist yet, so we never post a duplicate PkPaymentToken.
100
+ if (!ENGrid.getField("PkPaymentToken")) {
101
+ ENGrid.createHiddenInput("PkPaymentToken").setAttribute("id", "applePayToken");
102
+ }
103
+ this.writeButtonContainer();
104
+ // Fallback trigger: an implicit submit (e.g. Enter key) while Apple Pay
105
+ // is selected and no token exists yet opens the sheet instead of
106
+ // submitting. After authorization the token is set and the submit
107
+ // passes through.
108
+ this._form.onSubmit.subscribe(() => this.onSubmitFallback());
109
+ this.logger.log("ENABLED");
110
+ return true;
111
+ });
112
+ }
113
+ // Writes the native Apple Pay button container right before the submit
114
+ // button. CSS swaps it with the submit button while the applepay payment
115
+ // type is selected (data-engrid-payment-type="applepay").
116
+ writeButtonContainer() {
117
+ if (document.querySelector(".apple-pay-container"))
118
+ return;
119
+ if (!document.querySelector(".en__submit"))
120
+ return;
121
+ // The -apple-pay-button-* properties are set inline because cssnano's
122
+ // colormin rewrites the keyword "black" to #000 in built stylesheets,
123
+ // which is not a valid value for -apple-pay-button-style, so Safari
124
+ // drops it and falls back to white-outline.
125
+ ENGrid.addHtml('<div class="apple-pay-container showif-applepay-selected">' +
126
+ '<div class="apple-pay-button" role="button" tabindex="0" aria-label="Donate with Apple Pay" ' +
127
+ 'style="-apple-pay-button-type: donate; -apple-pay-button-style: black;"></div>' +
128
+ "</div>", ".en__submit", "before");
129
+ const button = document.querySelector(".apple-pay-container .apple-pay-button");
130
+ if (!button)
131
+ return;
132
+ button.addEventListener("click", () => this.onPayClicked());
133
+ button.addEventListener("keydown", (event) => {
134
+ if (event.key === "Enter" || event.key === " ") {
135
+ event.preventDefault();
136
+ this.onPayClicked();
137
+ }
138
+ });
139
+ }
140
+ onSubmitFallback() {
141
+ const applePayToken = document.getElementById("applePayToken");
142
+ if (ENGrid.getPaymentType().toLowerCase() !== "applepay" ||
143
+ (applePayToken && applePayToken.value !== "")) {
144
+ return; // Not Apple Pay, or already authorized: let the submit proceed
145
+ }
146
+ if (!this._form.submit)
147
+ return; // Another component vetoed this submit
148
+ this._form.submit = false; // Veto the submit and open the sheet instead
149
+ this.onPayClicked();
150
+ }
151
+ onPayClicked() {
152
+ if (!this.preflight())
153
+ return;
154
+ this.openSession();
155
+ }
156
+ preflight() {
157
+ this.clearErrors();
158
+ // The wallet supplies billing address and phone, but nothing else. Flag
159
+ // empty mandatory fields before the sheet opens so a donor never
160
+ // authorizes a payment EN will bounce for a missing mandatory field.
161
+ const missing = this.missingMandatoryFields();
162
+ missing.forEach((field) => {
163
+ ENGrid.setError(field, "This field is required");
164
+ this.errorFields.push(field);
165
+ });
166
+ if (missing.length) {
167
+ this.scrollToError();
168
+ return false;
169
+ }
170
+ const amount = this._amount.amount;
171
+ if (!amount || amount <= 0) {
172
+ ENGrid.setError(".en__field--donationAmt", "Please select a gift amount.");
173
+ this.scrollToError();
174
+ return false;
175
+ }
176
+ // The client hook owns the donation amount field error from here on
177
+ ENGrid.removeError(".en__field--donationAmt");
178
+ if (this.beforeSession && this.beforeSession() === false) {
179
+ this.scrollToError();
180
+ return false;
181
+ }
182
+ return true;
183
+ }
184
+ // Scrolls to the first field flagged with a validation error so the donor
185
+ // sees what needs fixing; without this the button looks unresponsive.
186
+ scrollToError() {
187
+ const errorField = (this.errorFields[0] ||
188
+ document.querySelector(".en__field--validationFailed"));
189
+ if (errorField) {
190
+ errorField.scrollIntoView({ behavior: "smooth", block: "center" });
191
+ }
192
+ }
193
+ clearErrors() {
194
+ this.errorFields.forEach((field) => ENGrid.removeError(field));
195
+ this.errorFields = [];
196
+ ENGrid.removeError(".en__field--donationAmt");
197
+ }
198
+ // Every visible mandatory field container that is empty and that the
199
+ // wallet cannot fill.
200
+ missingMandatoryFields() {
201
+ const missing = [];
202
+ document
203
+ .querySelectorAll(".en__field.en__mandatory")
204
+ .forEach((field) => {
205
+ const fieldElement = field;
206
+ if (!ENGrid.isVisible(fieldElement))
207
+ return;
208
+ const input = fieldElement.querySelector("input, select, textarea");
209
+ if (!input || !input.name)
210
+ return;
211
+ if (this.walletFields.indexOf(input.name) !== -1)
212
+ return;
213
+ if (input.type === "radio" || input.type === "checkbox") {
214
+ if (!fieldElement.querySelector("input:checked")) {
215
+ missing.push(fieldElement);
55
216
  }
56
- });
57
- if (ENGrid.debug)
58
- console.log("applePayEnabled", applePayEnabled);
59
- let applePayWrapper = this.applePay.closest(".en__field__item");
60
- if (applePayEnabled) {
61
- // Set Apple Pay Class
62
- applePayWrapper === null || applePayWrapper === void 0 ? void 0 : applePayWrapper.classList.add("applePayWrapper");
217
+ return;
63
218
  }
64
- else {
65
- // Hide Apple Pay Wrapper
66
- if (applePayWrapper)
67
- applePayWrapper.style.display = "none";
219
+ if (input.value.trim() === "") {
220
+ missing.push(fieldElement);
68
221
  }
69
- return applePayEnabled;
70
222
  });
223
+ return missing;
224
+ }
225
+ openSession() {
226
+ // ProcessingFees mirrors EN's own fee cover calculation, so the sheet
227
+ // total matches what EN will actually charge.
228
+ const donationAmount = (this._amount.amount + this._fees.fee).toFixed(2);
229
+ const request = {
230
+ supportedNetworks: merchantSupportedNetworks,
231
+ merchantCapabilities: merchantCapabilities,
232
+ countryCode: merchantCountryCode,
233
+ currencyCode: merchantCurrencyCode,
234
+ requiredBillingContactFields: ["postalAddress", "phone"],
235
+ total: {
236
+ label: merchantTotalLabel || merchantDisplayName || "Donation",
237
+ amount: donationAmount,
238
+ type: "final",
239
+ },
240
+ };
241
+ let session;
242
+ try {
243
+ session = new ApplePaySession(3, request);
244
+ }
245
+ catch (e) {
246
+ const errorTarget = (document.querySelector(".apple-pay-container") || document.querySelector(".en__submit"));
247
+ if (errorTarget) {
248
+ ENGrid.setError(errorTarget, "Apple Pay error: '" + e.message + "'");
249
+ }
250
+ this._form.dispatchError();
251
+ return;
252
+ }
253
+ const thisClass = this;
254
+ session.onvalidatemerchant = function (event) {
255
+ thisClass
256
+ .performValidation(event.validationURL)
257
+ .then(function (merchantSession) {
258
+ if (ENGrid.debug)
259
+ console.log("Apple Pay merchantSession", merchantSession);
260
+ session.completeMerchantValidation(merchantSession);
261
+ })
262
+ .catch(function () {
263
+ session.abort();
264
+ });
265
+ };
266
+ session.onpaymentauthorized = function (event) {
267
+ thisClass.onPaymentAuthorized(session, event);
268
+ };
269
+ session.oncancel = function () {
270
+ // Donor closed the sheet; return them to the form quietly.
271
+ thisClass.logger.log("Sheet cancelled by the donor");
272
+ };
273
+ session.begin();
274
+ }
275
+ onPaymentAuthorized(session, event) {
276
+ if (ENGrid.debug)
277
+ console.log("Apple Pay Token", event.payment.token);
278
+ // Pass the billing info from Apple Pay back into the EN billing fields -
279
+ // this won't happen automatically with Vantiv Apple Pay.
280
+ const billing = event.payment.billingContact || {};
281
+ const addressLines = billing.addressLines || [];
282
+ // Country goes first, dispatching change: EN swaps country-dependent
283
+ // fields (supporter.region is a select for some countries and a text
284
+ // input for others) when the country changes, so the region field must
285
+ // already be in its final shape when we fill it below.
286
+ this.setField("supporter.country", billing.countryCode, true);
287
+ this.setField("supporter.address1", addressLines[0]);
288
+ this.setField("supporter.address2", addressLines[1]);
289
+ this.setField("supporter.city", billing.locality);
290
+ if (billing.administrativeArea) {
291
+ this.setRegion(billing.administrativeArea);
292
+ }
293
+ this.setField("supporter.postcode", billing.postalCode);
294
+ this.setField("supporter.phoneNumber", billing.phone);
295
+ // Apple Pay gifts are one-time on this setup; make sure recurrpay isn't
296
+ // submitted blank when we bypass the EN submit button.
297
+ const recurrpay = ENGrid.getField("transaction.recurrpay");
298
+ if (recurrpay && !recurrpay.value)
299
+ recurrpay.value = "N";
300
+ const applePayToken = document.getElementById("applePayToken");
301
+ if (applePayToken) {
302
+ applePayToken.value = JSON.stringify(event.payment.token);
303
+ }
304
+ session.completePayment(ApplePaySession.STATUS_SUCCESS);
305
+ this._form.submitForm();
306
+ }
307
+ setField(name, value, dispatchEvents = false) {
308
+ if (value == null || value === "")
309
+ return;
310
+ if (!ENGrid.getField(name))
311
+ return;
312
+ ENGrid.setFieldValue(name, value, true, dispatchEvents);
313
+ }
314
+ // The region field is a select for countries EN has subdivisions for and a
315
+ // text input for the rest. On a select, the wallet value must match an
316
+ // option or the write is silently dropped, so match case-insensitively by
317
+ // option value or label (Apple returns subdivision codes for some
318
+ // countries and full names for others).
319
+ setRegion(value) {
320
+ const field = ENGrid.getField("supporter.region");
321
+ if (!field)
322
+ return;
323
+ if (field instanceof HTMLSelectElement) {
324
+ const option = Array.from(field.options).find((o) => o.value.toLowerCase() === value.toLowerCase() ||
325
+ o.text.toLowerCase() === value.toLowerCase());
326
+ if (!option) {
327
+ this.logger.log(`Region "${value}" doesn't match any region select option`);
328
+ return;
329
+ }
330
+ ENGrid.setFieldValue("supporter.region", option.value);
331
+ return;
332
+ }
333
+ ENGrid.setFieldValue("supporter.region", value);
71
334
  }
72
335
  performValidation(url) {
73
336
  return new Promise(function (resolve, reject) {
@@ -97,73 +360,4 @@ export class ApplePay {
97
360
  xhr.send();
98
361
  });
99
362
  }
100
- log(name, msg) {
101
- var xhr = new XMLHttpRequest();
102
- xhr.open("GET", "/ea-dataservice/rest/applepay/log?name=" + name + "&msg=" + msg);
103
- xhr.send();
104
- }
105
- sendPaymentToken(token) {
106
- return new Promise(function (resolve, reject) {
107
- resolve(true);
108
- });
109
- }
110
- onPayClicked() {
111
- if (!this._form.submit)
112
- return;
113
- const enFieldPaymentType = document.querySelector("#en__field_transaction_paymenttype");
114
- const applePayToken = document.getElementById("applePayToken");
115
- const formClass = this._form;
116
- // Only work if Payment Type is Apple Pay
117
- if (enFieldPaymentType.value == "applepay" && applePayToken.value == "") {
118
- try {
119
- let donationAmount = this._amount.amount + this._fees.fee;
120
- var request = {
121
- supportedNetworks: merchantSupportedNetworks,
122
- merchantCapabilities: merchantCapabilities,
123
- countryCode: merchantCountryCode,
124
- currencyCode: merchantCurrencyCode,
125
- total: {
126
- label: merchantTotalLabel,
127
- amount: donationAmount,
128
- },
129
- };
130
- var session = new ApplePaySession(1, request);
131
- var thisClass = this;
132
- session.onvalidatemerchant = function (event) {
133
- thisClass
134
- .performValidation(event.validationURL)
135
- .then(function (merchantSession) {
136
- if (ENGrid.debug)
137
- console.log("Apple Pay merchantSession", merchantSession);
138
- session.completeMerchantValidation(merchantSession);
139
- });
140
- };
141
- session.onpaymentauthorized = function (event) {
142
- thisClass
143
- .sendPaymentToken(event.payment.token)
144
- .then(function (success) {
145
- if (ENGrid.debug)
146
- console.log("Apple Pay Token", event.payment.token);
147
- document.getElementById("applePayToken").value = JSON.stringify(event.payment.token);
148
- formClass.submitForm();
149
- });
150
- };
151
- session.oncancel = function (event) {
152
- if (ENGrid.debug)
153
- console.log("Cancelled", event);
154
- alert("You cancelled. Sorry it didn't work out.");
155
- formClass.dispatchError();
156
- };
157
- session.begin();
158
- this._form.submit = false;
159
- return false;
160
- }
161
- catch (e) {
162
- alert("Developer mistake: '" + e.message + "'");
163
- formClass.dispatchError();
164
- }
165
- }
166
- this._form.submit = true;
167
- return true;
168
- }
169
363
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const AppVersion = "0.25.13";
1
+ export declare const AppVersion = "0.26.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const AppVersion = "0.25.13";
1
+ export const AppVersion = "0.26.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4site/engrid-scripts",
3
- "version": "0.25.13",
3
+ "version": "0.26.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": "6e3e2a64fd59921011967c02bd5c68fa2aaa3a65",
28
+ "gitHead": "c87a860f3a1f50d089c6219377dbaf87311a6914",
29
29
  "devDependencies": {
30
30
  "nodemon": "^2.0.20"
31
31
  }