PageSourceSearch

https://www.shipstation.com/wp-content/themes/shipstation-blocks/js/register.js?v=1790232222

js shipstation.com collected 2026-09-24 06:52:00 UTC 59,880 bytes, 1,618 lines download raw bytes

1let _registerEndpoint = "https://ss2.shipstation.com/api/account/ProxyRegistration";
2
3let step2Redirect = "https://ship.shipstation.com/login";
4let _v2Register = false;
5
6let saveButtonText;
7let recaptchaTokenGlobal = '';
8const optinConsentCountries = ['GB', "FR", "DE"];
9
10/**
11 * Execute reCAPTCHA verification and handle form submission
12 * DRY helper to avoid duplicating reCAPTCHA logic
13 * @param {jQuery} form - The registration form
14 * @returns {boolean} True if reCAPTCHA execution started, false if error shown
15 */
16function executeRecaptchaAndSubmit(form) {
17	const $saveButton = $("#save-button");
18
19	// SECURITY: After RecaptchaLazyLoader.ensure() resolves, grecaptcha MUST be available
20	// If it's not, this is an error state - DO NOT submit without verification
21	if (!window.grecaptcha) {
22		console.error('[Registration] grecaptcha not available after ensure() - blocking submission');
23
24		// SECURITY: Log this unexpected state
25		if (window.dataLayer) {
26			window.dataLayer.push({
27				event: 'registration_no_recaptcha',
28				recaptcha_status: 'unavailable_after_load'
29			});
30		}
31
32		// Re-enable button and show error
33		$saveButton
34			.prop('disabled', false)
35			.css('background-color', '')
36			.val(saveButtonText || 'Try Again');
37
38		alert(
39			'Security verification is required but unavailable. ' +
40			'Please check your internet connection and ad-blocker settings, then try again.'
41		);
42		return false;
43	}
44
45	// SECURITY: RecaptchaLazyLoader should always exist if grecaptcha does
46	// This path should be impossible - block submission if it occurs
47	if (!window.RecaptchaLazyLoader || !window.RecaptchaLazyLoader.siteKey) {
48		console.error('[Registration] RecaptchaLazyLoader not available - invalid state');
49
50		// SECURITY: Log this unexpected state
51		if (window.dataLayer) {
52			window.dataLayer.push({
53				event: 'registration_invalid_state',
54				reason: 'grecaptcha_without_loader'
55			});
56		}
57
58		// Re-enable button and show error
59		$saveButton
60			.prop('disabled', false)
61			.css('background-color', '')
62			.val(saveButtonText || 'Try Again');
63
64		alert(
65			'A technical error occurred during verification. ' +
66			'Please refresh the page and try again.'
67		);
68		return false;
69	}
70
71	const siteKey = window.RecaptchaLazyLoader.siteKey;
72
73	grecaptcha.ready(function() {
74		grecaptcha.execute(siteKey, {action: 'submit'})
75			.then(function(token) {
76				const data = {
77					countryCode: getHomeCountry(),
78				};
79
80				if(token) {
81					recaptchaTokenGlobal = token;
82				}
83
84				const supportedCountries = [
85					"US",
86					"GB",
87					"AU",
88					"CA",
89					"FR",
90					"NZ",
91					"DE",
92				];
93
94				// If unsupported country fails recaptcha, send to step2 without taking any other action -- otherwise, submit
95				if (supportedCountries.includes(data.countryCode)) {
96					data.success = true;
97					logCaptchaToTray(data, () => {
98						submitRegistrationForm(form)
99					});
100				} else {
101					data.success = false;
102					logCaptchaToTray(data, () => {
103						window.location.replace(step2Redirect);
104					});
105				}
106			})
107			.catch(function(error) {
108				// Handle grecaptcha.execute() failure (network timeout, service error, etc.)
109				console.error('[reCAPTCHA] Token generation failed:', error);
110
111				// SECURITY: Log token generation failure
112				if (window.dataLayer) {
113					window.dataLayer.push({
114						event: 'registration_recaptcha_execute_failed',
115						error: error.message || 'unknown'
116					});
117				}
118
119				// Re-enable button and show error to user
120				const $saveButton = $("#save-button");
121				$saveButton
122					.prop('disabled', false)
123					.css('background-color', '')
124					.val(saveButtonText || 'Try Again');
125
126				// Show user-facing error
127				alert(
128					'We were unable to verify your submission. ' +
129					'Please check your internet connection and try again. ' +
130					'If the problem persists, contact support.'
131				);
132			});
133	});
134}
135
136$(document).ready(function () {
137  // Update HDYHAU validation rules based on requirement flag
138  function updateHDYHAUValidation(shouldRequire) {
139    window.requireHowDidYouHearAboutUs = shouldRequire;
140    
141    // Update validation rules if form validator exists
142    const validator = $("#registration-form").data('validator');
143    
144    if (validator) {
145      $("#howdidyouhearaboutus").rules("remove");
146      $("#howdidyouhearaboutus").rules("add", {
147        required: shouldRequire,
148        notDefault: shouldRequire
149      });
150      
151      // Clear any existing errors if field is no longer required
152      if (!shouldRequire) {
153        $("#howdidyouhearaboutus").removeClass('error');
154        $("#howdidyouhearaboutus").closest('.row').find('label.error').remove();
155      }
156    } else {
157      // Retry if validator not ready yet
158      setTimeout(function() {
159        updateHDYHAUValidation(shouldRequire);
160      }, 100);
161    }
162  }
163  
164  // Check cookie on page load. ss_get_cookie returns null (not undefined) when the
165  // cookie is absent, so compare against null — otherwise this block would run on
166  // every page load and call updateHDYHAUValidation(false) when no cookie is set.
167  const cookieValue = ss_get_cookie('require_hdyhau');
168  if (cookieValue !== null) {
169    const shouldRequire = cookieValue === 'true' || cookieValue === '1';
170    updateHDYHAUValidation(shouldRequire);
171  }
172  
173  // Listen for vanilla JS custom event to update HDYHAU requirement
174  document.addEventListener('hdyhau:requirementChanged', function(event) {
175    updateHDYHAUValidation(event.detail);
176  });
177
178  // If we aren't in a production environment, use the staging endpoints.
179  if ( ! isProdEnvironment() ) {
180    let ddeTestingField = getFieldFromQuery("dde");
181  
182    // Default endpoints for non-staging, non-empty ddeTestingField
183    if (ddeTestingField && ddeTestingField !== "staging") {
184      _registerEndpoint = "https://ss0.shipstation.com/api/account/register";
185      step2Redirect = `https://ship-devss${ddeTestingField}.sslocal.com/login`;
186  
187      // Replace endpoints to match the DDE testing field
188      _registerEndpoint = _registerEndpoint.replace("ss0.shipstation.com", `ss-devss${ddeTestingField}.sslocal.com`);
189
190    } else if (ddeTestingField === "staging" || ddeTestingField === "" || ddeTestingField === null) {
191      // Specific handling for staging or if the ddeTestingField is empty
192      _registerEndpoint = "https://ss0.shipstation.com/api/account/register"; // Assuming this is also the staging endpoint
193      step2Redirect = "https://ship0.shipstation.com/login"; // Adjust if there's a specific staging URL
194    }
195  
196    console.table('Non-Prod URLs', { _registerEndpoint, step2Redirect });
197  } else {
198    // Production environment URLs.
199    console.table('Prod URLs', { _registerEndpoint, step2Redirect });
200  }
201
202  // Writes to hidden tracking fields and binds the country-change handler -- all against
203  // elements INSIDE #signup_form, which Vue rebuilds on mount. Grouped into a function so
204  // it can be deferred to run post-mount via ssOnSignupFormReady (see the end of ready()).
205  // Before this, when the deferred $(document).ready won the race against Vue's mount, these
206  // writes hit the doomed pre-mount form and were silently lost -- e.g. #affiliate/#referrer
207  // (read at submit) would submit empty, and the country handler never bound.
208  function bindSignupFormFields() {
209  // Check if input #experiments exist
210  if ($("input#experiments").length) {
211    // Function waits for a global variable object
212    const waitForGlobalObject = function (key, callback) {
213      if (!jQuery.isEmptyObject(window[key])) {
214        callback();
215      } else {
216        setTimeout(function () {
217          waitForGlobalObject(key, callback);
218        }, 100);
219      }
220    };
221
222    // Call waitForGlobalObject and run code after callback
223    waitForGlobalObject("gaData", function () {
224      $("input#experiments").val(
225        JSON.stringify(gaData["UA-25912405-1"].experiments)
226      );
227    });
228  }
229
230  if ($("select#country").length) {
231    register_country_handler();
232    $("select#country").change(function () {
233      register_country_handler();
234    });
235  }
236
237  $("#utcoffset").val(new Date().getTimezoneOffset() * -1);
238  if (ss_get_cookie("coupon-s1")) {
239    var couponContent = ss_get_cookie("coupon-s1");
240    $("#coupon-div").show();
241    $("#coupon-div").html(couponContent);
242    if ($("#reg-copy").length) {
243      $("#reg-copy").hide();
244    }
245  }
246  $("#affiliate").val(ss_get_cookie("affiliate"));
247  $("#referrer").val(ss_get_cookie("referrer"));
248  $("#referral_code").val(ss_get_cookie("referral_code"));
249  $("#token_payload").val(ss_get_cookie("ssbc-payload"));
250
251  // Capture YOFI journey_id from cookie for fraud detection
252  // This is set by the YOFI telemetry script in shipstation-yofi-fraud-detection.php
253  captureJourneyId();
254
255  if (
256    ($("#referral_code").val() || "").toLowerCase() === "amazonsellercentral"
257  ) {
258    $("#plan_profile_code").val("ss_starter3");
259  } else if (($("#referral_code").val() || "").toLowerCase() === "sqsp") {
260    $("#plan_profile_code").val("ss_starter3");
261  } else if (
262    ($("#referral_code").val() || "").search(/AUS-Carrier-Sendle/i) != -1
263  ) {
264    $("#plan_profile_code").val("free_starter3");
265  }
266  if (($("#affiliate").val() || "").toLowerCase() === "square") {
267    $("#plan_profile_code").val("square3");
268  }
269
270    // Set variant query param for onboarding testing
271    const queryParams = new URLSearchParams(window.location.search);
272    const variantParam = queryParams.get("variant");
273
274    if (variantParam) {
275      $("#onboardingVariant").val(variantParam);
276    }
277  }
278
279  // AI Communications Opt-In field logic — ClickUp 86e29h8y6
280  // The AI consent experiment (86e1y22ba) concluded as a WIN, so the behaviour now
281  // ships to everyone and we no longer listen for the Optimizely variant. Consent is
282  // implied by the presence of a phone number: if the phone field has a value the
283  // user has consented to AI communications, otherwise they have not.
284  window.updateAICommsOptInAtSubmission = function() {
285    const phoneValue = $("input[name=phone]").val();
286    const hasPhone = phoneValue && phoneValue.trim().length > 0;
287
288    $("#AI_comms_opt_in").val(hasPhone ? "Consented" : "Not Consented");
289  };
290
291  // Target app (ShipStation Lite) opt-in logic — ClickUp 86e27tym2
292  // The Optimizely experiment concluded as a WIN for the Lite variation, so it now
293  // ships to everyone eligible: the hidden `targetApp` field is "lite" for a low
294  // monthly shipment volume, "default" otherwise. No Optimizely bucketing involved.
295
296  // Shipment volume values that count as "50 shipments or less per month"
297  // ('1' = single package, '50' = 50 or less). See forms/shipment-volume.php.
298  const TARGET_APP_ELIGIBLE_VOLUMES = ['1', '50'];
299
300  // targetApp lite volume expansion — live Optimizely experiment.
301  // '500' = the "51-500 shipments" option (see forms/shipment-volume.php /
302  // forms/translations.php). Unlike TARGET_APP_ELIGIBLE_VOLUMES above, this tier
303  // only gets "lite" while the experiment is running and the visitor is bucketed
304  // into the reveal variant — not geo-gated, per the experiment spec.
305  const TARGET_APP_LITE_VOLUME_EXPERIMENT_VOLUME = '500';
306  const TARGET_APP_LITE_VOLUME_EXPERIMENT_ID = '5774645624832000';
307  const TARGET_APP_LITE_VOLUME_VARIANT_ID = '6377530184171520';
308
309  function isInTargetAppLiteVolumeVariant() {
310    try {
311      if (window.optimizely && typeof window.optimizely.get === 'function') {
312        const state = window.optimizely.get("state");
313        if (state) {
314          const experimentStates = state.getExperimentStates() || {};
315          const experimentState = experimentStates[TARGET_APP_LITE_VOLUME_EXPERIMENT_ID];
316
317          if (experimentState && experimentState.variation && experimentState.variation.id) {
318            return experimentState.variation.id.toString() === TARGET_APP_LITE_VOLUME_VARIANT_ID;
319          }
320        }
321      }
322    } catch (e) {
323      console.error('Error checking targetApp lite volume variant:', e);
324    }
325
326    return false;
327  }
328
329  // Set the hidden targetApp field value at submission time.
330  // "lite" = low-volume shipper, "default" = standard onboarding.
331  window.updateTargetAppAtSubmission = function() {
332    const shipmentVolume = $("#shipment_volume").length ? $("#shipment_volume").val() : "";
333    const isLite =
334      (TARGET_APP_ELIGIBLE_VOLUMES.includes(shipmentVolume) && typeof window.ssIsUsGeo === 'function' && window.ssIsUsGeo()) ||
335      (shipmentVolume === TARGET_APP_LITE_VOLUME_EXPERIMENT_VOLUME && isInTargetAppLiteVolumeVariant());
336
337    $("#targetApp").val(isLite ? "lite" : "default");
338  };
339
340  // ships_freight LTL field — ClickUp 86e2v839r. Toggled from
341  // register_country_handler() below (US-only, and only when the page opted in
342  // via show_ships_freight) rather than an Optimizely bucketing check.
343  // isShipsFreightRevealed() (single source of truth for "is it actually shown
344  // right now") is defined at top level below since register_country_handler()
345  // is also top level and can't see a function nested inside this closure.
346
347  $.validator.addMethod(
348    "country",
349    function (value, element) {
350      return (
351        value == "US" ||
352        value == "CA" ||
353        value == "AU" ||
354        value == "GB" ||
355        value == "FR" ||
356        value == "DE" ||
357        value == "NZ"
358      );
359    },
360    "ShipStation only supports shippers in the United States, Canada, France, Australia and United Kingdom at this time. More countries coming soon."
361  );
362
363	if (typeof emailBlacklistEnabled !== 'undefined' && emailBlacklistEnabled && typeof emailBlacklistDomains !== 'undefined') {
364		$.validator.addMethod("emailBlacklist", function(value, element) {
365			if (!value) return true;
366			var domain = value.split('@')[1];
367			if (!domain) return true;
368			domain = domain.toLowerCase();
369			return !emailBlacklistDomains.some(function(pattern) {
370				pattern = pattern.toLowerCase();
371				if (pattern.endsWith('.*')) {
372					var base = pattern.slice(0, -2);
373					return domain.startsWith(base + '.');
374				}
375				return domain === pattern;
376			});
377		}, typeof emailBlacklistMessage !== 'undefined' ? emailBlacklistMessage : "Please enter a valid business email address");
378	}
379
380  _v2Register = true;
381
382  const messages = {
383    FR: {
384      "password-confirm": {
385        equalTo: "Les mots de passe ne sont pas identiques",
386        required: "Merci de confirmer votre mot de passe",
387      },
388      "email-confirm": {
389        equalTo: "Les adresses email ne sont pas identiques",
390        required: "Merci de confirmer votre email",
391      },
392      email: {
393        required: "Merci de confirmer votre email",
394      },
395      password: {
396        required: "Ce champ est requis",
397        minlength: "Le mot de passe doit contenir au moins 12 caractères",
398        passwordComplexity: "Votre mot de passe doit contenir au moins 12 caractères et inclure au moins une lettre majuscule, une lettre minuscule et un caractère spécial. Il ne doit pas contenir de caractères répétitifs ou séquentiels",
399      },
400      firstName: {
401        required: "Le prénom est obligatoire",
402      },
403      lastName: {
404        required: "Le nom est obligatoire",
405      },
406      "answers[0]": {
407        required:
408          "Merci de compléter la question Comment avez-vous entendu parler de nous ?",
409      },
410      tos: {
411        required: "Veuillez accepter les Conditions d’Utilisation",
412      },
413      countryCode: {
414        country:
415          "ShipStation permet d’expédier uniquement à partir des États-Unis, du Canada, d’Australie, de France et du Royaume-Uni pour l’envoi des commandes. D’autres pays sont à venir prochainement.",
416      },
417      shipment_volume: {
418        required: "Ce champ est requis",
419      },
420      companyName: {
421        required: "Le nom de l’entreprise est obligatoire",
422      },
423      vat_number: {
424        required: "Ce champ est requis",
425        vat_pattern: 'Veuillez insérer un numéro de TVA valide'
426      }
427    },
428    DE: {
429      "password-confirm": {
430        equalTo: "Passwords do not match",
431        required: "Please confirm your password",
432      },
433      email: {
434        required: "Dieses Feld ist erforderlich.",
435        email: "Bitte geben Sie eine gültige E-Mail-Adresse ein."
436      },
437      "email-confirm": {
438        equalTo: "E-mail Addresses do not match",
439        required: "Please confirm your e-mail",
440      },
441      password: {
442        required: "Ein Passwort ist erforderlich",
443        minlength: "Passwörter müssen mindestens 12 Zeichen lang sein",
444        passwordComplexity: "Ihr Passwort muss mindestens 12 Zeichen lang sein und mindestens einen Großbuchstaben, einen Kleinbuchstaben und ein Sonderzeichen enthalten. Es darf keine sich wiederholenden oder aufeinanderfolgenden Zeichen enthalten",
445      },
446      firstName: {
447        required: "Vorname ist erforderlich",
448      },
449      lastName: {
450        required: "Nachname ist erforderlich",
451      },
452      "answers[0]": {
453        required: "Bitte antworten Sie Wie haben Sie von uns erfahren?",
454      },
455      tos: {
456        required: "Please agree to the Terms of Service",
457      },
458      countryCode: {
459        country:
460          "ShipStation unterstützt derzeit nur Versender in den Vereinigten Staaten, Kanada, Frankreich, Australien, Deutschland und dem Vereinigten Königreich. Weitere Länder folgen in Kürze.",
461      },
462      companyName: {
463        required: "Company Name is required",
464      },
465      vat_number: {
466        required: "Dieses Feld ist erforderlich.",
467        vat_pattern: 'Bitte gib eine gültige Ust.-Id.-Nr. ein'
468      }
469    },
470    US: {
471      "password-confirm": {
472        equalTo: "Passwords do not match",
473        required: "Please confirm your password",
474      },
475      email: {
476        required: "Email is required",
477      },
478      "email-confirm": {
479        equalTo: "E-mail Addresses do not match",
480        required: "Please confirm your e-mail",
481      },
482      password: {
483        required: "A password is required",
484        minlength: "Passwords must be at least 12 characters",
485        passwordComplexity: "Your password must be at least 12 characters long and contain at least one uppercase letter, one lowercase letter, and one special character. It must not contain repetitive or sequential characters",
486      },
487      firstName: {
488        required: "First Name is required",
489      },
490      lastName: {
491        required: "Last Name is required",
492      },
493      "answers[0]": {
494        required: "Please answer How did you hear about us?",
495      },
496      tos: {
497        required: "Please agree to the Terms of Service",
498      },
499      countryCode: {
500        country:
501          "ShipStation only supports shippers in the United States, Canada, France, Australia and United Kingdom at this time. More countries coming soon.",
502      },
503      companyName: {
504        required: "Company Name is required",
505      },
506      phone: {
507        required: "Phone Number is required",
508      },
509      vat_number: {
510        required: "This field is required",
511      }
512    },
513    GB: {
514      "password-confirm": {
515        equalTo: "Passwords do not match",
516        required: "Please confirm your password",
517      },
518      "email-confirm": {
519        equalTo: "E-mail Addresses do not match",
520        required: "Please confirm your e-mail",
521      },
522      email: {
523        required: "Email is required",
524      },
525      password: {
526        required: "A password is required",
527        minlength: "Passwords must be at least 12 characters",
528        passwordComplexity: "Your password must be at least 12 characters long and contain at least one uppercase letter, one lowercase letter, and one special character. It must not contain repetitive or sequential characters",
529      },
530      firstName: {
531        required: "First Name is required",
532      },
533      lastName: {
534        required: "Last Name is required",
535      },
536      "answers[0]": {
537        required: "Please answer How did you hear about us?",
538      },
539      tos: {
540        required: "Please agree to the Terms of Service",
541      },
542      countryCode: {
543        country:
544          "ShipStation only supports shippers in the United States, Canada, France, Australia and United Kingdom at this time. More countries coming soon.",
545      },
546      companyName: {
547        required: "Company Name is required",
548      },
549      phone: {
550        required: "Phone Number is required",
551      },
552      vat_number: {
553        required: "This field is required",
554        vat_pattern: 'Please enter a valid VAT number'
555      }
556    },
557    AU: {
558      "password-confirm": {
559        equalTo: "Passwords do not match",
560        required: "Please confirm your password",
561      },
562      "email-confirm": {
563        equalTo: "E-mail Addresses do not match",
564        required: "Please confirm your e-mail",
565      },
566      password: {
567        required: "A password is required",
568        minlength: "Passwords must be at least 12 characters",
569        passwordComplexity: "Your password must be at least 12 characters long and contain at least one uppercase letter, one lowercase letter, and one special character. It must not contain repetitive or sequential characters",
570      },
571      firstName: {
572        required: "First Name is required",
573      },
574      lastName: {
575        required: "Last Name is required",
576      },
577      "answers[0]": {
578        required: "Please answer How did you hear about us?",
579      },
580      tos: {
581        required: "Please agree to the Terms of Service",
582      },
583      countryCode: {
584        country:
585          "ShipStation only supports shippers in the United States, Canada, France, Australia and United Kingdom at this time. More countries coming soon.",
586      },
587      companyName: {
588        required: "Company Name is required",
589      },
590      phone: {
591        required: "Phone Number is required",
592      },
593      vat_number: {
594        required: "This field is required",
595        vat_pattern: 'Please enter a valid ABN'
596      }
597    },
598  };
599
600  // jQuery Validate binds its delegated focusin/focusout/keyup handlers -- and stores its
601  // validator via $.data() -- on the #registration-form element, which Vue rebuilds on
602  // mount (see signup.php mounted()). A validator bound before that mount dies with the old
603  // element, leaving the live form with no validation -- the "validation only works half the
604  // time" bug. Grouped into a function so it runs post-mount via ssOnSignupFormReady (see the
605  // end of ready()). Re-calling .validate() on the same element is a no-op (returns the
606  // cached validator), so it's safe even if invoked more than once.
607  function ssSetupRegistrationValidation() {
608  $("#registration-form").validate({
609    rules: {
610      password: {
611        required: true,
612        minlength: 12,
613        passwordComplexity: true
614      },
615      "password-confirm": {
616        required: true,
617        equalTo: "#password"
618      },
619      "email-confirm": {
620        required: true,
621        equalTo: "#email"
622      },
623      countryCode: {
624        required: true,
625        country: "US"
626      },
627      "answers[0]": {
628        required: window.requireHowDidYouHearAboutUs || false,
629        notDefault: window.requireHowDidYouHearAboutUs || false,
630      },
631      companyName: {
632        required: function() {
633          return ["FR", "GB"].includes(getHomeCountry());
634        }
635      },
636      phone: {
637        required: function() {
638          return getHomeCountry() === "GB";
639        },
640        phoneNumber: true
641      },
642      ships_freight: {
643        required: isShipsFreightRevealed,
644        // NOT the shared `notDefault` method below — it only rejects "" / null,
645        // and this field's own resting value ("null", its literal placeholder
646        // string) is neither, so it would pass right through. See
647        // shipsFreightNotDefault's own definition for why the gating lives
648        // inside the method rather than in this `true`.
649        shipsFreightNotDefault: true
650      }
651    },
652    messages: messages[getHomeCountry()] || messages["US"],
653    errorPlacement: function(error, element) {
654      const name = element.attr("name");
655
656      if (name === "firstName") {
657          error.attr('id', 'reg-fname-error').attr('for', 'reg-fname');
658          error.appendTo(element.closest('.error-container'));
659      } else if (name === "lastName") {
660          error.attr('id', 'reg-lname-error').attr('for', 'reg-lname');
661          error.appendTo(element.closest('.error-container'));
662      } else if (["tos", "countryCode"].includes(name)) {
663          error.appendTo(element.closest("div"));
664      } else {
665          error.insertAfter(element);
666      }
667    },
668    submitHandler: function (form) {
669      saveButtonText = $("#save-button").val();
670
671      // Set AI_comms_opt_in value at submission time to avoid race conditions
672      if (typeof window.updateAICommsOptInAtSubmission === 'function') {
673        window.updateAICommsOptInAtSubmission();
674      }
675
676      // Set targetApp (ShipStation Lite) value at submission time to avoid race
677      // conditions with Optimizely experiment activation
678      if (typeof window.updateTargetAppAtSubmission === 'function') {
679        window.updateTargetAppAtSubmission();
680      }
681
682      const $saveButton = $("#save-button");
683      $saveButton
684      .attr("disabled", "disabled")
685      .css("background-color", "#ddd");
686
687      // Show appropriate loading message based on reCAPTCHA state
688      if (window.RecaptchaLazyLoader && !RecaptchaLazyLoader.isLoaded()) {
689        $saveButton.val("Verifying...");
690      } else {
691        $saveButton.val("One Moment...");
692      }
693
694      // Ensure reCAPTCHA is loaded before executing (lazy-loaded for performance)
695      if (window.RecaptchaLazyLoader) {
696        RecaptchaLazyLoader.ensure()
697          .then(function() {
698            // reCAPTCHA is now loaded and ready
699            executeRecaptchaAndSubmit(form);
700          })
701          .catch(function(error) {
702            console.error('[reCAPTCHA] Load error:', error);
703
704            // SECURITY: Log load failure for monitoring
705            if (window.dataLayer) {
706              window.dataLayer.push({
707                event: 'registration_recaptcha_load_failed',
708                error: error.message || 'unknown'
709              });
710            }
711
712            // SECURITY: DO NOT submit without verification
713            // Block submission and show user-facing error
714            $saveButton
715              .prop('disabled', false)
716              .css('background-color', '')
717              .val(saveButtonText || 'Try Again');
718
719            alert(
720              'Security verification failed to load. ' +
721              'This may be caused by ad-blockers or firewall settings. ' +
722              'Please disable ad-blockers for this site, check your internet connection, and try again.'
723            );
724          });
725      } else {
726        // RecaptchaLazyLoader not available - this should never happen
727        console.error('[Registration] RecaptchaLazyLoader not loaded - invalid state');
728
729        if (window.dataLayer) {
730          window.dataLayer.push({
731            event: 'registration_loader_missing'
732          });
733        }
734
735        $saveButton
736          .prop('disabled', false)
737          .css('background-color', '')
738          .val(saveButtonText || 'Try Again');
739
740        alert(
741          'A technical error occurred. ' +
742          'Please refresh the page and try again.'
743        );
744      } 
745    }
746   });
747
748    // Custom validation methods
749    $.validator.addMethod("notDefault", function(value, element) {
750        return value !== "" && value !== null;
751    }, "Please select an option");
752
753    /*
754     * Deliberately NOT reusing notDefault above for ships_freight: it only
755     * rejects "" / null, and this field's own resting value is the literal
756     * STRING "null" (its placeholder option's value, chosen specifically so an
757     * untouched hidden field submits the "not applicable" sentinel) — which is
758     * neither of those, so notDefault would pass it right through. The
759     * revealed-or-not check lives INSIDE this method (rather than as jQuery
760     * Validate's usual function-valued rule parameter, e.g. companyName's
761     * `required: function(){...}` above) so there's no ambiguity about whether
762     * that mechanism applies to a non-required custom method the same way.
763     */
764    $.validator.addMethod("shipsFreightNotDefault", function(value) {
765        if (!isShipsFreightRevealed()) {
766            return true; // not shown right now — "null" is the correct value
767        }
768        return value !== "null";
769    }, "Please select an option");
770
771    $('select[name="answers[0]"]').on('change', function() {
772        if ($(this).val() !== '') {
773            // Remove error class and message
774            $(this).removeClass('error');
775            $('#howdidyouhearaboutus-error').remove();  // Remove existing error message
776            // Revalidate the field
777            $(this).valid();
778        }
779    });
780
781    $.validator.addMethod("passwordComplexity", function(value) {
782      const password = value.toLowerCase();
783      const email = $("#email").val().toLowerCase();
784      let errors = [];
785
786      if (email && password.includes(email)) {
787        errors.push("Password cannot contain your email address");
788      }
789
790      if (!/(?=.*[A-Z])/.test(value)) {
791        errors.push("Must contain uppercase letter");
792      }
793
794      if (!/(?=.*[a-z])/.test(value)) {
795        errors.push("Must contain lowercase letter");
796      }
797
798      if (!/(?=.*\d)/.test(value)) {
799        errors.push("Must contain a number");
800      }
801
802      if (!/(?=.*[ !"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/.test(value)) {
803        errors.push("Must contain special character");
804      }
805
806      if (value.length < 12) {
807        errors.push("Must be at least 12 characters");
808      }
809
810      if (errors.length > 0) {
811        $.validator.messages.passwordComplexity = errors.join('. ');
812        return false;
813      }
814
815      // Check for complexity requirements.
816      return /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[ !"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])[A-Za-z\d !"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]{12,}$/.test(value);
817    }, "Password must include uppercase, lowercase, number and special character, and cannot match username");
818
819	if (typeof emailBlacklistEnabled !== 'undefined' && emailBlacklistEnabled) {
820		$("#email").rules("add", {
821			emailBlacklist: true
822		});
823	}
824  }
825
826    /* ── Phone number country code lookup tables ── */
827    var CC1 = ['1','7'];
828    var CC2 = [
829      '20','27',
830      '30','31','32','33','34','36','39',
831      '40','41','43','44','45','46','47','48','49',
832      '51','52','53','54','55','56','57','58',
833      '60','61','62','63','64','65','66',
834      '81','82','84','86',
835      '90','91','92','93','94','95','98'
836    ];
837    var CC3 = [
838      '350','351','352','353','354','355','356','357','358','359',
839      '370','371','372','373','374','375','376','377','378',
840      '380','381','382','383','385','386','387','389',
841      '852','853','855','856','880','886',
842      '960','961','962','963','964','965','966','967','968',
843      '970','971','972','973','974','975','976','977',
844      '992','993','994','995','996','998'
845    ];
846
847    var prefixOf3 = [];
848    CC3.forEach(function(c) {
849      var p = c.slice(0, 2);
850      if (prefixOf3.indexOf(p) === -1) prefixOf3.push(p);
851    });
852
853    var prefixOf2or3 = [];
854    CC2.concat(CC3).forEach(function(c) {
855      var p = c.charAt(0);
856      if (prefixOf2or3.indexOf(p) === -1) prefixOf2or3.push(p);
857    });
858
859    function detectCCLen(digits) {
860      if (digits.length >= 3) {
861        if (CC3.indexOf(digits.slice(0, 3)) !== -1) return 3;
862      }
863      if (digits.length >= 2) {
864        if (CC2.indexOf(digits.slice(0, 2)) !== -1) return 2;
865        if (prefixOf3.indexOf(digits.slice(0, 2)) !== -1) return -1;
866      }
867      if (digits.length >= 1) {
868        if (CC1.indexOf(digits.charAt(0)) !== -1) return 1;
869        if (prefixOf2or3.indexOf(digits.charAt(0)) !== -1) return -1;
870      }
871      return 0;
872    }
873
874    /* ── Phone number formatting helpers ── */
875    function groupNational(digits) {
876      var result = '';
877      for (var i = 0; i < digits.length; i++) {
878        if (i > 0 && i % 4 === 0) result += ' ';
879        result += digits[i];
880      }
881      return result;
882    }
883
884    function formatUS(digits) {
885      if (digits.length > 6) {
886        return '(' + digits.slice(0, 3) + ') ' + digits.slice(3, 6) + '-' + digits.slice(6);
887      }
888      if (digits.length > 3) {
889        return '(' + digits.slice(0, 3) + ') ' + digits.slice(3);
890      }
891      if (digits.length > 0) {
892        return '(' + digits;
893      }
894      return '';
895    }
896
897    function formatPhone(raw) {
898      var international = raw.charAt(0) === '+';
899      var digits = raw.replace(/\D/g, '');
900
901      // Preserve a lone + while the user is still typing
902      if (international && digits.length === 0) return '+';
903
904      // No + sign, or starts with 1 — format as US
905      if (!international || digits.charAt(0) === '1') {
906        if (digits.charAt(0) === '1') {
907          var national = digits.slice(1, 11);
908          var result = (international ? '+' : '') + '1';
909          if (national.length > 0) result += ' ' + formatUS(national);
910          return result;
911        }
912        return formatUS(digits.slice(0, 10));
913      }
914
915      // International: + with non-1 digits — European/international formatting
916      digits = digits.slice(0, 15);
917
918      var ccLen = detectCCLen(digits);
919      if (ccLen === -1) return '+' + digits;
920      if (ccLen === 0) ccLen = Math.min(digits.length, 2);
921
922      var cc = digits.slice(0, ccLen);
923      var nat = digits.slice(ccLen);
924
925      if (nat.length === 0) return '+' + cc;
926      return '+' + cc + ' ' + groupNational(nat);
927    }
928
929    /* ── Phone number validation ── */
930    $.validator.addMethod("phoneNumber", function(value, element) {
931      if (!value) return true; // let 'required' rule handle empty
932
933      var digits = value.replace(/\D/g, '');
934      var international = value.charAt(0) === '+';
935
936      if (digits.length === 0) return true;
937
938      // Starts with +1 or no + with leading 1 — validate as US (1 + 10 digits)
939      if (digits.charAt(0) === '1') {
940        if (digits.length !== 11) {
941          $.validator.messages.phoneNumber = 'Please enter a valid 10-digit phone number (with or without the leading 1).';
942          return false;
943        }
944        return true;
945      }
946
947      // No + sign — validate as US (10 digits)
948      if (!international) {
949        if (digits.length !== 10) {
950          $.validator.messages.phoneNumber = 'Please enter a valid 10-digit phone number.';
951          return false;
952        }
953        return true;
954      }
955
956      // International (+ with non-1 country code) — 7 to 15 digits
957      if (digits.length < 7 || digits.length > 15) {
958        $.validator.messages.phoneNumber = 'Please enter a valid international phone number (7\u201315 digits).';
959        return false;
960      }
961      return true;
962    }, "Please enter a valid phone number");
963
964  // Phone input formatter + hint bind to the phone field inside #signup_form, so they must
965  // run after Vue rebuilds it. Grouped for deferral via ssOnSignupFormReady (end of ready()).
966  function bindPhoneInput() {
967    /* ── Phone input formatting & restriction ── */
968    var $phoneInput = $('input[name="phone"]');
969    var previousPhoneValue = '';
970
971    if ($phoneInput.length) {
972      // Add format hint below the phone input
973      if (!$phoneInput.parent().find('.phone-format-hint').length) {
974        $phoneInput.after(
975          '<div class="phone-format-hint">For phone numbers outside the US, start with a + and your country code.</div>'
976        );
977      }
978
979      previousPhoneValue = $phoneInput.val();
980
981      // Sanitize and format on every input event
982      $phoneInput.on('input', function() {
983        var raw = this.value;
984        var cursorPos = this.selectionStart;
985
986        // Check if the raw input starts with +
987        var hasPlus = raw.charAt(0) === '+';
988
989        // Strip everything except digits
990        var digits = raw.replace(/\D/g, '');
991
992        // Enforce max digit length before formatting
993        var international = hasPlus && digits.charAt(0) !== '1';
994        var maxDigits = international ? 15 : (digits.charAt(0) === '1' ? 11 : 10);
995
996        if (digits.length > maxDigits) {
997          // Revert to previous value — don't drop the end
998          this.value = previousPhoneValue;
999          return;
1000        }
1001
1002        // Count how many digits (and leading +) are before the cursor in the raw value
1003        var beforeCursor = raw.slice(0, cursorPos);
1004        var digitsBeforeCursor = beforeCursor.replace(/[^\d]/g, '').length;
1005        var plusBeforeCursor = hasPlus && cursorPos > 0;
1006
1007        // Rebuild sanitized raw for formatting
1008        var sanitized = (hasPlus ? '+' : '') + digits;
1009
1010        this.value = formatPhone(sanitized);
1011        previousPhoneValue = this.value;
1012
1013        // Walk through the formatted value to find where the cursor should land
1014        var formatted = this.value;
1015        var digitsSeen = 0;
1016        var newCursorPos = formatted.length; // default to end
1017        for (var i = 0; i < formatted.length; i++) {
1018          // Skip past the leading + when counting
1019          if (i === 0 && formatted.charAt(0) === '+') continue;
1020          if (/\d/.test(formatted.charAt(i))) {
1021            digitsSeen++;
1022            if (digitsSeen === digitsBeforeCursor) {
1023              newCursorPos = i + 1;
1024              break;
1025            }
1026          }
1027        }
1028        // If cursor was before any digits, place it after the + (or at 0)
1029        if (digitsBeforeCursor === 0) {
1030          newCursorPos = plusBeforeCursor ? 1 : 0;
1031        }
1032
1033        this.setSelectionRange(newCursorPos, newCursorPos);
1034      });
1035    }
1036  }
1037
1038  // ── Run all form-DOM-dependent setup once Vue has (re)built #signup_form ───────────
1039  // bindSignupFormFields, ssSetupRegistrationValidation, and bindPhoneInput each read/write
1040  // form fields or bind handlers to elements Vue rebuilds on mount, so they must run AFTER
1041  // that mount. ssOnSignupFormReady (js/utils/cookieSetup.js) runs the callback immediately
1042  // if the form is already mounted, or queues it until signup.php's mounted() signals ready
1043  // -- so setup no longer races Vue's mount (the "works only half the time" bug). Anything
1044  // above that does NOT touch the form (endpoint setup, submit-time window.* helpers, global
1045  // $.validator.addMethod definitions) stays in ready() and runs regardless.
1046  if (typeof ssOnSignupFormReady === 'function') {
1047    ssOnSignupFormReady(function () {
1048      bindSignupFormFields();
1049      ssSetupRegistrationValidation();
1050      bindPhoneInput();
1051    });
1052  } else {
1053    // Fallback if the helper failed to load: bind immediately (accepts the pre-fix race).
1054    bindSignupFormFields();
1055    ssSetupRegistrationValidation();
1056    bindPhoneInput();
1057  }
1058});
1059
1060// ships_freight LTL field — ClickUp 86e2v839r. Single source of truth for "is
1061// it actually shown right now" — used by both the visibility toggle
1062// (register_country_handler below) and the required/notDefault validation
1063// rules inside $(document).ready() above, so the two can't drift apart. Top
1064// level (not nested in that closure) so register_country_handler, itself top
1065// level, can see it too.
1066function isShipsFreightRevealed() {
1067  return (
1068    $("#ships-freight-div").attr("data-ships-freight-enabled") === "1" &&
1069    getHomeCountry() === "US"
1070  );
1071}
1072
1073function register_country_handler() {
1074  if ($("select#country").length) {
1075    if (
1076      $("select#country option:selected").val() == "AU" ||
1077      $("select#country option:selected").val() == "CA" ||
1078      $("select#country option:selected").val() == "US" ||
1079      $("select#country option:selected").val() == "GB"
1080    ) {
1081      $("#reg-compname")
1082        .parent()
1083        .find(".input-placeholder-text")
1084        .text("Company Name");
1085      if ($("#reg-compname").hasClass("required")) {
1086        $("#reg-compname").removeClass("required");
1087      }
1088      $(".vat_field").hide();
1089      if ($('input[name="optin_consent"]').hasClass("required")) {
1090        $('input[name="optin_consent"]')
1091          .removeClass("required")
1092          .prop("checked", true);
1093      }
1094      $(".optin-consent-field").hide();
1095    }
1096    if ($("select#country option:selected").val() == "AU") {
1097    } else if ($("select#country option:selected").val() == "GB") {
1098      $("#reg-compname")
1099        .parent()
1100        .find(".input-placeholder-text")
1101        .text("Business Name");
1102      if ($('label[for="reg-compname"]').html() === "Company") {
1103        $('label[for="reg-compname"]').append("<em> *</em>");
1104      }
1105      $("#reg-compname").addClass("required");
1106      $(".optin-consent-field").show();
1107      $('input[name="optin_consent"]')
1108      .removeClass("required")
1109      .prop("checked", false);
1110    } else if ($("select#country option:selected").val() == "CA") {
1111    } else if ($("select#country option:selected").val() == "FR") {
1112      $(".optin-consent-field").show();
1113    } else if ($("select#country option:selected").val() == "DE") {
1114      $(".optin-consent-field").show();
1115    } else {
1116      $(".vat_field").hide();
1117      $(".optin-consent-field").hide();
1118    }
1119  }
1120
1121  // ships_freight LTL reveal — deliberately OUTSIDE the `#country` guard above:
1122  // several pages hide the country field entirely (hide_fields=",country"), and
1123  // US visitors on those pages still need this to work, so it reads the
1124  // resolved home country directly instead of the select's value.
1125  var $shipsFreightRow = $("#ships-freight-div");
1126  if ($shipsFreightRow.length) {
1127    if (isShipsFreightRevealed()) {
1128      $shipsFreightRow.removeClass("ss-registration-field--hidden");
1129    } else {
1130      $shipsFreightRow.addClass("ss-registration-field--hidden");
1131    }
1132  }
1133}
1134
1135function submitRegistrationForm(form, captchaScore) {
1136  // If coupon value equals FREE30, then replace with blank value
1137  if ($("#coupon").val() === "FREE30" || $("#coupon").val() === "FREETRIAL") {
1138    $("#coupon").val("");
1139  }
1140  let home_country = getHomeCountry();
1141  // If country is France and coupon is blank, auto add in the Free Coupon Code
1142  if (home_country === "FR" && $("#coupon").val() === "") {
1143    autoAddFreeCouponByCountry();
1144  }
1145
1146  const referral_code = ss_get_cookie("referral_code")
1147
1148  if(referral_code === '14daytrial') {
1149    if(home_country !== 'US' || $("#coupon").val()){
1150      const { host } = window.location;
1151      const domain = host.includes("shipstation") ? `.shipstation${host.split("shipstation").pop()}` : host;
1152      document.cookie = `referral_code=;max-age=0; path=/; domain=${domain};`;  
1153      document.cookie = `refCode=;max-age=0; path=/; domain=${domain};`;  
1154      $("#referral_code").val('');
1155    }  
1156  }
1157
1158  // If Landing page is /toyota-parts-center-online
1159  const dealerCode = $("#coupon").val();
1160  const toyotaPartsCenterOnlineLandingPage = window.location.href.includes("/toyota-parts-center-online");
1161  if(toyotaPartsCenterOnlineLandingPage && dealerCode) {
1162    // Set Toyota Coupon Value to TOYOTASTANDARD500 per https://shipstation.clickup.com/t/9017669008/86e36gtbc
1163    $("#coupon").val("TOYOTASTANDARD500");
1164    $("#referral_code").val(`dealer-code-${dealerCode.toString()}`);
1165  }
1166
1167  // Assign geo-specific billing plan
1168  const billingPlanCode = getBillingPlanCode(home_country);
1169  $("#plan_profile_code").val(billingPlanCode);
1170
1171  // Send last visited page to GA
1172  let lastVisitedPageBeforeSignup;
1173
1174  // Async function to fetch last visited page.
1175  fetchLastVisitedPage().then((data) => {
1176    lastVisitedPageBeforeSignup = data;
1177  });
1178
1179  $(form).ajaxSubmit({
1180    type: "POST",
1181    url: _registerEndpoint,
1182    headers: {
1183      "reCAPTCHAresponseToken": recaptchaTokenGlobal ? recaptchaTokenGlobal : "",
1184    },
1185    dataType: "json",
1186    success: function (resp) {
1187      if (resp.success) {
1188        if (resp.userId && resp.username) {
1189          console.log(JSON.stringify(resp.host));
1190
1191          // CJ Tracking
1192          dataLayer.push({ event: "trialSignUp", sellerId: resp.userId });
1193
1194          const uspsRefCode = $('input[name=sales_rep_code]')?.val() || "";
1195          const coupon = $('#coupon')?.val() || "";
1196
1197          // Set email cookie
1198          const { host } = window.location;
1199          const domain = host.includes("shipstation") ? `.shipstation${host.split("shipstation").pop()}` : host;
1200          document.cookie = `signup_email=${resp.username}; path=/; domain=${domain}; samesite=none; secure;`;
1201
1202          const browserLanguage = navigator.language.toLowerCase() || 'en-us';
1203
1204          const currentUnixTimestamp = Date.now();
1205           // Modulo used to convert to midnight date format for hubspot
1206          const trialActivationDate = currentUnixTimestamp - (currentUnixTimestamp % 86400000) + 86400000;
1207          let trialEndDate = resp.trialEndDate ? resp.trialEndDate - (resp.trialEndDate % 86400000) : '';
1208                    
1209          const referral_code = ss_get_cookie("referral_code")
1210
1211          if(referral_code === '14daytrial' && !coupon) {
1212            trialEndDate = trialActivationDate + (86400 * 1000 * 13);
1213          }
1214
1215          dataLayer.push({
1216            event: "account_created", // hardcoded
1217            brand: 'ShipStation',
1218            userId: resp.userId,
1219            email: resp.username,
1220            home_country: home_country,
1221            orderId: resp.refno,
1222            affiliate: $("#affiliate").val(),
1223            referrer: $("#referrer").val() || "",
1224            referral_code: $("#referral_code").val(),
1225            coupon: coupon,
1226            phonenumber: $("input[name=phone]").val(),
1227            company: $("#reg-compname").val(),
1228            howdidyouhearaboutus: $("#howdidyouhearaboutus").length
1229              ? $("#howdidyouhearaboutus").val()
1230              : "",
1231            shipment_volume: $("#shipment_volume").length
1232              ? $("#shipment_volume").val()
1233              : "",
1234            ships_freight: $("#ships_freight").length ? $("#ships_freight").val() : "null",
1235            sellerId_int: parseInt(resp.refno, 16),
1236            experiments_object: getExperimentArray(ss_get_cookie("_gaexp")),
1237            experiments_string: JSON.stringify(getExperimentArray(ss_get_cookie("_gaexp")) || ''),
1238            last_visited_page: lastVisitedPageBeforeSignup,
1239            isBuyNow: $("#buy_now").is(":checked") ? "true" : "false",
1240          });
1241
1242          try {
1243            // Standard Segment tracking (only requires analytics)
1244            if (window.analytics && typeof window.analytics.track === 'function' && typeof window.analytics.identify === 'function') {
1245
1246              // Segment Identify
1247              analytics.identify(resp.userId, {
1248                "email": resp.username,
1249                "home_country": home_country
1250              });
1251
1252              // Segment Account Created tracking
1253              analytics.track("Account Created", {
1254                brand: 'ShipStation',
1255                utm_medium: ss_get_cookie('utm_medium'),
1256                utm_source: ss_get_cookie('utm_source'),
1257                utm_campaign: ss_get_cookie('utm_campaign'),
1258                utm_term: ss_get_cookie('utm_term'),
1259                utm_content: ss_get_cookie('utm_content'),
1260                referral_code: $("#referral_code").val(),
1261                coupon: coupon,
1262                email: resp.username,
1263                home_country: home_country,
1264                affiliate: $("#affiliate").val(),
1265                referrer: $("#referrer").val() || "",
1266                phoneNumber: $("input[name=phone]").val(),
1267                company: $("#reg-compname").val(),
1268                howdidyouhearaboutus: $("#howdidyouhearaboutus").length ? $("#howdidyouhearaboutus").val() : "",
1269                fb__fbp: ss_get_cookie('_fbp'),
1270                fb_fbc: ss_get_cookie('_fbc'),
1271                fb_event_id: resp.userId,
1272                cje_cookie: ss_get_cookie('cje'),
1273                customer_id: parseInt(resp.refno, 16),
1274                seller_id_hex: resp.refno,
1275                last_visited_page: lastVisitedPageBeforeSignup,
1276                gclid_value: ss_get_cookie('gclid'),
1277                shipment_volume: $("#shipment_volume").length ? $("#shipment_volume").val() : "",
1278                order_id: parseInt(resp.refno, 16),
1279                currency: "USD",
1280                isBuyNow: $("#buy_now").is(":checked") ? "true" : "false",
1281                ships_freight: $("#ships_freight").length ? $("#ships_freight").val() : "null",
1282              });
1283            } else {
1284              console.warn('Account Created tracking skipped: Missing analytics');
1285            }
1286          } catch (e) {
1287            console.error('Error in Account Created tracking:', e);
1288          }
1289
1290          // Optimizely data for HubSpot
1291          let optimizelyVisitorId = '';
1292          let optimizelyExperimentName = '';
1293          try {
1294            if (window.optimizely && typeof window.optimizely.get === 'function') {
1295              const visitor = window.optimizely.get("visitor");
1296              optimizelyVisitorId = visitor && visitor.visitorId ? visitor.visitorId : '';
1297
1298              const state = window.optimizely.get("state");
1299              if (state) {
1300                const activeIds = state.getActiveExperimentIds() || [];
1301                const experimentStates = state.getExperimentStates() || {};
1302                const experimentNames = activeIds
1303                  .map(function(id) {
1304                    const exp = experimentStates[id];
1305                    if (!exp) return '';
1306                    const name = exp.experimentName || exp.name || '';
1307                    const variationId = exp.variation && exp.variation.id ? exp.variation.id : '';
1308                    if (!name) return '';
1309                    return variationId ? name + ' + ' + variationId  : name;
1310                  })
1311                  .filter(Boolean);
1312                optimizelyExperimentName = experimentNames.join(', ');
1313              }
1314            }
1315          } catch (e) {
1316            console.error('Error capturing Optimizely data:', e);
1317          }
1318
1319          // send to hubspot
1320          $.get("/hubspot/form-api.php", {
1321            pageName: "Create My Account",
1322            pageURL: window.location.href,
1323            firstname: $("#reg-fname").length ? $("#reg-fname").val() : "",
1324            lastname: $("#reg-lname").length ? $("#reg-lname").val() : "",
1325            refno: resp.refno,
1326            email: resp.username,
1327            userId: resp.userId,
1328            is_fraud: resp.isFraud?.toString(),
1329            phonenumber: $("input[name=phone]").val(),
1330            company: $("#reg-compname").val(),
1331            howdidyouhearaboutus: $("#howdidyouhearaboutus").length
1332              ? $("#howdidyouhearaboutus").val()
1333              : "",
1334            optin_consent: optinConsentCountries.includes(home_country)
1335              ? $("input[name=optin_consent]:checked").val() ? "yes" : "no"
1336              : "",
1337            home_country: home_country,
1338            referral_code: $("#referral_code").length
1339              ? $("#referral_code").val()
1340              : "",
1341            coupon: coupon,
1342            shipment_volume: $("#shipment_volume").length
1343              ? $("#shipment_volume").val()
1344              : "",
1345            captcha_score: captchaScore || "",
1346            usps_referral_code: uspsRefCode,
1347            gclid_field: $('#gclid_field').val() || '',
1348            verified: resp.verified || '',
1349            trialActivationDate: trialActivationDate,
1350            trialEndDate: trialEndDate,
1351            browserLanguage: browserLanguage,
1352            utcOffsetInMinutes: $("#utcoffset").val(),
1353            isDST: window.luxon ? window.luxon.DateTime.local().isInDST : null,
1354            routing_owner_id: $('input[name=routing_owner_id]').val(),
1355            lead_source: $('input[name=lead_source]').val(),
1356            referrer: $("#referrer").val() || "",
1357            optimizely_id_station: optimizelyVisitorId,
1358            optimizely_experiment_name: optimizelyExperimentName,
1359            isBuyNow: $("#buy_now").is(":checked") ? "true" : "false",
1360            device_type: $("#device_type").val() || "",
1361            AI_comms_opt_in: $("#AI_comms_opt_in").val() || "",
1362            targetApp: $("#targetApp").val() || "default",
1363            ships_freight: $("#ships_freight").length ? $("#ships_freight").val() : "null",
1364          });
1365        }
1366
1367        // Check if user us submitting from an iframe
1368        if(window.location !== window.parent.location) {
1369          // Send Redirect Message to ShipWorks
1370          window.parent.postMessage({
1371            action: "REDIRECT_TO_STEP2",
1372            url: step2Redirect
1373          }, 'https://www.shipworks.com/');
1374        }
1375
1376        var thirtyDays = 1000 * 60 * 60 * 24 * 60;
1377        var expires = new Date(new Date().valueOf() + thirtyDays);
1378        // Pin domain to '.shipstation.com' explicitly so the cookie stays readable on
1379        // ship.shipstation.com for the step 2 redirect, including from non-www hosts
1380        // (staging.shipstation.com). The host-derived default only yields
1381        // '.shipstation.com' on www.*, so an override is required here.
1382        ss_cookie("orderId", resp.refno, expires, ".shipstation.com");
1383
1384        const homeCountry = getHomeCountry();
1385        const countriesWithStep2 = [];
1386
1387        const partitionId = resp.partitionId;
1388        const queryParam = queryParamFromObject({firsttime: true, a: partitionId ? resp.authToken : null });
1389        const onboarding = true;
1390        const newUsername = resp.username || '';
1391        const autoLoginNonce= resp.autoLoginNonce || '';
1392
1393        // Work towards updating the url to contain our auto login params.
1394        step2Redirect = partitionId && resp.authToken ? step2Redirect.replace('ship.', `ship${partitionId}.`) : step2Redirect;
1395        
1396        step2Redirect = step2Redirect + queryParam;
1397
1398        if(step2Redirect.includes('step2') || countriesWithStep2.includes(homeCountry)) {
1399          step2Redirect = "/step2/";
1400          if (homeCountry === "FR") {
1401            step2Redirect = "/step2-fr/";
1402          }
1403          if (homeCountry === "GB") {
1404            step2Redirect = "/uk/step2/";
1405          }
1406        }
1407
1408      /**
1409       * Redirects the user to the appropriate step in the onboarding process.
1410       */
1411      setTimeout(function () {
1412          // Determine final redirect URL based on presence of autoLoginNonce
1413          let finalRedirectUrl;
1414
1415          if (autoLoginNonce && autoLoginNonce !== false && autoLoginNonce !== '') {
1416              // Auth0 flow: Start with clean base URL and only add Auth0-specific params
1417              const baseUrl = new URL(step2Redirect);
1418              
1419              // Clear all existing query parameters to avoid legacy parameter conflicts
1420              baseUrl.search = '';
1421              
1422              // Remove /login path for Auth0 flow
1423              if (baseUrl.pathname === '/login') {
1424                baseUrl.pathname = '';
1425              }
1426
1427              // Add only the Auth0-specific parameters
1428              baseUrl.searchParams.set('onboarding', onboarding);
1429              baseUrl.searchParams.set('username', newUsername);
1430              baseUrl.searchParams.set('firsttime', true);
1431              baseUrl.searchParams.set('nonce', autoLoginNonce);
1432
1433              finalRedirectUrl = baseUrl.toString();
1434
1435          } else {
1436              // Legacy flow: use the URL with all existing parameters
1437              finalRedirectUrl = step2Redirect;
1438          }
1439
1440          // Perform the actual redirect
1441          window.location.replace(finalRedirectUrl);
1442          
1443      }, 1000);
1444      } else {
1445        window.alert(resp.message);
1446        //window.alert("An error has occurred. Please contact [email protected] if the problem persists.");
1447        $("#save-button")
1448          .removeAttr("disabled")
1449          .css("background-color", "")
1450          .val(saveButtonText);
1451      }
1452    },
1453    error: function (resp, a, b, c) {
1454      $("#save-button")
1455        .removeAttr("disabled")
1456        .css("background-color", "")
1457        .val(saveButtonText);
1458
1459      // If coupon value equals is blank, then replace with FREE30
1460      if ($("#coupon").val() === "") {
1461        // Adds FREE30 coupon by default or finds Country specific coupon to inject
1462        autoAddFreeCouponByCountry();
1463
1464        if(dealerCode && toyotaPartsCenterOnlineLandingPage) { 
1465          $("#coupon").val(dealerCode);
1466        }
1467      }
1468    },
1469  });
1470}
1471
1472// Pass in a country code; returns the associated billing plan or an empty string
1473function getBillingPlanCode(country) {
1474  const mappings = {
1475    NZ: "ss_anz_q12023",
1476    FR: "ss_eu_byoa_2023_q2",
1477    DE: "ss_eu_byoa_2023_q2"
1478  }
1479
1480  let plan = mappings[country] || "";
1481  
1482  return plan;
1483}
1484
1485function getHomeCountry() {
1486  if ($("#country").length && $("#country").val() != "") {
1487    return $("#country").val();
1488  }
1489
1490  return "US";
1491}
1492
1493function autoAddFreeCouponByCountry() {
1494  // Get country code
1495  const country = getHomeCountry();
1496
1497  // Object of coupons by country code
1498  const countryCoupon = {
1499    FR: "",
1500    US: "FREE30",
1501    GB: "FREE30",
1502    CA: "FREE30",
1503    AU: "FREE30",
1504    NZ: "",
1505    Other: "FREE30",
1506  };
1507
1508  const referral_code = ss_get_cookie("referral_code")
1509
1510  // This function runs both on submit (submitRegistrationForm) and from signup.php's
1511  // ss_populateSignupCookieFields. The name-attribute selector (rather than the #coupon id)
1512  // keeps both call sites consistent.
1513  if(referral_code !== '14daytrial') {
1514      // Replace coupon value with country code specific one
1515      $('input[name="coupon"]').val(countryCoupon[country]);
1516  } else {
1517      $('input[name="coupon"]').val('FREETRIAL');
1518  }
1519}
1520
1521function logCaptchaToTray(data, successCallback) {
1522  $.ajax({
1523    type: 'POST',
1524    url: 'https://f9772077-48b9-4629-81b1-37f3849062fd.trayapp.io', 
1525    data: data,
1526    success: () => {
1527      successCallback();
1528    },
1529    error: (err) => {
1530      console.error("error logging to tray.io: ", err);
1531      successCallback();
1532    }
1533  });
1534}
1535
1536function queryParamFromObject(params) {
1537  if (!params) return "";
1538
1539  let searchParams = new URLSearchParams();
1540
1541  for (const key in params) {
1542    if (Object.hasOwnProperty.call(params, key)) {
1543      const paramName = key;
1544      const paramValue = params[key];
1545
1546      if(paramValue) {
1547        searchParams.append(paramName, paramValue);
1548      }
1549    }
1550  }
1551
1552  return "?" + searchParams.toString();
1553}
1554
1555function isProdEnvironment() {
1556  return window.location.host.includes('shipstation.com')
1557}
1558
1559function getExperimentArray(cookie) {
1560
1561  if(!cookie) return null;
1562
1563  const experiments = cookie.split(".").splice(2);
1564
1565  return experiments
1566    .join()
1567    .split("!")
1568    .map((experiment) => {
1569      const experimentData = experiment.split(",");
1570
1571      return {
1572        id: experimentData[0],
1573        variant: experimentData.pop(),
1574      };
1575    });
1576}
1577
1578function getFieldFromQuery(fieldName) {
1579  const rawParams = new URLSearchParams(window.location.search);
1580  const formattedParams = new URLSearchParams();
1581
1582  for (const [name, value] of rawParams) {
1583    formattedParams.append(name.toLowerCase(), value);
1584  }
1585
1586  const searchParams = formattedParams;
1587  // Convert to lowercase before comparison
1588  const formattedName = fieldName.toLowerCase();
1589  return searchParams.get(formattedName)
1590      ? decodeURIComponent(searchParams.get(formattedName))
1591      : "";
1592}
1593
1594async function fetchLastVisitedPage() {
1595  if (window.analyticsModule && window.analyticsModule.getLastVisitedPageForGTM) {
1596      try {
1597          const data = await window.analyticsModule.getLastVisitedPageForGTM();
1598
1599          if (data && data.last_visited_page) {
1600              return data.last_visited_page;
1601          }
1602
1603      } catch (error) {
1604          return error;
1605      }
1606  }
1607}
1608
1609/**
1610 * Captures the YOFI journey_id from the yfai_group_id cookie and populates
1611 * Captures the YOFI journey_id and populates the hidden form field.
1612 */
1613function captureJourneyId() {
1614  const journeyId = window.YofiTelemetry?.getJourneyId?.();
1615  if (journeyId) {
1616    $('#journey_id').val(journeyId);
1617  }
1618}

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.