@@ -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