Skip to content

Commit 53ee7a7

Browse files
Merge pull request #230 from processout/bugfix/NO-TICKET-add-telemetry-calls-to-card-field
feat: enhance CardField with cleanup and error handling improvements
2 parents d0a9dc0 + e49578b commit 53ee7a7

2 files changed

Lines changed: 240 additions & 54 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "processout.js",
3-
"version": "1.6.6",
3+
"version": "1.6.7",
44
"description": "ProcessOut.js is a JavaScript library for ProcessOut's payment processing API.",
55
"scripts": {
66
"build:processout": "tsc -p src/processout && uglifyjs --compress --keep-fnames --ie8 dist/processout.js -o dist/processout.js",

src/processout/cardfield.ts

Lines changed: 239 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ module ProcessOut {
167167
*/
168168
protected handlers: { [key: string]: ((e: any) => void)[] } = {} ;
169169

170+
/**
171+
* Whether this CardField instance has been destroyed/cleaned up
172+
* @var {boolean}
173+
*/
174+
protected destroyed: boolean = false;
175+
176+
/**
177+
* Reference to the message event listener for cleanup
178+
* @var {function}
179+
*/
180+
protected messageListener: (event: MessageEvent) => void;
181+
182+
/**
183+
* MutationObserver to detect iframe removal from DOM
184+
* @var {MutationObserver}
185+
*/
186+
protected mutationObserver: MutationObserver;
187+
170188
/**
171189
* CardField constructor
172190
* @param {ProcessOut} instance
@@ -212,6 +230,10 @@ module ProcessOut {
212230

213231

214232
private postMessage(message: any, retries: number = 3, delay: number = 50): void {
233+
if (this.destroyed) {
234+
return;
235+
}
236+
215237
if (retries <= 0) {
216238
throw new Exception("processout-js.field.unavailable", "Tried to locate the iframe content window but failed.");
217239
}
@@ -252,12 +274,15 @@ module ProcessOut {
252274
// Hide the field until it's ready
253275
this.iframe.style.display = "none";
254276
this.iframe.height = "14px"; // Default height
277+
278+
if (typeof(error) !== typeof(Function)) {
279+
error = function () {}
280+
}
255281

256282
var errored = false;
257283
var iframeError = setTimeout(function() {
258284
errored = true;
259-
if (typeof(error) === typeof(Function))
260-
error(new Exception("processout-js.field.unavailable"));
285+
error(new Exception("processout-js.field.unavailable"));
261286
}, CardField.timeout);
262287

263288
this.iframe.onload = function() {
@@ -273,9 +298,9 @@ module ProcessOut {
273298
} catch(e) { /* ... */ }
274299
}.bind(this);
275300

276-
// Hook the ok message
277-
window.addEventListener("message", function (event) {
278-
if (errored)
301+
// Hook the ok message - store reference for cleanup
302+
this.messageListener = function (event: MessageEvent) {
303+
if (errored || this.destroyed)
279304
return;
280305

281306
try {
@@ -336,10 +361,78 @@ module ProcessOut {
336361
message: e.message,
337362
stack: e.stack,
338363
});
364+
error(e)
339365
}
340-
}.bind(this));
366+
}.bind(this);
367+
window.addEventListener("message", this.messageListener);
341368

342369
this.el.appendChild(this.iframe);
370+
371+
// Set up MutationObserver to detect iframe removal and cleanup
372+
this.setupUnmountObserver();
373+
}
374+
375+
/**
376+
* Sets up a MutationObserver to detect when the iframe is removed from the DOM
377+
* and automatically cleans up event listeners to prevent memory leaks and errors
378+
* @return {void}
379+
*/
380+
protected setupUnmountObserver(): void {
381+
// Check if MutationObserver is available (not in very old browsers)
382+
if (typeof MutationObserver === 'undefined') {
383+
return;
384+
}
385+
386+
this.mutationObserver = new MutationObserver((mutations) => {
387+
for (const mutation of mutations) {
388+
for (const removedNode of Array.from(mutation.removedNodes)) {
389+
// Check if our iframe was removed directly or as part of a parent
390+
if (removedNode === this.iframe ||
391+
(removedNode instanceof Element && removedNode.contains(this.iframe))) {
392+
this.destroy();
393+
return;
394+
}
395+
}
396+
}
397+
});
398+
399+
// Observe the document body for child removals (subtree to catch parent removals)
400+
this.mutationObserver.observe(document.body, {
401+
childList: true,
402+
subtree: true
403+
});
404+
}
405+
406+
/**
407+
* Destroys this CardField instance, removing all event listeners
408+
* and cleaning up resources. Called automatically when iframe is
409+
* removed from DOM, or can be called manually.
410+
* @return {void}
411+
*/
412+
public destroy(): void {
413+
if (this.destroyed) {
414+
return;
415+
}
416+
417+
this.destroyed = true;
418+
419+
// Remove the message event listener
420+
if (this.messageListener) {
421+
window.removeEventListener("message", this.messageListener);
422+
this.messageListener = null;
423+
}
424+
425+
// Disconnect the MutationObserver
426+
if (this.mutationObserver) {
427+
this.mutationObserver.disconnect();
428+
this.mutationObserver = null;
429+
}
430+
431+
// Clear handlers
432+
this.handlers = {};
433+
434+
// Clear references
435+
this.iframe = null;
343436
}
344437

