@4site/engrid-scripts 0.25.11 → 0.25.12
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/interfaces/options.d.ts +3 -0
- package/dist/remember-me.d.ts +65 -0
- package/dist/remember-me.js +323 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -50,7 +50,10 @@ export interface Options {
|
|
|
50
50
|
fieldOptInSelectorTargetLocation?: string;
|
|
51
51
|
fieldClearSelectorTarget?: string;
|
|
52
52
|
fieldClearSelectorTargetLocation?: string;
|
|
53
|
+
fieldClearLabel?: string;
|
|
53
54
|
checked?: boolean;
|
|
55
|
+
encryptData?: boolean;
|
|
56
|
+
hide?: boolean;
|
|
54
57
|
};
|
|
55
58
|
StickyNSG?: boolean;
|
|
56
59
|
StickyPrepopulation?: false | {
|
package/dist/remember-me.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { EnForm, RememberMeEvents } from "./events";
|
|
|
2
2
|
export declare class RememberMe {
|
|
3
3
|
_form: EnForm;
|
|
4
4
|
_events: RememberMeEvents;
|
|
5
|
+
private _frequency;
|
|
5
6
|
private remoteUrl;
|
|
6
7
|
private cookieName;
|
|
7
8
|
private fieldNames;
|
|
@@ -9,14 +10,18 @@ export declare class RememberMe {
|
|
|
9
10
|
private cookieExpirationDays;
|
|
10
11
|
private iframe;
|
|
11
12
|
private rememberMeOptIn;
|
|
13
|
+
private encryptData;
|
|
14
|
+
private hide;
|
|
12
15
|
private fieldDonationAmountRadioName;
|
|
13
16
|
private fieldDonationAmountOtherName;
|
|
14
17
|
private fieldDonationRecurrPayRadioName;
|
|
18
|
+
private fieldDonationRecurrFreqRadioName;
|
|
15
19
|
private fieldDonationAmountOtherCheckboxID;
|
|
16
20
|
private fieldOptInSelectorTarget;
|
|
17
21
|
private fieldOptInSelectorTargetLocation;
|
|
18
22
|
private fieldClearSelectorTarget;
|
|
19
23
|
private fieldClearSelectorTargetLocation;
|
|
24
|
+
private fieldClearLabel;
|
|
20
25
|
constructor(options: {
|
|
21
26
|
remoteUrl?: string;
|
|
22
27
|
cookieName?: string;
|
|
@@ -25,12 +30,16 @@ export declare class RememberMe {
|
|
|
25
30
|
fieldDonationAmountRadioName?: string;
|
|
26
31
|
fieldDonationAmountOtherName?: string;
|
|
27
32
|
fieldDonationRecurrPayRadioName?: string;
|
|
33
|
+
fieldDonationRecurrFreqRadioName?: string;
|
|
28
34
|
fieldDonationAmountOtherCheckboxID?: string;
|
|
29
35
|
fieldOptInSelectorTarget?: string;
|
|
30
36
|
fieldOptInSelectorTargetLocation?: string;
|
|
31
37
|
fieldClearSelectorTarget?: string;
|
|
32
38
|
fieldClearSelectorTargetLocation?: string;
|
|
39
|
+
fieldClearLabel?: string;
|
|
33
40
|
checked?: boolean;
|
|
41
|
+
encryptData?: boolean;
|
|
42
|
+
hide?: boolean;
|
|
34
43
|
});
|
|
35
44
|
private updateFieldData;
|
|
36
45
|
private insertClearRememberMeLink;
|
|
@@ -43,6 +52,42 @@ export declare class RememberMe {
|
|
|
43
52
|
private saveCookieToRemote;
|
|
44
53
|
private readCookie;
|
|
45
54
|
private saveCookie;
|
|
55
|
+
/**
|
|
56
|
+
* Reads and decrypts the local (non-remote) Remember Me cookie using
|
|
57
|
+
* browser-native AES-GCM (Web Crypto), with the key held in localStorage
|
|
58
|
+
* on this device. If the key is absent (different device or cleared
|
|
59
|
+
* storage) or decryption otherwise fails, the field data is left empty
|
|
60
|
+
* and the component falls back to the normal, no-autofill experience.
|
|
61
|
+
*/
|
|
62
|
+
private readCookieEncrypted;
|
|
63
|
+
/**
|
|
64
|
+
* Encrypts the current fieldData with AES-GCM (Web Crypto) and stores the
|
|
65
|
+
* base64-encoded result in the local cookie. If encryption isn't possible
|
|
66
|
+
* (e.g. Web Crypto unavailable), nothing is written.
|
|
67
|
+
*/
|
|
68
|
+
private saveCookieEncrypted;
|
|
69
|
+
private clearCookieEncrypted;
|
|
70
|
+
/**
|
|
71
|
+
* Retrieves the per-device AES-GCM encryption key. A random secret
|
|
72
|
+
* generated once per device and held in localStorage — never written
|
|
73
|
+
* to the cookie, so it never travels with the transported value.
|
|
74
|
+
*/
|
|
75
|
+
private getEncryptionKey;
|
|
76
|
+
/**
|
|
77
|
+
* Encrypts a plaintext string with AES-GCM and returns the base64-encoded
|
|
78
|
+
* IV + ciphertext, ready for storage. Returns null if a key isn't
|
|
79
|
+
* available (e.g. Web Crypto unsupported).
|
|
80
|
+
*/
|
|
81
|
+
private encryptPayload;
|
|
82
|
+
/**
|
|
83
|
+
* Decrypts a base64-encoded IV + ciphertext payload previously produced by
|
|
84
|
+
* encryptPayload. Returns null (rather than throwing) if the key is
|
|
85
|
+
* missing or decryption otherwise fails, so callers can gracefully fall
|
|
86
|
+
* back to the standard, no-autofill experience.
|
|
87
|
+
*/
|
|
88
|
+
private decryptPayload;
|
|
89
|
+
private arrayBufferToBase64;
|
|
90
|
+
private base64ToArrayBuffer;
|
|
46
91
|
private readFields;
|
|
47
92
|
private setFieldValue;
|
|
48
93
|
private clearFields;
|
|
@@ -57,5 +102,25 @@ export declare class RememberMe {
|
|
|
57
102
|
* @param overwrite - A boolean indicating whether to overwrite the existing value of the fields. Defaults to false.
|
|
58
103
|
*/
|
|
59
104
|
private writeFields;
|
|
105
|
+
/**
|
|
106
|
+
* SwapAmounts replaces the donationAmt radio DOM nodes ~1 second after page
|
|
107
|
+
* load (triggered by DonationFrequency.load() setTimeout). When that happens
|
|
108
|
+
* the selection the RememberMe just wrote gets wiped out.
|
|
109
|
+
*
|
|
110
|
+
* This method subscribes to the first onFrequencyChange event and, after a
|
|
111
|
+
* short delay to let SwapAmounts finish its DOM update, re-applies only the
|
|
112
|
+
* donation amount. It unsubscribes immediately so it only fires once.
|
|
113
|
+
*
|
|
114
|
+
* To avoid overwriting a manual donor interaction, the handler checks
|
|
115
|
+
* whether the current amount selection is empty/wiped (as SwapAmounts does)
|
|
116
|
+
* OR still matches what writeFields originally set. If the donor already
|
|
117
|
+
* picked a different amount, we skip re-application.
|
|
118
|
+
*/
|
|
119
|
+
private reapplyDonationAmtAfterSwap;
|
|
120
|
+
/**
|
|
121
|
+
* Returns the currently selected donation amount value, or null if nothing
|
|
122
|
+
* is selected. Checks both predefined radio buttons and the "Other" text input.
|
|
123
|
+
*/
|
|
124
|
+
private getCurrentSelectedAmount;
|
|
60
125
|
private isJson;
|
|
61
126
|
}
|
package/dist/remember-me.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
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
|
+
};
|
|
1
10
|
import * as cookie from "./cookie";
|
|
2
|
-
import { EnForm, RememberMeEvents } from "./events";
|
|
11
|
+
import { EnForm, RememberMeEvents, DonationFrequency } from "./events";
|
|
3
12
|
const tippy = require("tippy.js").default;
|
|
13
|
+
// localStorage key used to cache the per-device AES-GCM encryption key.
|
|
14
|
+
// A random secret generated once per device and held in localStorage.
|
|
15
|
+
const RM_ENCRYPTION_KEY_STORAGE_NAME = "engrid-remember-me-key";
|
|
4
16
|
export class RememberMe {
|
|
5
17
|
constructor(options) {
|
|
6
18
|
this._form = EnForm.getInstance();
|
|
7
19
|
this._events = RememberMeEvents.getInstance();
|
|
20
|
+
this._frequency = DonationFrequency.getInstance();
|
|
8
21
|
this.iframe = null;
|
|
22
|
+
this.encryptData = options.encryptData ? options.encryptData : false;
|
|
23
|
+
this.hide = options.hide ? options.hide : false;
|
|
9
24
|
this.remoteUrl = options.remoteUrl ? options.remoteUrl : null;
|
|
10
25
|
this.cookieName = options.cookieName
|
|
11
26
|
? options.cookieName
|
|
@@ -25,6 +40,10 @@ export class RememberMe {
|
|
|
25
40
|
options.fieldDonationRecurrPayRadioName
|
|
26
41
|
? options.fieldDonationRecurrPayRadioName
|
|
27
42
|
: "transaction.recurrpay";
|
|
43
|
+
this.fieldDonationRecurrFreqRadioName =
|
|
44
|
+
options.fieldDonationRecurrFreqRadioName
|
|
45
|
+
? options.fieldDonationRecurrFreqRadioName
|
|
46
|
+
: "transaction.recurrfreq";
|
|
28
47
|
this.fieldDonationAmountOtherCheckboxID =
|
|
29
48
|
options.fieldDonationAmountOtherCheckboxID
|
|
30
49
|
? options.fieldDonationAmountOtherCheckboxID
|
|
@@ -43,11 +62,18 @@ export class RememberMe {
|
|
|
43
62
|
options.fieldClearSelectorTargetLocation
|
|
44
63
|
? options.fieldClearSelectorTargetLocation
|
|
45
64
|
: "before";
|
|
65
|
+
this.fieldClearLabel = options.fieldClearLabel
|
|
66
|
+
? options.fieldClearLabel
|
|
67
|
+
: "(clear autofill)";
|
|
46
68
|
this.fieldData = {};
|
|
47
69
|
if (this.useRemote()) {
|
|
48
70
|
this.createIframe(() => {
|
|
49
71
|
if (this.iframe && this.iframe.contentWindow) {
|
|
50
|
-
this.iframe.contentWindow.postMessage(JSON.stringify({
|
|
72
|
+
this.iframe.contentWindow.postMessage(JSON.stringify({
|
|
73
|
+
key: this.cookieName,
|
|
74
|
+
operation: "read",
|
|
75
|
+
encryptData: this.encryptData,
|
|
76
|
+
}), "*");
|
|
51
77
|
this._form.onSubmit.subscribe(() => {
|
|
52
78
|
if (this.rememberMeOptIn) {
|
|
53
79
|
this.readFields();
|
|
@@ -66,7 +92,9 @@ export class RememberMe {
|
|
|
66
92
|
data.key &&
|
|
67
93
|
data.value !== undefined &&
|
|
68
94
|
data.key === this.cookieName) {
|
|
69
|
-
|
|
95
|
+
if (data.value !== null) {
|
|
96
|
+
this.updateFieldData(data.value);
|
|
97
|
+
}
|
|
70
98
|
this.writeFields();
|
|
71
99
|
let hasFieldData = Object.keys(this.fieldData).length > 0;
|
|
72
100
|
if (!hasFieldData) {
|
|
@@ -74,10 +102,37 @@ export class RememberMe {
|
|
|
74
102
|
}
|
|
75
103
|
else {
|
|
76
104
|
this.insertClearRememberMeLink();
|
|
105
|
+
this.reapplyDonationAmtAfterSwap();
|
|
77
106
|
}
|
|
78
107
|
}
|
|
79
108
|
});
|
|
80
109
|
}
|
|
110
|
+
else if (this.encryptData) {
|
|
111
|
+
// Same flow as the unencrypted branch below, but the cookie payload is
|
|
112
|
+
// AES-GCM encrypted/decrypted (browser-native Web Crypto), so reading
|
|
113
|
+
// the cookie is asynchronous. A failed decrypt (foreign device or
|
|
114
|
+
// cleared localStorage) leaves fieldData empty and silently falls back
|
|
115
|
+
// to the standard, no-autofill experience.
|
|
116
|
+
this.readCookieEncrypted().then(() => {
|
|
117
|
+
let hasFieldData = Object.keys(this.fieldData).length > 0;
|
|
118
|
+
if (!hasFieldData) {
|
|
119
|
+
this.insertRememberMeOptin();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.insertClearRememberMeLink();
|
|
123
|
+
}
|
|
124
|
+
this.writeFields();
|
|
125
|
+
if (hasFieldData) {
|
|
126
|
+
this.reapplyDonationAmtAfterSwap();
|
|
127
|
+
}
|
|
128
|
+
this._form.onSubmit.subscribe(() => {
|
|
129
|
+
if (this.rememberMeOptIn) {
|
|
130
|
+
this.readFields();
|
|
131
|
+
this.saveCookieEncrypted();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
81
136
|
else {
|
|
82
137
|
this.readCookie();
|
|
83
138
|
let hasFieldData = Object.keys(this.fieldData).length > 0;
|
|
@@ -88,6 +143,9 @@ export class RememberMe {
|
|
|
88
143
|
this.insertClearRememberMeLink();
|
|
89
144
|
}
|
|
90
145
|
this.writeFields();
|
|
146
|
+
if (hasFieldData) {
|
|
147
|
+
this.reapplyDonationAmtAfterSwap();
|
|
148
|
+
}
|
|
91
149
|
this._form.onSubmit.subscribe(() => {
|
|
92
150
|
if (this.rememberMeOptIn) {
|
|
93
151
|
this.readFields();
|
|
@@ -97,24 +155,31 @@ export class RememberMe {
|
|
|
97
155
|
}
|
|
98
156
|
}
|
|
99
157
|
updateFieldData(jsonData) {
|
|
100
|
-
if (jsonData)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
158
|
+
if (!jsonData)
|
|
159
|
+
return;
|
|
160
|
+
let data;
|
|
161
|
+
try {
|
|
162
|
+
data = JSON.parse(jsonData);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
// Payload is not valid JSON (e.g. corrupted or unexpected ciphertext).
|
|
166
|
+
// Fall back silently to the no-autofill experience.
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (let i = 0; i < this.fieldNames.length; i++) {
|
|
170
|
+
if (data[this.fieldNames[i]] !== undefined) {
|
|
171
|
+
this.fieldData[this.fieldNames[i]] = decodeURIComponent(data[this.fieldNames[i]]);
|
|
106
172
|
}
|
|
107
173
|
}
|
|
108
174
|
}
|
|
109
175
|
insertClearRememberMeLink() {
|
|
110
176
|
let clearRememberMeField = document.getElementById("clear-autofill-data");
|
|
111
177
|
if (!clearRememberMeField) {
|
|
112
|
-
const clearAutofillLabel = "clear autofill";
|
|
113
178
|
clearRememberMeField = document.createElement("a");
|
|
114
179
|
clearRememberMeField.setAttribute("id", "clear-autofill-data");
|
|
115
180
|
clearRememberMeField.classList.add("label-tooltip");
|
|
116
181
|
clearRememberMeField.setAttribute("style", "cursor: pointer;");
|
|
117
|
-
clearRememberMeField.innerHTML =
|
|
182
|
+
clearRememberMeField.innerHTML = this.fieldClearLabel;
|
|
118
183
|
const targetField = this.getElementByFirstSelector(this.fieldClearSelectorTarget);
|
|
119
184
|
if (targetField) {
|
|
120
185
|
if (this.fieldClearSelectorTargetLocation === "after") {
|
|
@@ -131,6 +196,9 @@ export class RememberMe {
|
|
|
131
196
|
if (this.useRemote()) {
|
|
132
197
|
this.clearCookieOnRemote();
|
|
133
198
|
}
|
|
199
|
+
else if (this.encryptData) {
|
|
200
|
+
this.clearCookieEncrypted();
|
|
201
|
+
}
|
|
134
202
|
else {
|
|
135
203
|
this.clearCookie();
|
|
136
204
|
}
|
|
@@ -202,6 +270,9 @@ export class RememberMe {
|
|
|
202
270
|
}
|
|
203
271
|
});
|
|
204
272
|
}
|
|
273
|
+
if (this.hide) {
|
|
274
|
+
rememberMeOptInField.classList.add("hide");
|
|
275
|
+
}
|
|
205
276
|
tippy("#rememberme-learn-more-toggle", { content: rememberMeInfo });
|
|
206
277
|
}
|
|
207
278
|
}
|
|
@@ -251,6 +322,7 @@ export class RememberMe {
|
|
|
251
322
|
value: this.fieldData,
|
|
252
323
|
operation: "write",
|
|
253
324
|
expires: this.cookieExpirationDays,
|
|
325
|
+
encryptData: this.encryptData,
|
|
254
326
|
}), "*");
|
|
255
327
|
}
|
|
256
328
|
}
|
|
@@ -262,6 +334,142 @@ export class RememberMe {
|
|
|
262
334
|
expires: this.cookieExpirationDays,
|
|
263
335
|
});
|
|
264
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Reads and decrypts the local (non-remote) Remember Me cookie using
|
|
339
|
+
* browser-native AES-GCM (Web Crypto), with the key held in localStorage
|
|
340
|
+
* on this device. If the key is absent (different device or cleared
|
|
341
|
+
* storage) or decryption otherwise fails, the field data is left empty
|
|
342
|
+
* and the component falls back to the normal, no-autofill experience.
|
|
343
|
+
*/
|
|
344
|
+
readCookieEncrypted() {
|
|
345
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
346
|
+
const raw = cookie.get(this.cookieName);
|
|
347
|
+
if (!raw) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const decrypted = yield this.decryptPayload(raw);
|
|
351
|
+
if (decrypted) {
|
|
352
|
+
this.updateFieldData(decrypted);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Encrypts the current fieldData with AES-GCM (Web Crypto) and stores the
|
|
358
|
+
* base64-encoded result in the local cookie. If encryption isn't possible
|
|
359
|
+
* (e.g. Web Crypto unavailable), nothing is written.
|
|
360
|
+
*/
|
|
361
|
+
saveCookieEncrypted() {
|
|
362
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
363
|
+
const encrypted = yield this.encryptPayload(JSON.stringify(this.fieldData));
|
|
364
|
+
if (encrypted) {
|
|
365
|
+
cookie.set(this.cookieName, encrypted, {
|
|
366
|
+
expires: this.cookieExpirationDays,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
clearCookieEncrypted() {
|
|
372
|
+
this.fieldData = {};
|
|
373
|
+
this.saveCookieEncrypted();
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Retrieves the per-device AES-GCM encryption key. A random secret
|
|
377
|
+
* generated once per device and held in localStorage — never written
|
|
378
|
+
* to the cookie, so it never travels with the transported value.
|
|
379
|
+
*/
|
|
380
|
+
getEncryptionKey() {
|
|
381
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
382
|
+
if (!window.crypto || !window.crypto.subtle) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const storedKey = window.localStorage.getItem(RM_ENCRYPTION_KEY_STORAGE_NAME);
|
|
386
|
+
if (storedKey) {
|
|
387
|
+
try {
|
|
388
|
+
return yield window.crypto.subtle.importKey("raw", this.base64ToArrayBuffer(storedKey), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const key = yield window.crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
|
396
|
+
const exported = yield window.crypto.subtle.exportKey("raw", key);
|
|
397
|
+
window.localStorage.setItem(RM_ENCRYPTION_KEY_STORAGE_NAME, this.arrayBufferToBase64(exported));
|
|
398
|
+
return key;
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Encrypts a plaintext string with AES-GCM and returns the base64-encoded
|
|
407
|
+
* IV + ciphertext, ready for storage. Returns null if a key isn't
|
|
408
|
+
* available (e.g. Web Crypto unsupported).
|
|
409
|
+
*/
|
|
410
|
+
encryptPayload(plaintext) {
|
|
411
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
412
|
+
const key = yield this.getEncryptionKey();
|
|
413
|
+
if (!key) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
417
|
+
const ciphertext = yield window.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext));
|
|
418
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
419
|
+
combined.set(iv);
|
|
420
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
421
|
+
return this.arrayBufferToBase64(combined);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Decrypts a base64-encoded IV + ciphertext payload previously produced by
|
|
426
|
+
* encryptPayload. Returns null (rather than throwing) if the key is
|
|
427
|
+
* missing or decryption otherwise fails, so callers can gracefully fall
|
|
428
|
+
* back to the standard, no-autofill experience.
|
|
429
|
+
*/
|
|
430
|
+
decryptPayload(encryptedBase64) {
|
|
431
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
432
|
+
const key = yield this.getEncryptionKey();
|
|
433
|
+
if (!key) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
let combined;
|
|
437
|
+
try {
|
|
438
|
+
combined = new Uint8Array(this.base64ToArrayBuffer(encryptedBase64));
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
if (combined.length < 13) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const iv = combined.slice(0, 12);
|
|
447
|
+
const ciphertext = combined.slice(12);
|
|
448
|
+
try {
|
|
449
|
+
const decrypted = yield window.crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
450
|
+
return new TextDecoder().decode(decrypted);
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
arrayBufferToBase64(buffer) {
|
|
458
|
+
let binary = "";
|
|
459
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
460
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
461
|
+
binary += String.fromCharCode(bytes[i]);
|
|
462
|
+
}
|
|
463
|
+
return window.btoa(binary);
|
|
464
|
+
}
|
|
465
|
+
base64ToArrayBuffer(base64) {
|
|
466
|
+
const binary = window.atob(base64);
|
|
467
|
+
const bytes = new Uint8Array(binary.length);
|
|
468
|
+
for (let i = 0; i < binary.length; i++) {
|
|
469
|
+
bytes[i] = binary.charCodeAt(i);
|
|
470
|
+
}
|
|
471
|
+
return bytes.buffer;
|
|
472
|
+
}
|
|
265
473
|
readFields() {
|
|
266
474
|
for (let i = 0; i < this.fieldNames.length; i++) {
|
|
267
475
|
let fieldSelector = "[name='" + this.fieldNames[i] + "']";
|
|
@@ -272,6 +480,17 @@ export class RememberMe {
|
|
|
272
480
|
if (type === "radio" || type === "checkbox") {
|
|
273
481
|
field = document.querySelector(fieldSelector + ":checked");
|
|
274
482
|
}
|
|
483
|
+
// When the donation amount radio is set to "Other", save the actual
|
|
484
|
+
// custom value from the .other text input instead of "Other".
|
|
485
|
+
if (this.fieldNames[i] === this.fieldDonationAmountRadioName &&
|
|
486
|
+
field &&
|
|
487
|
+
field.value.toLowerCase() === "other") {
|
|
488
|
+
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
489
|
+
if (otherField && otherField.value) {
|
|
490
|
+
this.fieldData[this.fieldNames[i]] = encodeURIComponent(otherField.value);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
275
494
|
this.fieldData[this.fieldNames[i]] = encodeURIComponent(field.value);
|
|
276
495
|
}
|
|
277
496
|
else if (field.tagName === "SELECT") {
|
|
@@ -355,17 +574,36 @@ export class RememberMe {
|
|
|
355
574
|
field.click();
|
|
356
575
|
}
|
|
357
576
|
}
|
|
577
|
+
else if (this.fieldNames[i] === this.fieldDonationRecurrFreqRadioName) {
|
|
578
|
+
// recurrfreq is a radio group — find the specific radio with the saved value and click it
|
|
579
|
+
const savedValue = this.fieldData[this.fieldNames[i]];
|
|
580
|
+
if (savedValue) {
|
|
581
|
+
const freqRadio = document.querySelector(fieldSelector + "[value='" + CSS.escape(savedValue) + "']");
|
|
582
|
+
if (freqRadio) {
|
|
583
|
+
freqRadio.click();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
358
587
|
else if (this.fieldDonationAmountRadioName === this.fieldNames[i]) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"']");
|
|
588
|
+
const savedAmt = this.fieldData[this.fieldNames[i]];
|
|
589
|
+
const escapedAmt = CSS.escape(savedAmt);
|
|
590
|
+
field = document.querySelector(fieldSelector + "[value='" + escapedAmt + "']");
|
|
363
591
|
if (field) {
|
|
592
|
+
// Saved value matches a predefined radio option — just click it
|
|
364
593
|
field.click();
|
|
365
594
|
}
|
|
366
595
|
else {
|
|
367
|
-
|
|
368
|
-
|
|
596
|
+
// No matching radio: the value is a custom amount.
|
|
597
|
+
// Click the "Other" radio first so the text input becomes active,
|
|
598
|
+
// then fill in the numeric value.
|
|
599
|
+
const otherRadio = document.querySelector(fieldSelector + "[value='Other'], " +
|
|
600
|
+
fieldSelector + "[value='other'], " +
|
|
601
|
+
fieldSelector + "[value='OTHER']");
|
|
602
|
+
if (otherRadio) {
|
|
603
|
+
otherRadio.click();
|
|
604
|
+
}
|
|
605
|
+
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
606
|
+
this.setFieldValue(otherField, savedAmt, true);
|
|
369
607
|
}
|
|
370
608
|
}
|
|
371
609
|
else {
|
|
@@ -378,6 +616,74 @@ export class RememberMe {
|
|
|
378
616
|
}
|
|
379
617
|
}
|
|
380
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* SwapAmounts replaces the donationAmt radio DOM nodes ~1 second after page
|
|
621
|
+
* load (triggered by DonationFrequency.load() setTimeout). When that happens
|
|
622
|
+
* the selection the RememberMe just wrote gets wiped out.
|
|
623
|
+
*
|
|
624
|
+
* This method subscribes to the first onFrequencyChange event and, after a
|
|
625
|
+
* short delay to let SwapAmounts finish its DOM update, re-applies only the
|
|
626
|
+
* donation amount. It unsubscribes immediately so it only fires once.
|
|
627
|
+
*
|
|
628
|
+
* To avoid overwriting a manual donor interaction, the handler checks
|
|
629
|
+
* whether the current amount selection is empty/wiped (as SwapAmounts does)
|
|
630
|
+
* OR still matches what writeFields originally set. If the donor already
|
|
631
|
+
* picked a different amount, we skip re-application.
|
|
632
|
+
*/
|
|
633
|
+
reapplyDonationAmtAfterSwap() {
|
|
634
|
+
const savedAmt = this.fieldData[this.fieldDonationAmountRadioName];
|
|
635
|
+
if (!savedAmt)
|
|
636
|
+
return;
|
|
637
|
+
// Capture the amount that writeFields just set so we can detect manual changes
|
|
638
|
+
const amountAtRegistration = this.getCurrentSelectedAmount();
|
|
639
|
+
const handler = () => {
|
|
640
|
+
// SwapAmounts calls _amount.load() after swapList — give it a tick to settle
|
|
641
|
+
window.setTimeout(() => {
|
|
642
|
+
const currentAmt = this.getCurrentSelectedAmount();
|
|
643
|
+
// Only re-apply if the selection is now empty (DOM was swapped out)
|
|
644
|
+
// or still matches what we originally wrote. If the donor manually
|
|
645
|
+
// selected a different amount, respect their choice.
|
|
646
|
+
const selectionWiped = currentAmt === null || currentAmt === "";
|
|
647
|
+
const selectionUnchanged = currentAmt === amountAtRegistration;
|
|
648
|
+
if (!selectionWiped && !selectionUnchanged) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const fieldSelector = "[name='" + this.fieldDonationAmountRadioName + "']";
|
|
652
|
+
const escapedAmt = CSS.escape(savedAmt);
|
|
653
|
+
let radio = document.querySelector(fieldSelector + "[value='" + escapedAmt + "']");
|
|
654
|
+
if (radio) {
|
|
655
|
+
radio.click();
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
// Custom amount: click "Other" radio then fill the text input
|
|
659
|
+
const otherRadio = document.querySelector(fieldSelector + "[value='Other'], " +
|
|
660
|
+
fieldSelector + "[value='other'], " +
|
|
661
|
+
fieldSelector + "[value='OTHER']");
|
|
662
|
+
if (otherRadio)
|
|
663
|
+
otherRadio.click();
|
|
664
|
+
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
665
|
+
this.setFieldValue(otherField, savedAmt, true);
|
|
666
|
+
}
|
|
667
|
+
}, 200);
|
|
668
|
+
};
|
|
669
|
+
// Subscribe once: fires on the first frequency change then auto-unsubscribes
|
|
670
|
+
this._frequency.onFrequencyChange.one(handler);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Returns the currently selected donation amount value, or null if nothing
|
|
674
|
+
* is selected. Checks both predefined radio buttons and the "Other" text input.
|
|
675
|
+
*/
|
|
676
|
+
getCurrentSelectedAmount() {
|
|
677
|
+
const fieldSelector = "[name='" + this.fieldDonationAmountRadioName + "']";
|
|
678
|
+
const checkedRadio = document.querySelector(fieldSelector + ":checked");
|
|
679
|
+
if (!checkedRadio)
|
|
680
|
+
return null;
|
|
681
|
+
if (checkedRadio.value.toLowerCase() === "other") {
|
|
682
|
+
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
683
|
+
return otherField ? otherField.value : null;
|
|
684
|
+
}
|
|
685
|
+
return checkedRadio.value;
|
|
686
|
+
}
|
|
381
687
|
isJson(str) {
|
|
382
688
|
try {
|
|
383
689
|
JSON.parse(str);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AppVersion = "0.25.
|
|
1
|
+
export declare const AppVersion = "0.25.12";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AppVersion = "0.25.
|
|
1
|
+
export const AppVersion = "0.25.12";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4site/engrid-scripts",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.12",
|
|
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": "
|
|
28
|
+
"gitHead": "79a43bee4e541f226dd438124f7a0d14f3169586",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"nodemon": "^2.0.20"
|
|
31
31
|
}
|