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.