345438
/**
@@ -425,12 +518,23 @@ module ProcessOut {
425518
this.options.style = (<any>Object).assign(
426519
this.options.style, options.style);
427520

428-
this.postMessage(JSON.stringify({
429-
"namespace": Message.fieldNamespace,
430-
"projectID": this.instance.getProjectID(),
431-
"action": "update",
432-
"data": this.options
433-
}));
521+
try {
522+
this.postMessage(JSON.stringify({
523+
"namespace": Message.fieldNamespace,
524+
"projectID": this.instance.getProjectID(),
525+
"action": "update",
526+
"data": this.options
527+
}));
528+
} catch (err) {
529+
this.instance.telemetryClient.reportError({
530+
host: "processout-js",
531+
fileName: "cardfield.ts",
532+
lineNumber: 533,
533+
message: err.message,
534+
stack: err.stack,
535+
});
536+
throw err;
537+
}
434538
}
435539

436540
/**
@@ -445,12 +549,23 @@ module ProcessOut {
445549
this.handlers[e] = [];
446550

447551
this.handlers[e].push(h);
448-
this.postMessage(JSON.stringify({
449-
"namespace": Message.fieldNamespace,
450-
"projectID": this.instance.getProjectID(),
451-
"action": "registerEvent",
452-
"data": e
453-
}));
552+
try {
553+
this.postMessage(JSON.stringify({
554+
"namespace": Message.fieldNamespace,
555+
"projectID": this.instance.getProjectID(),
556+
"action": "registerEvent",
557+
"data": e
558+
}));
559+
} catch (err) {
560+
this.instance.telemetryClient.reportError({
561+
host: "processout-js",
562+
fileName: "cardfield.ts",
563+
lineNumber: 563,
564+
message: err.message,
565+
stack: err.stack,
566+
});
567+
throw err;
568+
}
454569
}
455570

456571
/**
@@ -468,25 +583,47 @@ module ProcessOut {
468583
* @return {void}
469584
*/
470585
public blur(): void {
471-
this.postMessage(JSON.stringify({
472-
"messageID": Math.random().toString(),
473-
"namespace": Message.fieldNamespace,
474-
"projectID": this.instance.getProjectID(),
475-
"action": "blur"
476-
}));
586+
try {
587+
this.postMessage(JSON.stringify({
588+
"messageID": Math.random().toString(),
589+
"namespace": Message.fieldNamespace,
590+
"projectID": this.instance.getProjectID(),
591+
"action": "blur"
592+
}));
593+
} catch (err) {
594+
this.instance.telemetryClient.reportError({
595+
host: "processout-js",
596+
fileName: "cardfield.ts",
597+
lineNumber: 596,
598+
message: err.message,
599+
stack: err.stack,
600+
});
601+
throw err;
602+
}
477603
}
478604

479605
/**
480606
* focus focuses on the card field
481607
* @return {void}
482608
*/
483609
public focus(): void {
484-
this.postMessage(JSON.stringify({
485-
"messageID": Math.random().toString(),
486-
"namespace": Message.fieldNamespace,
487-
"projectID": this.instance.getProjectID(),
488-
"action": "focus"
489-
}));
610+
try {
611+
this.postMessage(JSON.stringify({
612+
"messageID": Math.random().toString(),
613+
"namespace": Message.fieldNamespace,
614+
"projectID": this.instance.getProjectID(),
615+
"action": "focus"
616+
}));
617+
} catch (err) {
618+
this.instance.telemetryClient.reportError({
619+
host: "processout-js",
620+
fileName: "cardfield.ts",
621+
lineNumber: 619,
622+
message: err.message,
623+
stack: err.stack,
624+
});
625+
throw err;
626+
}
490627
}
491628

