PageSourceSearch

https://pairfection.us/wp-content/plugins/power-coupons/public/assets/js/frontend.js?ver=1.0.7

js pairfection.us collected 2026-10-04 06:18:11 UTC 24,507 bytes, 865 lines download raw bytes

1/**
2 * Power Coupons Public JavaScript
3 *
4 * Handles coupon apply/remove actions and refreshes on the frontend.
5 * Uses jQuery for DOM manipulation and AJAX requests.
6 *
7 * @param {Object} $ jQuery object
8 * @package
9 * @since 1.0.0
10 */
11
12( function ( $ ) {
13	'use strict';
14
15	/**
16	 * Power Coupons Main Object
17	 *
18	 * Manages all coupon-related functionality on the frontend.
19	 */
20	const PowerCoupons = {
21		/**
22		 * Active AJAX requests tracker
23		 * Prevents duplicate requests from multiple clicks
24		 *
25		 * @type {Object}
26		 */
27		activeRequests: {
28			apply: null,
29			remove: null,
30			refresh: null,
31		},
32
33		/**
34		 * Request timeout, in milliseconds.
35		 *
36		 * activeRequests gates apply/remove/refresh so a double click cannot
37		 * fire twice, and each slot is only released in the request's
38		 * `complete` handler. jQuery does not time a request out on its own, so
39		 * a connection that stalls rather than fails leaves its slot occupied
40		 * and the button inert for as long as the browser holds the socket.
41		 *
42		 * @type {number}
43		 */
44		requestTimeout: 30000,
45
46		/**
47		 * Initialize the Power Coupons functionality
48		 *
49		 * Sets up event handlers and WooCommerce integrations.
50		 *
51		 * @since 1.0.0
52		 */
53		init() {
54			// Validate required data is available
55			if ( ! this.validateDependencies() ) {
56				console.error( 'Power Coupons: Required data not available' );
57				return;
58			}
59
60			// Setup WooCommerce event handlers
61			this.handleWooCommerceEvents();
62
63			// Bind click events for apply/remove buttons
64			this.bindEvents();
65		},
66
67		/**
68		 * Sync every cart-dependent UI after the server-side cart changed.
69		 *
70		 * Refreshes in place through the shared cart-refresh helper, which
71		 * picks the right mechanism for the current context (Blocks data
72		 * store, classic checkout, classic cart).
73		 *
74		 * A full page reload is opt-in only, via the
75		 * `power_coupons_reload_page_after_coupon_is_applied` filter: reloading
76		 * discards the in-page state that multi-step checkout plugins such as
77		 * CartFlows and FunnelKit hold in the browser, which sends the shopper
78		 * back to step one.
79		 *
80		 * @since 1.0.6
81		 * @return {Promise} Resolves once the cart UI is in sync.
82		 */
83		syncCartUi() {
84			if ( powerCouponsData.reloadPageAfterCouponApplied ) {
85				window.location.reload();
86
87				// Never resolves: the document is being torn down, so no
88				// follow-up UI work should run.
89				return new Promise( function () {} );
90			}
91
92			if ( ! window.PowerCouponsCartRefresh ) {
93				return Promise.resolve( false );
94			}
95
96			return window.PowerCouponsCartRefresh.refresh( {
97				source: 'coupon-list',
98			} );
99		},
100
101		/**
102		 * Validate that all required dependencies are available
103		 *
104		 * Checks for powerCouponsData object and required AJAX URLs.
105		 *
106		 * @since 1.0.0
107		 * @return {boolean} True if all dependencies are available
108		 */
109		validateDependencies() {
110			if ( typeof powerCouponsData === 'undefined' ) {
111				return false;
112			}
113
114			if ( ! powerCouponsData.ajaxUrl || ! powerCouponsData.nonce ) {
115				return false;
116			}
117
118			return true;
119		},
120
121		/**
122		 * Escape a value for use inside an attribute selector.
123		 *
124		 * Extracted for readability; the fallback below is belt-and-braces and
125		 * is not expected to run. The audit flagged the bare `CSS.escape` call
126		 * as unpolyfilled, but this file already uses optional chaining and
127		 * `??` and is shipped untranspiled, so any engine old enough to lack
128		 * `CSS.escape` (pre-2016) fails to parse the file long before reaching
129		 * this line — and `.browserslistrc` targets the last four versions of
130		 * each major browser anyway. Keeping the guard costs nothing and makes
131		 * the selector construction explicit.
132		 *
133		 * @since 1.0.7
134		 * @param {string} value Raw value, typically a coupon code.
135		 * @return {string} Value safe to interpolate into a selector.
136		 */
137		escapeForSelector( value ) {
138			const raw = String( value ?? '' );
139
140			if (
141				typeof window.CSS !== 'undefined' &&
142				typeof window.CSS.escape === 'function'
143			) {
144				return window.CSS.escape( raw );
145			}
146
147			return raw.replace( /["'\\\]\[\s]/g, '\\$&' );
148		},
149
150		/**
151		 * Pending deferred refresh timer, if one is queued.
152		 *
153		 * @type {?number}
154		 */
155		refreshTimer: null,
156
157		/**
158		 * Queue a coupon-list refresh to run after the current task.
159		 *
160		 * WooCommerce Blocks calls its checkout filters during render and
161		 * expects them to be pure value transforms. Calling
162		 * ajaxRefreshCouponsHTML() straight from one issued an XHR from inside
163		 * someone else's render pass; deferring by a tick moves the side effect
164		 * out of it, and the single timer collapses any repeat calls within one
165		 * pass into a single request.
166		 *
167		 * Measured on a real block cart, this is request-count neutral for a
168		 * quantity change (8 requests over 4 changes, before and after) — the
169		 * old code's abort-and-retry already collapsed most of the repeats. The
170		 * point of this is the render-pass contract, not throughput.
171		 *
172		 * @since 1.0.7
173		 * @return {void}
174		 */
175		scheduleCouponsRefresh() {
176			if ( null !== PowerCoupons.refreshTimer ) {
177				return;
178			}
179
180			PowerCoupons.refreshTimer = window.setTimeout( function () {
181				PowerCoupons.refreshTimer = null;
182				PowerCoupons.ajaxRefreshCouponsHTML();
183			}, 0 );
184		},
185
186		/**
187		 * Normalise a coupon code so both sources compare equal.
188		 *
189		 * The order review row carries the code as a `coupon-<code>` class run
190		 * through WordPress's `sanitize_title()`, while the cards carry it raw
191		 * in `data-coupon`. A code containing a space or symbol would otherwise
192		 * never match itself and the two views would look permanently out of
193		 * sync.
194		 *
195		 * @since 1.0.7
196		 * @param {string} code Raw or sanitised coupon code.
197		 * @return {string} Comparable form.
198		 */
199		normalizeCouponCode( code ) {
200			return String( code ?? '' )
201				.toLowerCase()
202				.replace( /[^a-z0-9]+/g, '-' )
203				.replace( /^-+|-+$/g, '' );
204		},
205
206		/**
207		 * Coupons the cart says are applied, read from the order review.
208		 *
209		 * Read from the `tr.cart-discount` rows rather than any one plugin's
210		 * remove control: WooCommerce renders
211		 * `a.woocommerce-remove-coupon` and CartFlows renders
212		 * `a.wcf-remove-coupon` inside the same row, but both put the code in a
213		 * `coupon-<code>` class on the row itself.
214		 *
215		 * @since 1.0.7
216		 * @return {string} Sorted, comma-separated coupon codes.
217		 */
218		appliedCouponSignature() {
219			const codes = [];
220
221			document
222				.querySelectorAll( '.cart-discount' )
223				.forEach( function ( row ) {
224					const fromControl = row.querySelector( '[data-coupon]' );
225
226					if ( fromControl && fromControl.dataset.coupon ) {
227						codes.push(
228							PowerCoupons.normalizeCouponCode(
229								fromControl.dataset.coupon
230							)
231						);
232
233						return;
234					}
235
236					row.classList.forEach( function ( className ) {
237						if ( 0 === className.indexOf( 'coupon-' ) ) {
238							codes.push(
239								PowerCoupons.normalizeCouponCode(
240									className.slice( 'coupon-'.length )
241								)
242							);
243						}
244					} );
245				} );
246
247			return PowerCoupons.toSignature( codes );
248		},
249
250		/**
251		 * Coupons the rendered cards currently show as applied.
252		 *
253		 * @since 1.0.7
254		 * @return {string} Sorted, comma-separated coupon codes.
255		 */
256		renderedCouponSignature() {
257			const codes = [];
258
259			document
260				.querySelectorAll(
261					'.power-coupons-apply-coupon-btn[data-coupon-status="applied"]'
262				)
263				.forEach( function ( button ) {
264					if ( button.dataset.coupon ) {
265						codes.push(
266							PowerCoupons.normalizeCouponCode(
267								button.dataset.coupon
268							)
269						);
270					}
271				} );
272
273			return PowerCoupons.toSignature( codes );
274		},
275
276		/**
277		 * Reduce a list of codes to a comparable signature.
278		 *
279		 * @since 1.0.7
280		 * @param {Array<string>} codes Coupon codes.
281		 * @return {string} Sorted, de-duplicated, comma-separated codes.
282		 */
283		toSignature( codes ) {
284			return Array.from( new Set( codes.filter( Boolean ) ) )
285				.sort()
286				.join( ',' );
287		},
288
289		/**
290		 * Re-render the coupon list when it disagrees with the cart.
291		 *
292		 * Compares two live views — what the order review says is applied
293		 * against what the cards show — rather than diffing against a
294		 * remembered value. A remembered baseline goes stale whenever the cart
295		 * changes through a route that does not refresh both, and then
296		 * suppresses the very refresh it exists to trigger. Comparing the two
297		 * current views instead makes this self-correcting: it fires whenever
298		 * they drift apart, however they got there, and stays quiet when they
299		 * agree.
300		 *
301		 * That quiet matters — `updated_checkout` fires on address edits,
302		 * shipping switches and quantity changes, none of which touch coupons.
303		 *
304		 * @since 1.0.7
305		 * @return {void}
306		 */
307		refreshIfCouponsChanged() {
308			if (
309				PowerCoupons.appliedCouponSignature() ===
310				PowerCoupons.renderedCouponSignature()
311			) {
312				return;
313			}
314
315			// No coupon code: take the whole-list branch, because this path
316			// only knows the views drifted, not which coupon moved.
317			PowerCoupons.ajaxRefreshCouponsHTML();
318		},
319
320		/**
321		 * AJAX function to refresh coupons HTML
322		 *
323		 * Fetches updated coupon list from server and replaces existing list.
324		 * Uses modern DOM APIs for better performance.
325		 * @param {Event}  event      Click event.
326		 * @param {string} couponCode Removed coupon code.
327		 */
328		ajaxRefreshCouponsHTML( event, couponCode ) {
329			// Cancel any pending refresh request
330			if ( PowerCoupons.activeRequests.refresh ) {
331				PowerCoupons.activeRequests.refresh.abort();
332			}
333
334			const dataContext = $( '.power-coupons-list' ).attr(
335				'data-context'
336			);
337
338			// Make AJAX request to get fresh coupon HTML
339			PowerCoupons.activeRequests.refresh = $.ajax( {
340				type: 'GET',
341				url: powerCouponsData.ajaxUrl.getCouponsHtml,
342				timeout: PowerCoupons.requestTimeout,
343				data: {
344					nonce: powerCouponsData.nonce,
345					context: dataContext || 'ajax',
346					couponCode,
347				},
348				success( response ) {
349					if ( parseInt( response.coupon_id, 10 ) > 0 ) {
350						const selector = `.power-coupons-apply-coupon-btn[data-coupon="${ PowerCoupons.escapeForSelector(
351							response.coupon_code
352						) }"]`;
353
354						const couponCards =
355							document.querySelectorAll( selector );
356
357						const template = document.createElement( 'template' );
358						template.innerHTML = response.html.trim();
359
360						// The response is a whole `.power-coupons-list` wrapper
361						// containing just this coupon, but each coupon IS the
362						// button (see views/coupon-list.php), and every button
363						// shares one `.power-coupons-section` parent. Replacing
364						// the button's grandparent therefore swapped the entire
365						// list for a single card and silently dropped every
366						// other coupon — visible in the drawer, which renders
367						// them all. Swap the button itself instead.
368						const freshCard =
369							template.content.querySelector( selector );
370
371						if ( ! freshCard ) {
372							return;
373						}
374
375						couponCards.forEach( function ( couponCard ) {
376							couponCard.replaceWith(
377								freshCard.cloneNode( true )
378							);
379						} );
380
381						return;
382					}
383
384					const drawerList = document.querySelector(
385						'.power-coupons-drawer-coupons-list'
386					);
387
388					if ( drawerList ) {
389						drawerList.innerHTML = response.html;
390
391						// Trigger custom event for extensions.
392						jQuery( document ).trigger( 'powerCoupons:refreshed' );
393					}
394				},
395				error( jqXHR, textStatus ) {
396					if ( textStatus !== 'abort' ) {
397						// Leave the stale list in place rather than reloading:
398						// the cart itself is already correct, and a reload
399						// would reset multi-step checkout progress.
400						console.error(
401							'Power Coupons: Failed to refresh coupons list'
402						);
403					}
404				},
405				complete() {
406					// Clear request reference
407					PowerCoupons.activeRequests.refresh = null;
408				},
409			} );
410		},
411
412		/**
413		 * Handle WooCommerce-specific events
414		 *
415		 * Refreshes coupon list when coupons are applied/removed via:
416		 * - Classic WooCommerce cart/checkout forms
417		 * - WooCommerce Blocks (cart/checkout blocks)
418		 *
419		 * @since 1.0.0
420		 */
421		handleWooCommerceEvents() {
422			// ============================================
423			// Classic WooCommerce Cart/Checkout Events
424			// ============================================
425
426			// Coupon applied in classic cart
427			$( 'body' ).on(
428				'applied_coupon',
429				PowerCoupons.ajaxRefreshCouponsHTML
430			);
431
432			// Coupon removed in classic cart
433			$( 'body' ).on(
434				'removed_coupon',
435				PowerCoupons.ajaxRefreshCouponsHTML
436			);
437
438			// Coupon applied in classic checkout
439			$( 'body' ).on(
440				'applied_coupon_in_checkout',
441				PowerCoupons.ajaxRefreshCouponsHTML
442			);
443
444			// Coupon removed in classic checkout
445			$( 'body' ).on(
446				'removed_coupon_in_checkout',
447				PowerCoupons.ajaxRefreshCouponsHTML
448			);
449
450			// Coupon removed in classic checkout
451			$( 'body' ).on(
452				'updated_cart_totals',
453				PowerCoupons.ajaxRefreshCouponsHTML
454			);
455
456			// Checkouts that never fire WooCommerce's coupon events.
457			//
458			// CartFlows' step checkout applies and removes coupons through its
459			// own handlers and its own `a.wcf-remove-coupon` control, and on
460			// success triggers only `update_checkout`
461			// (cartflows/assets/js/checkout-template.js). None of the five
462			// events above fire, so removing a coupon from the step's order
463			// summary left the drawer still showing it as applied.
464			//
465			// Watching `updated_checkout` covers any such integration without
466			// needing to know about it, and comparing the applied-coupon set
467			// keeps the ordinary checkout refreshes free.
468			$( 'body' ).on( 'updated_checkout', function () {
469				PowerCoupons.refreshIfCouponsChanged();
470			} );
471
472			// ============================================
473			// WooCommerce Blocks Integration
474			// ============================================
475
476			// Check if WooCommerce Blocks API is available
477			if (
478				window &&
479				window.wc &&
480				window.wc.blocksCheckout &&
481				typeof window.wc.blocksCheckout.registerCheckoutFilters ===
482					'function'
483			) {
484				/*
485				 * These are the only signal Blocks gives us that the cart
486				 * changed, but Blocks treats them as pure value transforms and
487				 * calls them during render — `totalValue` once per totals row.
488				 *
489				 * So each one records that a refresh is wanted and returns the
490				 * value untouched. The refresh itself is queued out of the
491				 * render pass by scheduleCouponsRefresh(), which also collapses
492				 * a whole render's worth of calls into a single request.
493				 */
494				window.wc.blocksCheckout.registerCheckoutFilters(
495					'powerCouponsRefreshCoupons',
496					{
497						/**
498						 * Cart totals changed.
499						 *
500						 * @param {string} defaultValue - The formatted total.
501						 * @return {string} The default value (pass through)
502						 */
503						totalValue( defaultValue ) {
504							PowerCoupons.scheduleCouponsRefresh();
505							return defaultValue;
506						},
507
508						/**
509						 * Filter for apply coupon notice
510						 * Refreshes coupon list when coupon is applied in blocks
511						 *
512						 * @param {boolean} defaultValue - Whether to show notice
513						 * @return {boolean} The default value (pass through)
514						 */
515						showApplyCouponNotice( defaultValue ) {
516							PowerCoupons.scheduleCouponsRefresh();
517							return defaultValue;
518						},
519
520						/**
521						 * Filter for remove coupon notice
522						 * Refreshes coupon list when coupon is removed in blocks
523						 *
524						 * @param {boolean} defaultValue - Whether to show notice
525						 * @return {boolean} The default value (pass through)
526						 */
527						showRemoveCouponNotice( defaultValue ) {
528							PowerCoupons.scheduleCouponsRefresh();
529							return defaultValue;
530						},
531					}
532				);
533			}
534		},
535
536		/**
537		 * Bind click events to coupon action buttons
538		 *
539		 * Uses event delegation for better performance and to handle
540		 * dynamically added elements (after AJAX refresh).
541		 *
542		 * @since 1.0.0
543		 */
544		bindEvents() {
545			const self = this;
546
547			// Apply coupon button click
548			// Simpler: clicking a coupon card applies it (unless user clicks a button in it)
549			$( document ).on(
550				'click',
551				'.power-coupons-list .power-coupon-card',
552				function ( e ) {
553					if (
554						! $( e.target ).closest(
555							'.apply-coupon, .remove-coupon'
556						).length
557					) {
558						$( this )
559							.find( '.apply-coupon' )
560							.first()
561							.trigger( 'click' );
562					}
563				}
564			);
565
566			$( document ).on(
567				'click',
568				'.power-coupons-apply-coupon-btn',
569				function ( e ) {
570					self.applyCoupon.call( this, e, self );
571				}
572			);
573
574			// Remove coupon button click
575			$( document ).on( 'click', '.remove-coupon', function ( e ) {
576				self.removeCoupon.call( this, e, self );
577			} );
578		},
579
580		/**
581		 * Apply coupon to cart
582		 *
583		 * Sends AJAX request to apply coupon and updates UI accordingly.
584		 *
585		 * @since 1.0.0
586		 * @param {Event}  e    - Click event object
587		 * @param {Object} self - Reference to PowerCoupons object
588		 */
589		applyCoupon( e, self ) {
590			e.preventDefault();
591
592			const $button = $( this );
593			const couponCode = $button.data( 'coupon' );
594
595			// Validate coupon code
596			if ( ! couponCode ) {
597				console.error( 'Power Coupons: No coupon code found' );
598				return;
599			}
600
601			// Prevent duplicate requests
602			if ( self.activeRequests.apply ) {
603				return;
604			}
605
606			const originalText = $button.html();
607
608			// Update button state - show loading
609			$button
610				.prop( 'disabled', true )
611				.attr( 'aria-disabled', 'true' )
612				.attr( 'aria-busy', 'true' )
613				.addClass( 'pc-loading' )
614				.find( '.power-coupons-coupon-status' )
615				.text( powerCouponsData.text?.applyingText || 'Applying...' );
616
617			// Send AJAX request
618			self.activeRequests.apply = $.ajax( {
619				type: 'POST',
620				url: powerCouponsData.ajaxUrl.applyCoupon,
621				timeout: PowerCoupons.requestTimeout,
622				data: {
623					coupon_code: couponCode,
624					nonce: powerCouponsData.nonce,
625					billing_email: $( 'input[name="billing_email"]' ).val(),
626				},
627				success( response ) {
628					if ( response.success ) {
629						// Sync cart totals in place, then re-render the
630						// coupon cards against the fresh cart.
631						self.syncCartUi().then( function () {
632							PowerCoupons.ajaxRefreshCouponsHTML();
633						} );
634					} else {
635						// Failed - show error message
636						const errorMessage =
637							response.data?.message ||
638							powerCouponsData.text?.applyErrorText ||
639							'Failed to apply coupon.';
640
641						self.showNotice( errorMessage, 'error' );
642
643						// Refresh coupon list after a short delay
644						setTimeout( function () {
645							PowerCoupons.ajaxRefreshCouponsHTML();
646						}, 500 );
647					}
648				},
649				error( jqXHR, textStatus ) {
650					// Network error or server error
651					if ( textStatus !== 'abort' ) {
652						const errorMessage =
653							powerCouponsData.text?.applyErrorText ||
654							'Failed to apply coupon. Please try again.';
655
656						self.showNotice( errorMessage, 'error' );
657
658						// Restore button state
659						$button
660							.prop( 'disabled', false )
661							.attr( 'aria-disabled', 'false' )
662							.attr( 'aria-busy', 'false' )
663							.removeClass( 'pc-loading' )
664							.text( originalText );
665					}
666				},
667				complete() {
668					// Clear request reference
669					self.activeRequests.apply = null;
670				},
671			} );
672		},
673
674		/**
675		 * Remove coupon from cart
676		 *
677		 * Sends AJAX request to remove coupon and updates UI accordingly.
678		 *
679		 * @since 1.0.0
680		 * @param {Event}  e    - Click event object
681		 * @param {Object} self - Reference to PowerCoupons object
682		 */
683		removeCoupon( e, self ) {
684			e.preventDefault();
685
686			const $button = $( this );
687			const couponCode = $button.data( 'coupon' );
688
689			// Validate coupon code
690			if ( ! couponCode ) {
691				console.error( 'Power Coupons: No coupon code found' );
692				return;
693			}
694
695			// Prevent duplicate requests
696			if ( self.activeRequests.remove ) {
697				return;
698			}
699
700			const originalText = $button.text();
701
702			// Update button state - show loading
703			$button
704				.prop( 'disabled', true )
705				.attr( 'aria-disabled', 'true' )
706				.attr( 'aria-busy', 'true' )
707				.addClass( 'pc-loading' )
708				.text( powerCouponsData.text?.removingText || 'Removing...' );
709
710			// Send AJAX request
711			self.activeRequests.remove = $.ajax( {
712				type: 'POST',
713				url: powerCouponsData.ajaxUrl.removeCoupon,
714				timeout: PowerCoupons.requestTimeout,
715				data: {
716					coupon_code: couponCode,
717					nonce: powerCouponsData.nonce,
718				},
719				success( response ) {
720					if ( response.success ) {
721						// Sync cart totals in place, then re-render the
722						// coupon cards against the fresh cart.
723						self.syncCartUi().then( function () {
724							PowerCoupons.ajaxRefreshCouponsHTML();
725						} );
726					} else {
727						// Failed - show error message
728						const errorMessage =
729							response.data?.message ||
730							powerCouponsData.text?.removeErrorText ||
731							'Failed to remove coupon.';
732
733						self.showNotice( errorMessage, 'error' );
734
735						// Restore button state
736						$button
737							.prop( 'disabled', false )
738							.attr( 'aria-disabled', 'false' )
739							.attr( 'aria-busy', 'false' )
740							.removeClass( 'pc-loading' )
741							.text( originalText );
742					}
743				},
744				error( jqXHR, textStatus ) {
745					// Network error or server error
746					if ( textStatus !== 'abort' ) {
747						const errorMessage =
748							powerCouponsData.text?.removeErrorText ||
749							'Failed to remove coupon. Please try again.';
750
751						self.showNotice( errorMessage, 'error' );
752
753						// Restore button state
754						$button
755							.prop( 'disabled', false )
756							.attr( 'aria-disabled', 'false' )
757							.attr( 'aria-busy', 'false' )
758							.removeClass( 'pc-loading' )
759							.text( originalText );
760					}
761				},
762				complete() {
763					// Clear request reference
764					self.activeRequests.remove = null;
765				},
766			} );
767		},
768
769		/**
770		 * Show notice message to user
771		 *
772		 * Attempts to use WooCommerce notice system if available,
773		 * falls back to browser alert as last resort.
774		 *
775		 * @since 1.0.0
776		 * @param {string} message - Message to display
777		 * @param {string} type    - Notice type: 'success', 'error', 'notice'
778		 */
779		showNotice( message, type ) {
780			type = type || 'notice';
781
782			// Use role="status" (polite) for success, role="alert" (assertive) for errors.
783			const noticeRole = type === 'error' ? 'alert' : 'status';
784			const ariaLive = type === 'error' ? 'assertive' : 'polite';
785
786			// Try to use WooCommerce notices if available
787			const noticeContainer = $( '.woocommerce-notices-wrapper' ).first();
788
789			if ( noticeContainer.length ) {
790				// Create WooCommerce-style notice
791				const noticeClass =
792					type === 'error'
793						? 'woocommerce-error'
794						: 'woocommerce-message';
795				const notice = $( '<div>' )
796					.addClass( noticeClass )
797					.attr( 'role', noticeRole )
798					.attr( 'aria-live', ariaLive )
799					.attr( 'aria-atomic', 'true' )
800					.text( message );
801
802				// Clear existing notices and add new one
803				noticeContainer.empty().append( notice );
804
805				// Scroll to notice
806				$( 'html, body' ).animate(
807					{
808						scrollTop: noticeContainer.offset().top - 100,
809					},
810					500
811				);
812
813				// Auto-dismiss success notices after 10 seconds (increased from 5 for accessibility)
814				if ( type === 'success' ) {
815					setTimeout( function () {
816						notice.fadeOut( function () {
817							$( this ).remove();
818						} );
819					}, 10000 );
820				}
821			} else {
822				// Fallback: create a temporary inline notice instead of disruptive alert().
823				const fallbackNotice = $( '<div>' )
824					.addClass( 'power-coupons-inline-notice' )
825					.attr( 'role', noticeRole )
826					.attr( 'aria-atomic', 'true' )
827					.text( message )
828					.css( {
829						position: 'fixed',
830						top: '20px',
831						right: '20px',
832						zIndex: 999999,
833						padding: '12px 20px',
834						borderRadius: '4px',
835						maxWidth: '400px',
836						background: type === 'error' ? '#fee2e2' : '#f0fdf4',
837						color: type === 'error' ? '#991b1b' : '#166534',
838						border:
839							'1px solid ' +
840							( type === 'error' ? '#fecaca' : '#86efac' ),
841						boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
842						fontSize: '14px',
843						lineHeight: '1.5',
844					} );
845
846				$( 'body' ).append( fallbackNotice );
847
848				setTimeout( function () {
849					fallbackNotice.fadeOut( function () {
850						$( this ).remove();
851					} );
852				}, 5000 );
853			}
854		},
855	};
856
857	/**
858	 * Initialize on DOM ready
859	 *
860	 * Waits for jQuery and DOM to be ready before initializing.
861	 */
862	$( document ).ready( function () {
863		PowerCoupons.init();
864	} );
865} )( jQuery );

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.