492629
/**
@@ -499,13 +636,29 @@ module ProcessOut {
499636
error: (err: Exception) => void): void {
500637
var id = Math.random().toString();
501638

639+
if (typeof(error) !== typeof(Function)) {
640+
error = () => {};
641+
}
642+
502643
// Ask the iframe for its value
503-
this.postMessage(JSON.stringify({
504-
"messageID": id,
505-
"namespace": Message.fieldNamespace,
506-
"projectID": this.instance.getProjectID(),
507-
"action": "validate"
508-
}));
644+
try {
645+
this.postMessage(JSON.stringify({
646+
"messageID": id,
647+
"namespace": Message.fieldNamespace,
648+
"projectID": this.instance.getProjectID(),
649+
"action": "validate"
650+
}));
651+
} catch (err) {
652+
this.instance.telemetryClient.reportError({
653+
host: "processout-js",
654+
fileName: "cardfield.ts",
655+
lineNumber: 648,
656+
message: err.message,
657+
stack: err.stack,
658+
});
659+
error(err);
660+
return;
661+
}
509662

510663
// Our timeout, just in case
511664
var fetchingTimeout =
@@ -546,19 +699,36 @@ module ProcessOut {
546699
public tokenize(fields: any[], data: any, success: (token: string, card: Card) => void,
547700
error: (err: Exception) => void): void {
548701

702+
if (typeof(error) !== typeof(Function)) {
703+
error = () => {};
704+
}
705+
549706
// Tell our field it should start the tokenization process and
550707
// expect a response
551708
var id = Math.random().toString();
552-
this.postMessage(JSON.stringify({
553-
"messageID": id,
554-
"namespace": Message.fieldNamespace,
555-
"projectID": this.instance.getProjectID(),
556-
"action": "tokenize",
557-
"data": {
558-
"fields": fields,
559-
"data": data
560-
}
561-
}));
709+
710+
try {
711+
this.postMessage(JSON.stringify({
712+
"messageID": id,
713+
"namespace": Message.fieldNamespace,
714+
"projectID": this.instance.getProjectID(),
715+
"action": "tokenize",
716+
"data": {
717+
"fields": fields,
718+
"data": data
719+
}
720+
}));
721+
} catch (err) {
722+
this.instance.telemetryClient.reportError({
723+
host: "processout-js",
724+
fileName: "cardfield.ts",
725+
lineNumber: 708,
726+
message: err.message,
727+
stack: err.stack,
728+
});
729+
error(err);
730+
return;
731+
}
562732

563733
// Our timeout, just in case
564734
var fetchingTimeout =
@@ -594,16 +764,32 @@ module ProcessOut {
594764
public refreshCVC(cardUID: string, success: (token: string) => void,
595765
error: (err: Exception) => void): void {
596766

767+
if (typeof(error) !== typeof(Function)) {
768+
error = () => {};
769+
}
770+
597771
// Tell our field it should start the tokenization process and
598772
// expect a response
599773
var id = Math.random().toString();
600-
this.postMessage(JSON.stringify({
601-
"messageID": id,
602-
"namespace": Message.fieldNamespace,
603-
"projectID": this.instance.getProjectID(),
604-
"action": "refresh-cvc",
605-
"data": cardUID
606-
}));
774+
try {
775+
this.postMessage(JSON.stringify({
776+
"messageID": id,
777+
"namespace": Message.fieldNamespace,
778+
"projectID": this.instance.getProjectID(),
779+
"action": "refresh-cvc",
780+
"data": cardUID
781+
}));
782+
} catch (err) {
783+
this.instance.telemetryClient.reportError({
784+
host: "processout-js",
785+
fileName: "cardfield.ts",
786+
lineNumber: 779,
787+
message: err.message,
788+
stack: err.stack,
789+
});
790+
error(err);
791+
return;
792+
}
607793

608794
// Our timeout, just in case
609795
var fetchingTimeout =

0 commit comments

Comments
 (0)