1'use strict'; 2 3(function () { 4 if (!window.newOnboardingConfig) { 5 return; 6 } 7 8 const config = window.newOnboardingConfig; 9 document.documentElement.classList.add('new-onboarding-page'); 10 const $root = $('.new-onboarding'); 11 const $form = $('#new-onboarding-form'); 12 const $landing = $('#onboarding-landing'); 13 const $body = $('body'); 14 const MENU_TRANSITION_ANIMATION_MS = 280; 15 let menuTransitionAnimationTimer = null; 16 let landingWizardResetTimer = null; 17 18 const DEFAULT_PACKAGE = 'CORE'; 19 const UI_TO_BACKEND_PACKAGE = { 20 CORE: 'STARTUP', 21 ESSENTIAL: 'BUSINESS', 22 BUSINESS: 'BUSINESS', 23 AI: 'SAAS', 24 }; 25 26 const FLOW_BY_PACKAGE = { 27 CORE: ['welcome', 'subscription', /* 'support', */ 'company', 'verify', 'domain', 'checkout'], 28 ESSENTIAL: ['welcome', 'subscription', /* 'support', */ 'desktop_client',/* 'desktop_office', */ 'company', 'verify', 'domain', 'checkout'], 29 BUSINESS: ['welcome', 'subscription', 'desktop_client', /* 'desktop_office', */ 'company', 'verify', 'domain', 'checkout'], 30 // AI: ['subscription', 'desktop_client', 'desktop_office', 'company', 'verify', 'checkout'], 31 }; 32 const FLOW_QUOTE = ['quote_contact']; 33 34 const state = { 35 selectedPackage: DEFAULT_PACKAGE, 36 selectedUsers: 20, 37 selectedPlanName: 'Essential', 38 selectedPlanPrice: '', 39 isQuoteRequest: false, 40 flow: FLOW_BY_PACKAGE[DEFAULT_PACKAGE].slice(), 41 currentStep: '', 42 maxReachedStepIndex: -1, 43 seenSteps: {}, 44 email: '', 45 checkoutMounted: false, 46 resendCodeCooldownEndsAt: 0, 47 }; 48 49 const RESEND_CODE_COOLDOWN_SEC = 60; 50 const ANNUAL_SUBSCRIPTION_DISCOUNT_RATE = 0.1; 51 const MOBILE_SYNC_MONTHLY_PRICE = 500; 52 const SUPPORT_PLUS_MONTHLY_PRICE = 200; 53 let resendCodeCooldownTimer = null; 54 55 function normalizePackageName(value) { 56 return String(value || '').toUpperCase(); 57 } 58 59 function getBackendPackageName(uiPackageName) { 60 if (getOnboardingMode() === 'self-hosted') { 61 return 'SAAS'; 62 } 63 64 const normalized = normalizePackageName(uiPackageName); 65 return UI_TO_BACKEND_PACKAGE[normalized] || UI_TO_BACKEND_PACKAGE[DEFAULT_PACKAGE]; 66 } 67 68 function getFlow(uiPackageName) { 69 const normalized = normalizePackageName(uiPackageName); 70 const flow = (FLOW_BY_PACKAGE[normalized] || FLOW_BY_PACKAGE[DEFAULT_PACKAGE]).slice(); 71 return getOnboardingMode() === 'self-hosted' 72 ? flow.filter(function (step) { return step !== 'domain'; }) 73 : flow; 74 } 75 76 function setFeedback(id, message, isError) { 77 const $el = $('#' + id); 78 if (!$el.length) return; 79 $el.text(message || ''); 80 $el.toggleClass('is-error', !!isError); 81 $el.toggleClass('is-success', !!message && !isError); 82 } 83 84 function setButtonLoading($btn, isLoading) { 85 if (!$btn || !$btn.length) return; 86 if (isLoading) { 87 if ($btn.data('loading')) return; 88 $btn.data('loading', true); 89 $btn.data('prev-disabled', !!$btn.prop('disabled')); 90 $btn.prop('disabled', true).addClass('is-loading'); 91 return; 92 } 93 if (!$btn.data('loading')) return; 94 $btn.data('loading', false); 95 $btn.prop('disabled', !!$btn.data('prev-disabled')).removeClass('is-loading'); 96 } 97 98 function clearFeedbacks() { 99 setFeedback('create-order-feedback', ''); 100 setFeedback('verify-feedback', ''); 101 setFeedback('domain-feedback', ''); 102 setFeedback('checkout-feedback', ''); 103 } 104 105 function serializeForm() { 106 return $form.serialize(); 107 } 108 109 function getCurrentStepIndex() { 110 return state.flow.indexOf(state.currentStep); 111 } 112 113 function getStepElements() { 114 return $form.find('.new-onboarding__step'); 115 } 116 117 const STEP_DOM_ALIASES = { 118 desktop_client: ['desktop_client', 'desktop-client'], 119 desktop_office: ['desktop_office', 'desktop-office'], 120 }; 121 122 function getStepName($step) { 123 return String($step.attr('data-step') || ''); 124 } 125 126 function getFlowStepName(domStepName) { 127 if (!domStepName) { 128 return ''; 129 } 130 const aliases = Object.keys(STEP_DOM_ALIASES); 131 for (let i = 0; i < aliases.length; i++) { 132 const flowName = aliases[i]; 133 if (STEP_DOM_ALIASES[flowName].indexOf(domStepName) !== -1) { 134 return flowName; 135 } 136 } 137 return domStepName; 138 } 139 140 function findStepElement(flowStepName) { 141 const candidates = STEP_DOM_ALIASES[flowStepName] || [flowStepName]; 142 for (let i = 0; i < candidates.length; i++) { 143 const $step = $form.find('.new-onboarding__step[data-step="' + candidates[i] + '"]'); 144 if ($step.length) { 145 return $step; 146 } 147 } 148 return $(); 149 } 150 151 function getDomStepName(flowStepName) { 152 const $step = findStepElement(flowStepName); 153 return $step.length ? getStepName($step) : flowStepName; 154 } 155 156 function isWizardTemplateCurrent() { 157 const $content = $form.find('.new-onboarding__wizard-content'); 158 if ($content.attr('data-wizard-version') === '2') { 159 return true; 160 } 161 return $form.find('.new-onboarding__step[data-step="desktop_client"]').length > 0 162 && $form.find('.new-onboarding__step[data-step="mobile_sync"]').length > 0; 163 } 164 165 function warnIfWizardDomOutdated() { 166 const $warning = $('#wizard-dom-warning'); 167 if (!$warning.length) { 168 return; 169 } 170 171 if (isWizardTemplateCurrent()) { 172 $warning.prop('hidden', true).text(''); 173 return; 174 } 175 176 const missing = state.flow.filter(function (stepName) { 177 return !findStepElement(stepName).length; 178 }); 179 180 if (!missing.length) { 181 $warning.prop('hidden', true).text(''); 182 return; 183 } 184 185 $warning.prop('hidden', false).text( 186 'Wizard template is outdated (missing steps: ' + missing.join(', ') + '). ' 187 + 'Clear Latte cache (temp/cache) and hard-reload the page.' 188 ); 189 } 190 191 const WIZARD_STEP_SCROLL_DURATION_MS = 600; 192 193 function getWizardStepScrollEasing() { 194 return $.easing && $.easing.easeInOutCubic ? 'easeInOutCubic' : 'swing'; 195 } 196 197 function scrollToStep(stepName) { 198 const $step = findStepElement(stepName); 199 if (!$step.length || !$step.hasClass('is-visible')) { 200 return; 201 } 202 window.requestAnimationFrame(function () { 203 window.requestAnimationFrame(function () { 204 const top = $step.offset().top - 110; 205 if (prefersReducedMotion()) { 206 window.scrollTo(0, Math.max(0, top)); 207 return; 208 } 209 $('html, body').stop(true).animate( 210 { scrollTop: Math.max(0, top) }, 211 { duration: WIZARD_STEP_SCROLL_DURATION_MS, easing: getWizardStepScrollEasing() } 212 ); 213 }); 214 }); 215 } 216 217 function resolveStepIndex(sourceStepName) { 218 if (sourceStepName) { 219 const sourceIdx = state.flow.indexOf(sourceStepName); 220 if (sourceIdx >= 0) { 221 return sourceIdx; 222 } 223 } 224 225 const stateIdx = getCurrentStepIndex(); 226 if (stateIdx >= 0) { 227 return stateIdx; 228 } 229 230 const domActive = getFlowStepName(getStepName(getStepElements().filter('.is-active').first())); 231 const domIdx = state.flow.indexOf(domActive); 232 return domIdx; 233 } 234 235 function hasSeenStep(stepName) { 236 return !!state.seenSteps[stepName]; 237 } 238 239 function markStepSeen(stepName) { 240 state.seenSteps[stepName] = true; 241 } 242 243 const CHOICE_PULSE_MS = 200; 244 245 function pulseAndAdvance($btn, advance) { 246 if ($btn && $btn.length && !prefersReducedMotion()) {
247 $btn.addClass('is-clicked'); 248 window.setTimeout(function () { 249 $btn.removeClass('is-clicked'); 250 }, CHOICE_PULSE_MS); 251 } 252 window.setTimeout(advance, CHOICE_PULSE_MS); 253 } 254 255 function isWizardEnterNavigationEnabled() { 256 return $root.hasClass('is-wizard-mode') && !$form.hasClass('is-hidden'); 257 } 258 259 function getWizardStepFocusables($step) { 260 return $step 261 .find('input, select, textarea') 262 .filter(':visible') 263 .filter(function () { 264 const $el = $(this); 265 if ($el.is('[type="hidden"], [disabled], [readonly]')) { 266 return false; 267 } 268 if ($el.hasClass('verify-digit')) { 269 return false; 270 } 271 return true; 272 }); 273 } 274 275 function focusWizardField($field) { 276 const el = $field[0]; 277 if (!el) { 278 return; 279 } 280 281 window.requestAnimationFrame(function () { 282 if (!el.isConnected || !$field.is(':visible') || el.disabled) { 283 return; 284 } 285 286 el.focus({ preventScroll: true }); 287 288 if ( 289 el.select && 290 $field.is('input') && 291 !/^(checkbox|radio|button|submit|number)$/i.test(el.type || '') 292 ) { 293 el.select(); 294 } 295 }); 296 } 297 298 function focusActiveWizardStepFirstField(stepName) { 299 const $step = findStepElement(stepName); 300 if (!$step.length) { 301 return; 302 } 303 304 const $first = getWizardStepFocusables($step).first(); 305 if ($first.length) { 306 focusWizardField($first); 307 } 308 } 309 310 function scheduleFocusActiveWizardStepFirstField(stepName) { 311 window.requestAnimationFrame(function () { 312 focusActiveWizardStepFirstField(stepName); 313 }); 314 } 315 316 function clickWizardStepPrimaryAction($step) { 317 const $primary = $step 318 .find('.js-wizard-next') 319 .add( 320 $step.find( 321 '.new-onboarding__wizard-users-actions > .new-onboarding__button:not(.is-secondary), .new-onboarding__actions > .new-onboarding__button:not(.is-secondary)' 322 ) 323 ) 324 .filter(':visible') 325 .first(); 326 327 if ($primary.length) { 328 $primary.trigger('click'); 329 return true; 330 } 331 332 return false; 333 } 334 335 function handleWizardFieldEnter(event) { 336 if (event.key !== 'Enter' || event.isComposing) { 337 return; 338 } 339 340 if (!isWizardEnterNavigationEnabled()) { 341 return; 342 } 343 344 const $field = $(event.currentTarget); 345 if ($field.hasClass('verify-digit')) { 346 return; 347 } 348 349 const $step = $field.closest('.new-onboarding__step'); 350 if (!$step.length || !$step.hasClass('is-active')) { 351 return; 352 } 353 354 event.preventDefault(); 355 356 if ( 357 $step.attr('data-step') === 'company' && 358 $field.closest('.js-company-form-fields').length && 359 !validateSingleCompanyField($field) 360 ) { 361 return; 362 } 363 364 const $focusables = getWizardStepFocusables($step); 365 const currentIndex = $focusables.index($field); 366 367 if (currentIndex >= 0 && currentIndex < $focusables.length - 1) { 368 focusWizardField($focusables.eq(currentIndex + 1)); 369 return; 370 } 371 372 clickWizardStepPrimaryAction($step); 373 } 374 375 function showStep(stepName, options) { 376 const opts = options || {}; 377 const stepIndex = state.flow.indexOf(stepName); 378 if (stepIndex === -1) { 379 return; 380 } 381 382 const previousStepName = state.currentStep; 383 const isFirstVisit = !hasSeenStep(stepName); 384 const shouldScroll = opts.scroll !== false && (isFirstVisit || opts.forceScroll === true); 385 386 if (opts.reveal !== false) { 387 state.maxReachedStepIndex = Math.max(state.maxReachedStepIndex, stepIndex); 388 } 389 state.currentStep = stepName; 390 391 getStepElements().each(function () { 392 $(this) 393 .removeClass('is-active is-visible is-inactive') 394 .addClass('is-flow-hidden') 395 .prop('inert', false); 396 }); 397 398 state.flow.forEach(function (flowStepName) { 399 const $step = findStepElement(flowStepName); 400 if (!$step.length) { 401 return; 402 } 403 $step.removeClass('is-flow-hidden').addClass('is-visible'); 404 if (flowStepName === stepName) { 405 $step.addClass('is-active'); 406 } else if (!hasSeenStep(flowStepName)) { 407 $step.addClass('is-inactive').prop('inert', true); 408 } 409 }); 410 411 warnIfWizardDomOutdated(); 412 413 if (shouldScroll) { 414 scrollToStep(stepName); 415 } 416 417 if (isFirstVisit) { 418 markStepSeen(stepName); 419 } 420 421 if (stepName === 'company' && !state.isQuoteRequest) { 422 initVisitorCountryAndPrefix({ 423 $country: $('.js-company-country-input'), 424 $prefix: $('.js-company-phone-prefix-input'), 425 onAfter: function () { 426 syncCloudContactToForm(); 427 updateCompanyCountrySelectStyle(); 428 if (opts.focusFirstField !== false && isWizardEnterNavigationEnabled()) { 429 scheduleFocusActiveWizardStepFirstField(stepName); 430 } 431 }, 432 }); 433 } else if (stepName === 'subscription') { 434 syncSubscriptionUsersWrap(); 435 if (opts.focusFirstField !== false && isWizardEnterNavigationEnabled() && getSubscriptionPeriod() === 'annually') { 436 scheduleFocusActiveWizardStepFirstField(stepName); 437 } 438 } else if (stepName === 'domain') { 439 $('#check-domain-and-continue').prop('disabled', true); 440 if (isFirstVisit && state.oniceDomain) { 441 $('#iwEmailDomain').val(state.oniceDomain); 442 checkIwDomainAvailability(); 443 } 444 if (opts.focusFirstField !== false && isWizardEnterNavigationEnabled()) { 445 scheduleFocusActiveWizardStepFirstField(stepName); 446 } 447 } else if (stepName === 'verify') { 448 ensureResendCodeCooldownTicker(); 449 if (opts.focusFirstField !== false && isWizardEnterNavigationEnabled()) { 450 scheduleFocusActiveWizardStepFirstField(stepName); 451 } 452 } else if (stepName === 'checkout' && !state.checkoutMounted) { 453 renderEmbeddedCheckout().catch(function () { 454 setFeedback('checkout-feedback', 'Unable to load Stripe checkout.', true); 455 }); 456 } else if (opts.focusFirstField !== false && isWizardEnterNavigationEnabled()) { 457 scheduleFocusActiveWizardStepFirstField(stepName); 458 } 459 } 460 461 function getVisitorIso2() { 462 if (config.visitorIso2) { 463 return config.visitorIso2; 464 } 465 if (config.i18n && config.i18n.visitorIso2) { 466 return config.i18n.visitorIso2; 467 } 468 if (typeof window.visitorIso2 !== 'undefined' && window.visitorIso2) { 469 return window.visitorIso2; 470 } 471 return ''; 472 } 473 474 function applyPhonePrefixForCountry(iso2, $prefixSelect) { 475 if (!iso2 || !$prefixSelect || !$prefixSelect.length) { 476 return; 477 } 478 479 if ($prefixSelect.find('option[value="' + iso2 + '"]').length) { 480 $prefixSelect.val(iso2); 481 } 482 } 483 484 function initVisitorCountryAndPrefix(opts) { 485 const $country = opts.$country; 486 const $prefix = opts.$prefix; 487 if (!$country || !$country.length) { 488 return; 489 } 490 491 const visitorIso2 = getVisitorIso2();
492 let countryIso = ($country.val() || '').trim(); 493 494 if (!countryIso && visitorIso2 && $country.find('option[value="' + visitorIso2 + '"]').length) { 495 $country.val(visitorIso2); 496 countryIso = visitorIso2; 497 } 498 499 if (countryIso) { 500 applyPhonePrefixForCountry(countryIso, $prefix); 501 } 502 503 if (typeof opts.onAfter === 'function') { 504 opts.onAfter(); 505 } 506 } 507 508 function getActiveWizardStepName() { 509 if (state.currentStep) { 510 return state.currentStep; 511 } 512 return getFlowStepName(getStepName($form.find('.new-onboarding__step.is-active').first())); 513 } 514 515 function goToNextStep(sourceStepName, options) { 516 const stepName = sourceStepName || getActiveWizardStepName(); 517 if (!stepName) { 518 return; 519 } 520 const idx = state.flow.indexOf(stepName); 521 if (idx < 0 || idx + 1 >= state.flow.length) { 522 return; 523 } 524 const showOpts = Object.assign({ forceScroll: true }, options || {}); 525 showStep(state.flow[idx + 1], showOpts); 526 } 527 528 function advanceActiveWizardStep() { 529 goToNextStep(getActiveWizardStepName()); 530 } 531 532 function getWizardStepNameFromElement($el) { 533 return getFlowStepName(getStepName($el.closest('.new-onboarding__step'))); 534 } 535 536 function advanceWizardFromButton($btn) { 537 const stepName = getWizardStepNameFromElement($btn); 538 if (!stepName) { 539 return; 540 } 541 goToNextStep(stepName, { forceScroll: true }); 542 } 543 544 function canUseWizardNext($btn) { 545 return state.flow.indexOf(getWizardStepNameFromElement($btn)) >= 0; 546 } 547 548 function goToPrevStep(sourceStepName) { 549 const idx = resolveStepIndex(sourceStepName); 550 if (idx <= 0) { 551 return; 552 } 553 showStep(state.flow[idx - 1], { reveal: false }); 554 } 555 556 function goBackFromVerifyToCompanyEmail() { 557 const idx = state.flow.indexOf('verify'); 558 if (idx <= 0) { 559 return; 560 } 561 $('.verify-digit').prop('disabled', false).val(''); 562 delete state.seenSteps['verify']; 563 showStep(state.flow[idx - 1], { reveal: false, focusFirstField: false, forceScroll: true }); 564 focusWizardField($('.js-company-email-input').first()); 565 } 566 567 function setQuoteSuccessLayout(active) { 568 $root.toggleClass('is-quote-success-mode', !!active); 569 } 570 571 function showQuoteSuccessStep() { 572 // Slide-in panel: hide the form, reveal the success message in place (sitewide-style). 573 $('#onboarding-quote-form-content').hide(); 574 $('#onboarding-quote-success').removeAttr('hidden'); 575 } 576 577 function clearTopMenuTransitionAnimation() { 578 if (menuTransitionAnimationTimer) { 579 window.clearTimeout(menuTransitionAnimationTimer); 580 menuTransitionAnimationTimer = null; 581 } 582 583 $body.removeClass('new-onboarding-menu-hiding new-onboarding-menu-restoring'); 584 } 585 586 function prepareWizardTopMenu(options) { 587 const opts = options || {}; 588 const animate = opts.animate === true && !prefersReducedMotion(); 589 590 clearTopMenuTransitionAnimation(); 591 592 if (!animate) { 593 $body.addClass('new-onboarding-wizard-mode'); 594 return; 595 } 596 597 $body.removeClass('new-onboarding-wizard-mode'); 598 void document.body.offsetWidth; 599 $body.addClass('new-onboarding-menu-hiding'); 600 menuTransitionAnimationTimer = window.setTimeout(function () { 601 $body.addClass('new-onboarding-wizard-mode'); 602 $body.removeClass('new-onboarding-menu-hiding'); 603 menuTransitionAnimationTimer = null; 604 }, MENU_TRANSITION_ANIMATION_MS); 605 } 606 607 function restoreLandingTopMenu(options) { 608 const opts = options || {}; 609 const animate = opts.animate === true && !prefersReducedMotion(); 610 611 clearTopMenuTransitionAnimation(); 612 $body.removeClass('new-onboarding-wizard-mode'); 613 614 if (!animate) { 615 return; 616 } 617 618 void document.body.offsetWidth; 619 $body.addClass('new-onboarding-menu-restoring'); 620 menuTransitionAnimationTimer = window.setTimeout(function () { 621 $body.removeClass('new-onboarding-menu-restoring'); 622 menuTransitionAnimationTimer = null; 623 }, MENU_TRANSITION_ANIMATION_MS); 624 } 625 626 function syncChoiceButtons(selector, value) { 627 const $buttons = $(selector); 628 $buttons.removeClass('is-selected').attr('aria-pressed', 'false'); 629 if (!value) { 630 return; 631 } 632 $buttons.filter('[data-value="' + value + '"]').addClass('is-selected').attr('aria-pressed', 'true'); 633 } 634 635 function syncSubscriptionChoices() { 636 const period = $('#subscriptionPeriod').val(); 637 const $buttons = $('.js-subscription-choice'); 638 $buttons.removeClass('is-selected').attr('aria-pressed', 'false'); 639 if (!period) { 640 return; 641 } 642 $buttons.filter('[data-period="' + period + '"]').addClass('is-selected').attr('aria-pressed', 'true'); 643 } 644 645 function getSubscriptionUsersCount(forCalculation) { 646 const $input = findStepElement('subscription').find('.js-subscription-users-input'); 647 const raw = (($input.length ? $input.val() : $('.js-subscription-users-input').val()) || '').trim(); 648 const users = parseInt(raw, 10); 649 if (!users || users < 1) { 650 if (forCalculation) { 651 const fallback = parseInt($('#UsersNum').val(), 10) || state.selectedUsers || 1; 652 return fallback > 0 ? fallback : 1; 653 } 654 return 0; 655 } 656 return users; 657 } 658 659 function updateSubscriptionUsersPrice() { 660 const $price = $('.js-subscription-users-price'); 661 if (!$price.length || getSubscriptionPeriod() !== 'annually') { 662 if ($price.length) { 663 $price.text(''); 664 } 665 return; 666 } 667 668 const users = getSubscriptionUsersCount(true); 669 const planDetails = getSelectedPlanDetails(); 670 if (!planDetails || !planDetails.Plans || !planDetails.Plans[0]) { 671 $price.text(''); 672 return; 673 } 674 675 const currency = resolvePlanCurrency(planDetails); 676 const pricePerUser = resolvePrepaidPlanPricePerUser(planDetails); 677 if (pricePerUser === null) { 678 $price.text(''); 679 return; 680 } 681 682 const annualTotal = pricePerUser * users * 12; 683 const formatted = formatPlanPrice(annualTotal, currency); 684 const perYear = getI18n('quotePricePerYear', '/year'); 685 $price.text(formatted + ' ' + perYear); 686 } 687 688 function getSubscriptionPeriod() { 689 const fromField = String($('#subscriptionPeriod').val() || '').trim(); 690 if (fromField) { 691 return fromField; 692 } 693 694 const $selected = findStepElement('subscription') 695 .find('.js-subscription-choice.is-selected') 696 .first(); 697 return String($selected.attr('data-period') || '').trim(); 698 } 699 700 function setSubscriptionUsersWrapOpen(isOpen) { 701 const $wrap = findStepElement('subscription').find('.js-subscription-users-wrap'); 702 if (!$wrap.length) { 703 return; 704 } 705 706 $wrap 707 .toggleClass('is-open', isOpen) 708 .removeClass('is-hidden') 709 .attr('aria-hidden', isOpen ? 'false' : 'true'); 710 711 if (!isOpen) { 712 return; 713 } 714 715 const fallbackUsers = parseInt($('#UsersNum').val(), 10) || state.selectedUsers || 20; 716 const $input = $wrap.find('.js-subscription-users-input'); 717 if ($input.length && !(($input.val() || '').trim())) { 718 $input.val(String(fallbackUsers > 0 ? fallbackUsers : 20)); 719 } 720 updateSubscriptionUsersPrice(); 721 } 722 723 function syncSubscriptionUsersWrap() { 724 setSubscriptionUsersWrapOpen(getSubscriptionPeriod() === 'annually'); 725 } 726 727 function commitSubscriptionUsersToForm() { 728 const users = getSubscriptionUsersCount(true); 729 if (!users || users < 1) { 730 return false;
731 } 732 733 state.selectedUsers = users; 734 $('#UsersNum').val(String(users)); 735 if (updatePlanHiddenFields() === false) { 736 return false; 737 } 738 renderSummary(); 739 return true; 740 } 741 742 function setDesktopUsersWrapOpen(selector, isOpen) { 743 const $wrap = $(selector); 744 if (!$wrap.length) { 745 return; 746 } 747 748 $wrap 749 .toggleClass('is-open', isOpen) 750 .removeClass('is-hidden') 751 .attr('aria-hidden', isOpen ? 'false' : 'true'); 752 } 753 754 function syncDesktopInputs() { 755 const desktopClientEnabled = $('#optional_desktop_suite').val() === 'included'; 756 const desktopOfficeEnabled = $('#optional_ai_assistant').val() === 'included'; 757 const isAnnual = getSubscriptionPeriod() === 'annually'; 758 759 setDesktopUsersWrapOpen('.js-desktop-client-users-wrap', desktopClientEnabled && isAnnual); 760 setDesktopUsersWrapOpen('.js-desktop-office-users-wrap', desktopOfficeEnabled && isAnnual); 761 762 const clientUsers = $('#optional_desktop_suite_users').val(); 763 const officeUsers = $('#optional_ai_assistant_users').val(); 764 $('.js-desktop-client-users').val(clientUsers !== '0' ? clientUsers : '10'); 765 $('.js-desktop-office-users').val(officeUsers !== '0' ? officeUsers : '10'); 766 767 if (desktopClientEnabled && isAnnual) { 768 updateDesktopClientUsersPrice(); 769 } 770 if (desktopOfficeEnabled && isAnnual) { 771 updateDesktopOfficeUsersPrice(); 772 } 773 } 774 775 function syncChoices() { 776 syncSubscriptionChoices(); 777 syncSubscriptionUsersWrap(); 778 syncChoiceButtons('.js-mobile-sync-choice', $('#optional_mobile_sync').val()); 779 syncChoiceButtons('.js-support-choice', $('#support_option').val()); 780 syncChoiceButtons('.js-desktop-client-choice', $('#optional_desktop_suite').val()); 781 syncChoiceButtons('.js-desktop-office-choice', $('#optional_ai_assistant').val()); 782 syncDesktopInputs(); 783 } 784 785 function getAnnualSummaryAddonsSubtotal(useListPrice) { 786 let addonsSubtotal = 0; 787 788 if (state.flow.indexOf('mobile_sync') !== -1 && $('#optional_mobile_sync').val() === 'included') { 789 addonsSubtotal += MOBILE_SYNC_MONTHLY_PRICE * 12; 790 } 791 792 if (state.flow.indexOf('support') !== -1 && $('#support_option').val() === 'plus') { 793 addonsSubtotal += SUPPORT_PLUS_MONTHLY_PRICE * 12; 794 } 795 796 if (state.flow.indexOf('desktop_client') !== -1 && $('#optional_desktop_suite').val() === 'included') { 797 const lineTotal = getWizardAddonLineTotal(getWizardDesktopClientUsersCount(), 'client', useListPrice); 798 if (lineTotal !== null) { 799 addonsSubtotal += lineTotal; 800 } 801 } 802 803 if (state.flow.indexOf('desktop_office') !== -1 && $('#optional_ai_assistant').val() === 'included') { 804 const lineTotal = getWizardAddonLineTotal(getWizardDesktopOfficeUsersCount(), 'office', useListPrice); 805 if (lineTotal !== null) { 806 addonsSubtotal += lineTotal; 807 } 808 } 809 810 return addonsSubtotal; 811 } 812 813 function getAnnualSubscriptionPricing() { 814 if (getSubscriptionPeriod() !== 'annually') { 815 return null; 816 } 817 818 const planDetails = getSelectedPlanDetails(); 819 if (!planDetails || !planDetails.Plans || !planDetails.Plans[0]) { 820 return null; 821 } 822 823 const standardMonthly = resolvePlanPricePerUser(planDetails); 824 if (standardMonthly === null) { 825 return null; 826 } 827 828 const users = getSubscriptionUsersCount(true); 829 const pricePerUserYear = standardMonthly * 12; 830 const coreSubtotal = pricePerUserYear * users; 831 const addonsSubtotal = getAnnualSummaryAddonsSubtotal(true); 832 const subtotal = coreSubtotal + addonsSubtotal; 833 834 const apiPrepaidPerUser = resolvePrepaidPlanPricePerUser(planDetails); 835 const prepaidPerUser = apiPrepaidPerUser === null 836 ? standardMonthly * (1 - ANNUAL_SUBSCRIPTION_DISCOUNT_RATE) 837 : apiPrepaidPerUser; 838 const corePrepaidSubtotal = prepaidPerUser * 12 * users; 839 const totalPerYear = corePrepaidSubtotal + getAnnualSummaryAddonsSubtotal(false); 840 const discountAmount = subtotal - totalPerYear; 841 842 return { 843 currency: resolvePlanCurrency(planDetails), 844 users: users, 845 pricePerUserYear: pricePerUserYear, 846 coreSubtotal: coreSubtotal, 847 addonsSubtotal: addonsSubtotal, 848 subtotal: subtotal, 849 discountAmount: discountAmount, 850 totalPerYear: totalPerYear, 851 }; 852 } 853 854 function buildSummaryRows() { 855 const rows = []; 856 const period = getSubscriptionPeriod(); 857 858 if (state.flow.indexOf('subscription') !== -1 && period) { 859 if (period === 'annually') { 860 rows.push({ 861 title: getI18n('summarySubscription', 'Subscription'), 862 value: getI18n('summaryAnnually', 'Annually'), 863 badge: getI18n('summaryDiscountBadge', '10% Off'), 864 action: 'change', 865 step: 'subscription', 866 }); 867 868 const annualPricing = getAnnualSubscriptionPricing(); 869 if (annualPricing) { 870 const usersLabel = getI18n('summaryUsersCount', '{users} users').replace( 871 '{users}', 872 String(annualPricing.users) 873 ); 874 const perYear = getI18n('summaryPricePerYear', '/year'); 875 rows.push({ 876 title: getI18n('summaryCoreUsers', 'Core users'), 877 value: usersLabel, 878 price: formatPlanPrice(annualPricing.coreSubtotal, annualPricing.currency) + ' ' + perYear, 879 action: 'edit', 880 step: 'subscription', 881 }); 882 } 883 } else { 884 rows.push({ 885 title: getI18n('summarySubscription', 'Subscription'), 886 value: getI18n('summaryMonthly', 'Monthly'), 887 action: 'change', 888 step: 'subscription', 889 }); 890 } 891 } 892 893 if (state.flow.indexOf('mobile_sync') !== -1 && $('#optional_mobile_sync').val() === 'included') { 894 rows.push({ 895 title: 'Mobile Sync', 896 value: '$500.00 /mo', 897 action: 'remove', 898 remove: 'mobile_sync', 899 }); 900 } 901 902 if (state.flow.indexOf('support') !== -1 && $('#support_option').val() === 'plus') { 903 rows.push({ 904 title: 'Cloud Plus Support', 905 value: '$200.00 /mo', 906 action: 'remove',
907 remove: 'support', 908 }); 909 } 910 911 if (state.flow.indexOf('desktop_client') !== -1 && $('#optional_desktop_suite').val() === 'included') { 912 const desktopClientUsers = getWizardDesktopClientUsersCount(); 913 rows.push({ 914 title: 'Desktop Client', 915 value: period === 'annually' ? desktopClientUsers + ' users' : getI18n('planPerUserMonth', 'per user /month'), 916 price: period === 'annually' 917 ? formatWizardAddonSummaryPrice(desktopClientUsers, 'client') 918 : formatWizardAddonPerUserSummaryPrice('client'), 919 action: 'remove', 920 remove: 'desktop_client', 921 }); 922 } 923 924 if (state.flow.indexOf('desktop_office') !== -1 && $('#optional_ai_assistant').val() === 'included') { 925 const desktopOfficeUsers = getWizardDesktopOfficeUsersCount(); 926 rows.push({ 927 title: 'Desktop Office', 928 value: period === 'annually' ? desktopOfficeUsers + ' users' : getI18n('planPerUserMonth', 'per user /month'), 929 price: period === 'annually' 930 ? formatWizardAddonSummaryPrice(desktopOfficeUsers, 'office') 931 : formatWizardAddonPerUserSummaryPrice('office'), 932 action: 'remove', 933 remove: 'desktop_office', 934 }); 935 } 936 937 return rows; 938 } 939 940 const FEATURE_ICON_BASE = '/img/redesign/'; 941 942 function renderFeaturesPopup() { 943 const $trigger = $('.new-onboarding__features-trigger'); 944 const $title = $('#summary-features-popup-title'); 945 const $includes = $('#summary-features-popup-includes'); 946 const $list = $('#summary-features-popup-list'); 947 if (!$trigger.length || !$list.length) { 948 return; 949 } 950 951 $trigger.removeClass('is-dismissed is-pinned'); 952 953 const selectedPackage = normalizePackageName(state.selectedPackage); 954 const planFeaturesPopup = (window.newOnboardingConfig && window.newOnboardingConfig.planFeaturesPopup) || {}; 955 const planData = planFeaturesPopup[selectedPackage]; 956 957 if (!planData || !planData.items || !planData.items.length) { 958 $trigger.addClass('is-empty'); 959 $list.empty(); 960 if ($includes.length) { 961 $includes.empty().prop('hidden', true); 962 } 963 return; 964 } 965 966 $trigger.removeClass('is-empty'); 967 968 const planName = state.selectedPlanName || state.selectedPackage; 969 const titleTemplate = getI18n('featuresPopupTitle', '%plan% features'); 970 $title.text(titleTemplate.replace('%plan%', planName)); 971 972 if ($includes.length) { 973 const includesText = (planData.includes || '').trim(); 974 if (includesText) { 975 $includes.text(includesText).prop('hidden', false); 976 } else { 977 $includes.empty().prop('hidden', true); 978 } 979 } 980 981 const mode = $landing.attr('data-current-mode') === 'self-hosted' ? 'self-hosted' : 'cloud'; 982 $list.empty(); 983 984 planData.items.forEach(function (item) { 985 if (item.cloudOnly && mode === 'self-hosted') { 986 return; 987 } 988 989 const $li = $('<li class="new-onboarding__features-popup-item"></li>'); 990 const isCheckIcon = item.iconType === 'check'; 991 const $icon = $('<img>', { 992 class: isCheckIcon 993 ? 'new-onboarding__features-popup-icon is-check' 994 : 'new-onboarding__features-popup-icon', 995 src: FEATURE_ICON_BASE + item.iconSrc, 996 alt: '', 997 width: isCheckIcon ? 14 : 24, 998 height: isCheckIcon ? 11 : 24, 999 }); 1000 1001 const $label = $('<span class="new-onboarding__features-popup-label"></span>'); 1002 if (item.hasModes) { 1003 $label.append( 1004 $('<span class="new-onboarding__compare-feature-mode new-onboarding__compare-feature-mode--cloud"></span>') 1005 .toggleClass('is-mode-active', mode === 'cloud') 1006 .html(item.labelCloud) 1007 ); 1008 $label.append( 1009 $('<span class="new-onboarding__compare-feature-mode new-onboarding__compare-feature-mode--self-hosted"></span>') 1010 .toggleClass('is-mode-active', mode === 'self-hosted') 1011 .html(item.labelSelfHosted) 1012 ); 1013 } else { 1014 $label.html(item.labelHtml); 1015 } 1016 1017 $li.append($icon, $label); 1018 $list.append($li); 1019 }); 1020 } 1021 1022 function formatSummaryPlanPrice() { 1023 const period = getSubscriptionPeriod(); 1024 if (!period) { 1025 return ''; 1026 } 1027 1028 const planDetails = getSelectedPlanDetails(); 1029 const isAnnual = period === 'annually'; 1030 1031 if (!planDetails || !planDetails.Plans || !planDetails.Plans[0]) { 1032 if (!state.selectedPlanPrice) { 1033 return ''; 1034 } 1035 const suffix = isAnnual 1036 ? getI18n('planPerUserYear', 'per user /year') 1037 : getI18n('planPerUserMonth', 'per user /month'); 1038 return state.selectedPlanPrice + ' ' + suffix; 1039 } 1040 1041 const currency = resolvePlanCurrency(planDetails); 1042 const monthlyPrice = resolvePlanPricePerUser(planDetails); 1043 1044 if (isAnnual) { 1045 if (monthlyPrice === null) { 1046 return ''; 1047 } 1048 return ( 1049 formatPlanPrice((resolvePrepaidPlanPricePerUser(planDetails) || monthlyPrice) * 12, currency) + 1050 ' ' + 1051 getI18n('planPerUserYear', 'per user /year') 1052 ); 1053 } 1054 1055 if (monthlyPrice === null) { 1056 return ''; 1057 } 1058 1059 return formatPlanPrice(monthlyPrice, currency) + ' ' + getI18n('planPerUserMonth', 'per user /month'); 1060 } 1061
1062 function renderSummaryTotals() { 1063 const $totals = $('#summary-totals'); 1064 const $subtotal = $('#summary-total-subtotal'); 1065 const $discount = $('#summary-total-discount'); 1066 const $perYear = $('#summary-total-per-year'); 1067 1068 if (!$totals.length || !$subtotal.length || !$discount.length || !$perYear.length) { 1069 return; 1070 } 1071 1072 const annualPricing = getAnnualSubscriptionPricing(); 1073 if (!annualPricing) { 1074 $totals.addClass('is-hidden'); 1075 $subtotal.text(''); 1076 $discount.text(''); 1077 $perYear.text(''); 1078 return; 1079 } 1080 1081 const currency = annualPricing.currency; 1082 $subtotal.text(formatPlanPrice(annualPricing.subtotal, currency)); 1083 $discount.text(formatPlanPrice(annualPricing.discountAmount, currency)); 1084 $perYear.text(formatPlanPrice(annualPricing.totalPerYear, currency)); 1085 $totals.removeClass('is-hidden'); 1086 } 1087 1088 function renderSummary() { 1089 $('#summary-plan-name').text(state.selectedPlanName || state.selectedPackage); 1090 $('#summary-plan-price').text(formatSummaryPlanPrice()); 1091 renderFeaturesPopup(); 1092 1093 const $rows = $('#summary-rows'); 1094 if ($rows.length) { 1095 $rows.empty(); 1096 buildSummaryRows().forEach(function (row) { 1097 const $row = $('<div class="new-onboarding__summary-row"></div>'); 1098 1099 const $main = $('<div class="new-onboarding__summary-row-main"></div>'); 1100 $('<div class="new-onboarding__summary-row-title"></div>').text(row.title).appendTo($main); 1101 1102 const $value = $('<div class="new-onboarding__summary-row-value"></div>'); 1103 $('<span class="new-onboarding__summary-row-value-text"></span>').text(row.value).appendTo($value); 1104 if (row.badge) { 1105 $('<span class="new-onboarding__summary-discount-badge"></span>').text(row.badge).appendTo($value); 1106 } 1107 $value.appendTo($main); 1108 1109 if (row.price) { 1110 $('<div class="new-onboarding__summary-row-price"></div>').text(row.price).appendTo($main); 1111 } 1112 $row.append($main); 1113 1114 if (row.action === 'change') { 1115 $('<button type="button" class="new-onboarding__summary-change js-summary-change"></button>') 1116 .text(getI18n('summaryChange', 'Change')) 1117 .attr('data-step', row.step) 1118 .appendTo($row); 1119 } else if (row.action === 'edit') { 1120 $('<button type="button" class="new-onboarding__summary-edit js-summary-edit"></button>') 1121 .attr('data-step', row.step) 1122 .attr('aria-label', getI18n('summaryEditUsers', 'Edit number of users')) 1123 .appendTo($row); 1124 } else if (row.action === 'remove') { 1125 $('<button type="button" class="new-onboarding__summary-remove js-summary-remove" aria-label="Remove"></button>') 1126 .attr('data-remove', row.remove) 1127 .appendTo($row); 1128 } 1129 1130 $rows.append($row); 1131 }); 1132 } 1133 1134 renderSummaryTotals(); 1135 } 1136 1137 function removeSummaryItem(type) { 1138 if (type === 'mobile_sync') { 1139 $('#optional_mobile_sync').val('none'); 1140 } else if (type === 'support') { 1141 $('#support_option').val('standard'); 1142 } else if (type === 'desktop_client') { 1143 $('#optional_desktop_suite').val('none'); 1144 $('#optional_desktop_suite_users').val('0'); 1145 $('#DesktopNum').val('0'); 1146 } else if (type === 'desktop_office') { 1147 $('#optional_ai_assistant').val('none'); 1148 $('#optional_ai_assistant_users').val('0'); 1149 } 1150 1151 syncChoices(); 1152 renderSummary(); 1153 } 1154 1155 // const CURRENCY_RATES = { CZK: 25, ZAR: 20 }; 1156 1157 function convertFromBaseCurrency(amount, currencyCode) { 1158 // CZK/ZAR temporarily disabled â CZK uses EUR, ZAR uses USD 1159 // const rate = CURRENCY_RATES[currencyCode]; 1160 // return rate ? amount * rate : amount; 1161 return amount; 1162 } 1163 1164 function formatPlanPrice(amount, currencyCode) { 1165 const numeric = Number(amount); 1166 if (!Number.isFinite(numeric)) { 1167 return ''; 1168 } 1169 1170 const display = convertFromBaseCurrency(numeric, currencyCode).toFixed(2); 1171 if (currencyCode === 'CZK') { 1172 return display + ' KÄ'; 1173 } 1174 if (currencyCode === 'EUR') {
1175 return display + 'â¬'; 1176 } 1177 if (currencyCode === 'ZAR') { 1178 return display + ' ZAR'; 1179 } 1180 return '$' + display; 1181 } 1182 1183 function getOnboardingMode() { 1184 const mode = String($landing.attr('data-current-mode') || 'cloud').trim(); 1185 return mode === 'self-hosted' ? 'self-hosted' : 'cloud'; 1186 } 1187 1188 function getBillingDcId() { 1189 if (getOnboardingMode() !== 'self-hosted') { 1190 return $('#dc').val(); 1191 } 1192 1193 const saasBillingDcId = parseInt(config.saasBillingDcId, 10); 1194 return Number.isFinite(saasBillingDcId) ? saasBillingDcId : 0; 1195 } 1196 1197 const SUPPORTED_DISPLAY_CURRENCIES = ['USD', 'EUR', 'CZK', 'ZAR']; 1198 1199 function normalizeDisplayCurrency(currencyCode) { 1200 return SUPPORTED_DISPLAY_CURRENCIES.indexOf(currencyCode) !== -1 ? currencyCode : 'USD'; 1201 } 1202 1203 function resolveSelectedDcCurrency() { 1204 const dcData = getCloudPlansForDc($('#dc').val()); 1205 return dcData && dcData.Currency ? dcData.Currency : null; 1206 } 1207 1208 function resolvePlanCurrency(planDetails) { 1209 const apiCurrency = 1210 (planDetails && planDetails.Currency) || 1211 (planDetails && planDetails.CurrencyCode) || 1212 (planDetails && planDetails.Plans && planDetails.Plans[0] && planDetails.Plans[0].CurrencyCode) || 1213 resolveSelectedDcCurrency() || 1214 'USD'; 1215 return normalizeDisplayCurrency(apiCurrency); 1216 } 1217 1218 function resolvePlanPricePerUser(planDetails, uiPackageName) { 1219 const plan = getPlanByPackage(planDetails, uiPackageName || state.selectedPackage); 1220 if (!plan) { 1221 return null; 1222 } 1223 1224 const price = Number(plan.PricePerUser); 1225 return Number.isFinite(price) ? price : null; 1226 } 1227 1228 function resolvePrepaidPlanPricePerUser(planDetails, uiPackageName) { 1229 const plan = getPlanByPackage(planDetails, uiPackageName || state.selectedPackage); 1230 if (!plan) { 1231 return null; 1232 } 1233 1234 const prepaidPrice = Number(plan.PrepaidPricePerUser); 1235 return Number.isFinite(prepaidPrice) ? prepaidPrice : null; 1236 } 1237 1238 function getCloudPlansForDc(dcId) { 1239 let dcData = null; 1240 1241 $.each(config.cloudPlans || [], function () { 1242 if (parseInt(this.Dc, 10) === parseInt(dcId, 10)) { 1243 dcData = this; 1244 return false; 1245 } 1246 }); 1247 1248 return dcData; 1249 } 1250 1251 function getSaasGroup() { 1252 return config.saasPlan || null; 1253 } 1254 1255 function getPlanByPackage(group, uiPackageName) { 1256 const packageName = normalizePackageName(uiPackageName); 1257 const planNames = { 1258 CORE: 'CORE', 1259 ESSENTIAL: 'ESSENTIAL', 1260 BUSINESS: 'BUSINESS', 1261 }; 1262 const expectedName = planNames[packageName]; 1263 1264 if (!group || !expectedName) { 1265 return null; 1266 } 1267 1268 return (group.Plans || []).find(function (plan) { 1269 return String(plan.Name || '').toUpperCase() === expectedName; 1270 }) || null; 1271 } 1272 1273 function getDesktopPlan(group, uiPackageName) { 1274 const packageName = normalizePackageName(uiPackageName); 1275 const expectedName = packageName === 'ESSENTIAL' 1276 ? 'ESSENTIAL WITH DESKTOP CLIENT' 1277 : packageName === 'BUSINESS' ? 'BUSINESS WITH DESKTOP CLIENT' : ''; 1278 1279 if (!group || !expectedName) { 1280 return null; 1281 } 1282 1283 return (group.Plans || []).find(function (plan) { 1284 return String(plan.Name || '').toUpperCase() === expectedName; 1285 }) || null; 1286 } 1287 1288 function getSelectedPlanDetails() { 1289 const dcId = $('#dc').val(); 1290 1291 if (getOnboardingMode() === 'self-hosted') { 1292 return getSaasGroup(); 1293 } 1294 1295 return getCloudPlansForDc(dcId); 1296 } 1297 1298 function getWizardAddonLineTotal(users, addonType, useListPrice) { 1299 if (!users || users < 1) { 1300 return null; 1301 } 1302 1303 const isAnnual = getSubscriptionPeriod() === 'annually'; 1304 const pricePerUser = getWizardAddonPricePerUser(addonType, useListPrice); 1305 if (pricePerUser === null) { 1306 return null; 1307 } 1308 1309 const multiplier = isAnnual ? 12 : 1; 1310 return pricePerUser * users * multiplier; 1311 } 1312 1313 function getWizardAddonPricePerUser(addonType, useListPrice) { 1314 if (addonType !== 'client') { 1315 return null; 1316 } 1317 1318 const group = getSelectedPlanDetails(); 1319 const basePlan = getPlanByPackage(group, state.selectedPackage); 1320 const desktopPlan = getDesktopPlan(group, state.selectedPackage); 1321 if (!basePlan || !desktopPlan) { 1322 return null; 1323 } 1324 1325 const priceField = getSubscriptionPeriod() === 'annually' && !useListPrice 1326 ? 'PrepaidPricePerUser' 1327 : 'PricePerUser'; 1328 const basePrice = Number(basePlan[priceField]); 1329 const desktopPrice = Number(desktopPlan[priceField]); 1330 return Number.isFinite(basePrice) && Number.isFinite(desktopPrice) 1331 ? desktopPrice - basePrice 1332 : null; 1333 } 1334 1335 function getWizardDesktopClientUsersCount() { 1336 const hidden = parseInt($('#optional_desktop_suite_users').val(), 10); 1337 if (hidden > 0) { 1338 return hidden; 1339 } 1340 const input = parseInt(($('.js-desktop-client-users').val() || '').trim(), 10); 1341 return input > 0 ? input : 0; 1342 } 1343 1344 function getWizardDesktopOfficeUsersCount() { 1345 const hidden = parseInt($('#optional_ai_assistant_users').val(), 10); 1346 if (hidden > 0) { 1347 return hidden; 1348 } 1349 const input = parseInt(($('.js-desktop-office-users').val() || '').trim(), 10); 1350 return input > 0 ? input : 0; 1351 } 1352 1353 function formatWizardAddonSummaryPrice(users, addonType) { 1354 const total = getWizardAddonLineTotal(users, addonType); 1355 if (total === null || !users) { 1356 return ''; 1357 } 1358 1359 const planDetails = getSelectedPlanDetails(); 1360 const currency = resolvePlanCurrency(planDetails); 1361 const isAnnual = getSubscriptionPeriod() === 'annually'; 1362 const periodSuffix = isAnnual 1363 ? getI18n('quotePricePerYear', '/year') 1364 : getI18n('summaryPricePerMonth', '/month'); 1365 1366 return formatPlanPrice(total, currency) + ' ' + periodSuffix; 1367 } 1368 1369 function formatWizardAddonPerUserSummaryPrice(addonType) { 1370 const pricePerUser = getWizardAddonPricePerUser(addonType); 1371 if (pricePerUser === null) { 1372 return ''; 1373 } 1374 1375 const planDetails = getSelectedPlanDetails(); 1376 const currency = resolvePlanCurrency(planDetails); 1377 return formatPlanPrice(pricePerUser, currency); 1378 } 1379 1380 function updateDesktopClientUsersPrice() { 1381 const $price = $('.js-desktop-client-users-price'); 1382 if (!$price.length) { 1383 return; 1384 } 1385 1386 const users = parseInt(($('.js-desktop-client-users').val() || '').trim(), 10); 1387 if (!users || users < 1) { 1388 $price.text(''); 1389 return; 1390 } 1391 1392 const planDetails = getSelectedPlanDetails(); 1393 const desktopPlan = getDesktopPlan(planDetails, state.selectedPackage); 1394 if (desktopPlan) { 1395 $('#PlanDesktopId').val(desktopPlan.PlanID || '0'); 1396 } 1397 1398 const total = getWizardAddonLineTotal(users, 'client'); 1399 if (total === null) { 1400 $price.text(''); 1401 return; 1402 } 1403 1404 const currency = resolvePlanCurrency(planDetails); 1405 $price.text(formatPlanPrice(total, currency)); 1406 } 1407 1408 function updateDesktopOfficeUsersPrice() { 1409 const $price = $('.js-desktop-office-users-price'); 1410 if (!$price.length) { 1411 return; 1412 } 1413 1414 const users = parseInt(($('.js-desktop-office-users').val() || '').trim(), 10); 1415 if (!users || users < 1) { 1416 $price.text(''); 1417 return; 1418 } 1419 1420 const planDetails = getSelectedPlanDetails(); 1421 const total = getWizardAddonLineTotal(users, 'office'); 1422 if (total === null) { 1423 $price.text(''); 1424 return; 1425 } 1426 1427 const currency = resolvePlanCurrency(planDetails); 1428 $price.text(formatPlanPrice(total, currency)); 1429 } 1430 1431 function updatePlanCardPrices(dcId) { 1432 const selfHosted = getOnboardingMode() === 'self-hosted'; 1433 const group = selfHosted ? getSaasGroup() : getCloudPlansForDc(dcId); 1434 if (!group) { 1435 return; 1436 } 1437 1438 const currency = resolvePlanCurrency(group); 1439 const prices = {}; 1440 ['CORE', 'ESSENTIAL', 'BUSINESS'].forEach(function (packageName) { 1441 const plan = getPlanByPackage(group, packageName); 1442 if (!plan) { 1443 prices[packageName] = { main: '', annual: '' }; 1444 return; 1445 } 1446 1447 const mainPrice = Number(plan[selfHosted ? 'PrepaidPricePerUser' : 'PricePerUser']); 1448 const annualPrice = Number(plan.PrepaidPricePerUser); 1449 prices[packageName] = { 1450 main: formatPlanPrice(mainPrice * (selfHosted ? 12 : 1), currency), 1451 annual: formatPlanPrice(annualPrice, currency), 1452 }; 1453 }); 1454 1455 $('.new-onboarding__plan-card').each(function () { 1456 const packageName = String($(this).find('.js-start-onboarding').data('package') || '').toUpperCase(); 1457 if (prices[packageName]) { 1458 $(this).find('.new-onboarding__plan-price').first().text(prices[packageName].main); 1459 $(this).find('.js-plan-annual-price').first().text(prices[packageName].annual); 1460 } 1461 }); 1462 1463 syncCompareHeaderPricesFromCards(); 1464 $(window).trigger('resize.compareTableHeader'); 1465 1466 if (state.isQuoteRequest) { 1467 updateQuoteUsersPrice(); 1468 } 1469 1470 updateDesktopClientUsersPrice(); 1471 updateDesktopOfficeUsersPrice(); 1472 1473 if (state.selectedPackage) { 1474 const $selectedCard = $('.js-start-onboarding[data-package="' + state.selectedPackage + '"]') 1475 .closest('.new-onboarding__plan-card'); 1476 if ($selectedCard.length) { 1477 state.selectedPlanPrice = ($selectedCard.find('.new-onboarding__plan-price').first().text() || '').trim(); 1478 renderSummary(); 1479 } 1480 } 1481 } 1482 1483 function updatePlanHiddenFields() { 1484 const backendPackage = getBackendPackageName(state.selectedPackage); 1485 const dcId = $('#dc').val(); 1486 $('#dc_hidden').val(getBillingDcId()); 1487 $('#package').val(backendPackage); 1488 $('#UsersNum').val(String(state.selectedUsers)); 1489 1490 let planDetails = null; 1491 if (backendPackage === 'SAAS') { 1492 planDetails = getSaasGroup(); 1493 } else { 1494 planDetails = getCloudPlansForDc(dcId); 1495 } 1496 1497 if (!planDetails) { 1498 setFeedback('create-order-feedback', 'Unable to resolve selected plan for current datacenter.', true); 1499 return false;
1500 } 1501 1502 $('#GroupId').val(planDetails.GroupID || planDetails.groupId || ''); 1503 const selectedPlan = getPlanByPackage(planDetails, state.selectedPackage); 1504 $('#PlanId').val(selectedPlan ? selectedPlan.PlanID || '' : ''); 1505 return true; 1506 } 1507 1508 function resetOptionalValues() { 1509 $('#subscriptionPeriod').val(''); 1510 $('#optional_mobile_sync').val(''); 1511 $('#optional_desktop_suite').val(''); 1512 $('#optional_ai_assistant').val(''); 1513 $('#optional_desktop_suite_users').val('0'); 1514 $('#optional_ai_assistant_users').val('0'); 1515 $('#optional_webinar_mode').val(''); 1516 $('#support_option').val(''); 1517 $('#DesktopNum').val('0'); 1518 } 1519 1520 function getI18n(key, fallback) { 1521 return (config.i18n && config.i18n[key]) || fallback; 1522 } 1523 1524 function getResendCodeButton() { 1525 return $('#resend-code-button'); 1526 } 1527 1528 function getResendCodeLabel() { 1529 const $btn = getResendCodeButton(); 1530 const $label = $btn.find('.js-resend-label'); 1531 return $label.length ? $label : $btn; 1532 } 1533 1534 function isResendCodeCooldownActive() { 1535 return (state.resendCodeCooldownEndsAt || 0) > Date.now(); 1536 } 1537 1538 function clearResendCodeCooldown() { 1539 if (resendCodeCooldownTimer) { 1540 window.clearInterval(resendCodeCooldownTimer); 1541 resendCodeCooldownTimer = null; 1542 } 1543 state.resendCodeCooldownEndsAt = 0; 1544 const $btn = getResendCodeButton(); 1545 if (!$btn.length) { 1546 return; 1547 } 1548 $btn.prop('disabled', false).removeClass('is-cooldown'); 1549 const defaultLabel = $btn.data('default-label') || getI18n('resendCode', 'Resend code'); 1550 getResendCodeLabel().text(defaultLabel); 1551 } 1552 1553 function updateResendCodeCooldownUi() { 1554 const $btn = getResendCodeButton(); 1555 if (!$btn.length) { 1556 return; 1557 } 1558 1559 const remainingMs = (state.resendCodeCooldownEndsAt || 0) - Date.now(); 1560 if (remainingMs <= 0) { 1561 clearResendCodeCooldown(); 1562 return; 1563 } 1564 1565 if (!$btn.data('default-label')) { 1566 $btn.data('default-label', getResendCodeLabel().text().trim()); 1567 } 1568 1569 const seconds = Math.ceil(remainingMs / 1000); 1570 const template = getI18n('resendCodeCooldown', 'Resend code ({seconds}s)'); 1571 $btn.prop('disabled', true).addClass('is-cooldown'); 1572 getResendCodeLabel().text(template.replace('{seconds}', String(seconds))); 1573 } 1574 1575 function ensureResendCodeCooldownTicker() { 1576 if (!isResendCodeCooldownActive()) { 1577 return; 1578 } 1579 updateResendCodeCooldownUi(); 1580 if (resendCodeCooldownTimer) { 1581 return; 1582 } 1583 resendCodeCooldownTimer = window.setInterval(updateResendCodeCooldownUi, 1000); 1584 } 1585 1586 function startResendCodeCooldown() { 1587 state.resendCodeCooldownEndsAt = Date.now() + RESEND_CODE_COOLDOWN_SEC * 1000; 1588 ensureResendCodeCooldownTicker(); 1589 } 1590 1591 function syncQuoteNameToForm() { 1592 $('#name').val(($('.js-quote-name-input').val() || '').trim()); 1593 } 1594 1595 function syncCompanyNameToForm() { 1596 $('#name').val(($('.js-company-name-input').val() || '').trim()); 1597 } 1598 1599 function syncQuoteCompanyToForm() { 1600 const company = ($('.js-quote-company-input').val() || '').trim(); 1601 if (company) { 1602 $('#company').val(company); 1603 } else { 1604 const name = ($('.js-quote-name-input').val() || '').trim(); 1605 const parts = name.split(' '); 1606 $('#company').val(parts.length > 1 ? parts[parts.length - 1] : name); 1607 } 1608 } 1609 1610 function syncQuoteSolutionToForm() { 1611 const value = ($('.js-quote-solution-input:checked').val() || '').trim(); 1612 $('#HostingOption').val(value === 'self-hosted' ? 'self-hosted' : 'cloud'); 1613 } 1614 1615 function syncCloudCompanyToForm() { 1616 const company = ($('.js-company-company-input').val() || '').trim(); 1617 if (company) { 1618 $('#company').val(company); 1619 } else { 1620 const name = ($('.js-company-name-input').val() || '').trim(); 1621 const parts = name.split(' '); 1622 $('#company').val(parts.length > 1 ? parts[parts.length - 1] : name); 1623 } 1624 } 1625 1626 function syncQuoteContactToForm() { 1627 $('#email').val(($('.js-quote-email-input').val() || '').trim()); 1628 $('#phone').val(($('.js-quote-phone-input').val() || '').trim()); 1629 $('#phone_prefix').val(($('.js-quote-phone-prefix-input').val() || '').trim()); 1630 $('#country').val(($('.js-quote-country-input').val() || '').trim()); 1631 } 1632 1633 function syncCloudContactToForm() { 1634 $('#email').val(($('.js-company-email-input').val() || '').trim()); 1635 $('#phone').val(($('.js-company-phone-input').val() || '').trim()); 1636 $('#phone_prefix').val(($('.js-company-phone-prefix-input').val() || '').trim()); 1637 $('#country').val(($('.js-company-country-input').val() || '').trim()); 1638 } 1639 1640 function updateQuoteFlowFieldsVisibility() { 1641 // Company step is cloud-flow only; no field toggles needed for quote mode. 1642 } 1643 1644 function updateCompanyCountrySelectStyle() { 1645 const $country = $('.js-company-country-input'); 1646 if (!$country.length) { 1647 return; 1648 }
1649 $country.toggleClass('is-placeholder', !($country.val() || '').trim()); 1650 } 1651 1652 function syncCompanyStepForSubmit() { 1653 syncCompanyNameToForm(); 1654 syncCloudCompanyToForm(); 1655 syncCloudContactToForm(); 1656 } 1657 1658 const COMPANY_FIELD_VALIDATION = [ 1659 { 1660 key: 'company', 1661 companySelector: '.js-company-company-input', 1662 messageKey: 'companyFieldInvalid', 1663 fallback: 'Enter your company or name.', 1664 }, 1665 { 1666 key: 'country', 1667 companySelector: '.js-company-country-input', 1668 messageKey: 'countryFieldInvalid', 1669 fallback: 'Select your country.', 1670 }, 1671 { 1672 key: 'name', 1673 companySelector: '.js-company-name-input', 1674 messageKey: 'nameFieldRequired', 1675 fallback: 'Enter your full name.', 1676 }, 1677 { 1678 key: 'name_invalid', 1679 companySelector: '.js-company-name-input', 1680 messageKey: 'nameFieldInvalidFormat', 1681 fallback: 'Enter your full name (first and last name).', 1682 }, 1683 { 1684 key: 'email', 1685 companySelector: '.js-company-email-input', 1686 messageKey: 'emailFieldRequired', 1687 fallback: 'Enter your email.', 1688 }, 1689 { 1690 key: 'email_invalid', 1691 companySelector: '.js-company-email-input', 1692 messageKey: 'emailFieldInvalid', 1693 fallback: 'Please enter valid email.', 1694 }, 1695 { 1696 key: 'phone_prefix', 1697 companySelector: '.js-company-phone-prefix-input', 1698 messageKey: 'phonePrefixFieldInvalid', 1699 fallback: 'Select your phone prefix.', 1700 }, 1701 { 1702 key: 'phone', 1703 companySelector: '.js-company-phone-input', 1704 messageKey: 'phoneFieldInvalid', 1705 fallback: 'Please enter valid phone number.', 1706 }, 1707 ]; 1708 1709 function companyFieldConfig(key) { 1710 return COMPANY_FIELD_VALIDATION.find(function (item) { 1711 return item.key === key; 1712 }); 1713 } 1714 1715 function getCompanyFieldsScopeSelector() { 1716 return '.js-company-form-fields'; 1717 } 1718 1719 function resolveCompanyField(config) { 1720 return $(getCompanyFieldsScopeSelector()).find(config.companySelector).first(); 1721 } 1722 1723 const COMPANY_FIELD_ERROR_TRANSITION_MS = 340; 1724 1725 function revealCompanyFieldError($error) { 1726 if (!$error || !$error.length) { 1727 return; 1728 } 1729 1730 window.requestAnimationFrame(function () { 1731 window.requestAnimationFrame(function () { 1732 $error.addClass('is-visible'); 1733 }); 1734 }); 1735 } 1736 1737 function dismissCompanyFieldError($error) { 1738 return new Promise(function (resolve) { 1739 if (!$error || !$error.length) { 1740 resolve(); 1741 return; 1742 } 1743 1744 if (!$error.hasClass('is-visible')) { 1745 $error.remove(); 1746 resolve(); 1747 return; 1748 } 1749 1750 let settled = false; 1751 const finish = function () { 1752 if (settled) { 1753 return; 1754 } 1755 settled = true; 1756 $error.off('transitionend', onTransitionEnd); 1757 window.clearTimeout(fallbackTimer); 1758 $error.remove(); 1759 resolve(); 1760 }; 1761 1762 const onTransitionEnd = function (event) { 1763 if (event.target !== $error[0]) { 1764 return; 1765 } 1766 finish(); 1767 }; 1768 1769 $error.on('transitionend', onTransitionEnd); 1770 const fallbackTimer = window.setTimeout(finish, COMPANY_FIELD_ERROR_TRANSITION_MS + 60); 1771 $error.removeClass('is-visible'); 1772 }); 1773 } 1774 1775 function clearCompanyFieldErrors() { 1776 $('.js-company-form-fields') 1777 .find('.is-invalid') 1778 .removeClass('is-invalid'); 1779 1780 const $errors = $('.js-company-form-fields .new-onboarding__field-error'); 1781 if (!$errors.length) { 1782 setFeedback('create-order-feedback', ''); 1783 return; 1784 } 1785 1786 let remaining = $errors.length; 1787 const done = function () { 1788 remaining -= 1; 1789 if (remaining <= 0) { 1790 setFeedback('create-order-feedback', ''); 1791 } 1792 }; 1793 1794 $errors.each(function () { 1795 dismissCompanyFieldError($(this)).then(done); 1796 }); 1797 } 1798 1799 function setCompanyFieldError($field, message) { 1800 if (!$field || !$field.length) { 1801 return; 1802 } 1803 1804 $field.addClass('is-invalid'); 1805 const $wrap = $field.closest('.new-onboarding__company-field'); 1806 const $host = $wrap.length ? $wrap : $field.parent(); 1807 let $error = $host.find('.new-onboarding__field-error'); 1808 1809 if (!$error.length) { 1810 $error = $('<p class="new-onboarding__field-error" role="alert"></p>').appendTo($host); 1811 } 1812 1813 $error.text(message); 1814 revealCompanyFieldError($error); 1815 } 1816
1817 function focusFirstCompanyFieldError() { 1818 const $first = $(getCompanyFieldsScopeSelector()).find('.is-invalid').first(); 1819 if ($first.length) { 1820 focusWizardField($first); 1821 } 1822 } 1823 1824 function isCompanyEmailValid(email) { 1825 return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test(email); 1826 } 1827 1828 function isFullNameValid(name) { 1829 const value = (name || '').trim(); 1830 if (!value) { 1831 return false; 1832 } 1833 1834 try { 1835 return /\s+[\p{L}\p{M}]/u.test(value); 1836 } catch (e) { 1837 return /\s+[A-Za-zÃ-ÃÃ-öø-ÿ]/.test(value); 1838 } 1839 } 1840 1841 function findCompanyValidationConfigForField($field) { 1842 if (!$field || !$field.length) { 1843 return null; 1844 } 1845 1846 const fieldEl = $field[0]; 1847 let found = null; 1848 1849 COMPANY_FIELD_VALIDATION.some(function (config) { 1850 if (config.key === 'name_invalid' || config.key === 'email_invalid') { 1851 return false; 1852 } 1853 1854 const $inCompany = $('.js-company-form-fields').find(config.companySelector); 1855 1856 if ($inCompany.length && $inCompany[0] === fieldEl) { 1857 found = config; 1858 return true; 1859 } 1860 return false; 1861 }); 1862 1863 return found; 1864 } 1865 1866 function validateSingleCompanyField($field) { 1867 const config = findCompanyValidationConfigForField($field); 1868 if (!config) { 1869 clearCompanyFieldErrorForInput($field); 1870 return true; 1871 } 1872 1873 clearCompanyFieldErrorForInput($field); 1874 const value = ($field.val() || '').trim(); 1875 1876 if (config.key === 'name') { 1877 if (!value) { 1878 const nameConfig = companyFieldConfig('name'); 1879 setCompanyFieldError($field, getI18n(nameConfig.messageKey, nameConfig.fallback)); 1880 return false; 1881 } 1882 if (!isFullNameValid(value)) { 1883 const nameInvalidConfig = companyFieldConfig('name_invalid'); 1884 setCompanyFieldError($field, getI18n(nameInvalidConfig.messageKey, nameInvalidConfig.fallback)); 1885 return false; 1886 } 1887 return true; 1888 } 1889 1890 if (config.key === 'email') { 1891 if (!value) { 1892 const emailConfig = companyFieldConfig('email'); 1893 setCompanyFieldError($field, getI18n(emailConfig.messageKey, emailConfig.fallback)); 1894 return false; 1895 } 1896 if (!isCompanyEmailValid(value)) { 1897 const emailInvalidConfig = companyFieldConfig('email_invalid'); 1898 setCompanyFieldError($field, getI18n(emailInvalidConfig.messageKey, emailInvalidConfig.fallback)); 1899 return false; 1900 } 1901 return true; 1902 } 1903 1904 if (!value) { 1905 if (config.key === 'company') { 1906 return true; 1907 } 1908 setCompanyFieldError($field, getI18n(config.messageKey, config.fallback)); 1909 return false; 1910 } 1911 1912 return true; 1913 } 1914 1915 function validateCompanyStepForm() { 1916 clearCompanyFieldErrors(); 1917 syncCompanyStepForSubmit(); 1918 1919 const $scope = $(getCompanyFieldsScopeSelector()); 1920 let isValid = true; 1921 1922 function fail(config) { 1923 const $field = resolveCompanyField(config); 1924 setCompanyFieldError($field, getI18n(config.messageKey, config.fallback)); 1925 isValid = false; 1926 } 1927 1928 const val = function (companySelector) { 1929 return ($scope.find(companySelector).val() || '').trim(); 1930 }; 1931 1932 if (!val('.js-company-country-input')) { 1933 fail(companyFieldConfig('country')); 1934 } 1935 const name = val('.js-company-name-input'); 1936 if (!name) { 1937 fail(companyFieldConfig('name')); 1938 } else if (!isFullNameValid(name)) { 1939 fail(companyFieldConfig('name_invalid')); 1940 } 1941 1942 const email = val('.js-company-email-input'); 1943 if (!email) { 1944 fail(companyFieldConfig('email')); 1945 } else if (!isCompanyEmailValid(email)) { 1946 fail(companyFieldConfig('email_invalid')); 1947 } 1948 1949 if (!val('.js-company-phone-prefix-input')) { 1950 fail(companyFieldConfig('phone_prefix')); 1951 } 1952 if (!val('.js-company-phone-input')) { 1953 fail(companyFieldConfig('phone')); 1954 } 1955 1956 if (!isValid) { 1957 focusFirstCompanyFieldError(); 1958 } 1959 1960 return isValid; 1961 } 1962 1963 function applyServerCompanyErrors(errors) { 1964 if (!errors || typeof errors !== 'object' || errors.success) { 1965 return false; 1966 } 1967 1968 const knownKeys = COMPANY_FIELD_VALIDATION.map(function (item) { 1969 return item.key; 1970 }); 1971 const hasKnownError = knownKeys.some(function (key) { 1972 return !!errors[key]; 1973 }); 1974 if (!hasKnownError) { 1975 return false;
1976 } 1977 1978 clearCompanyFieldErrors(); 1979 1980 if (errors.name_invalid) { 1981 const nameConfig = companyFieldConfig('name_invalid'); 1982 setCompanyFieldError( 1983 resolveCompanyField(nameConfig), 1984 getI18n(nameConfig.messageKey, nameConfig.fallback) 1985 ); 1986 } else if (errors.name) { 1987 const nameConfig = companyFieldConfig('name'); 1988 setCompanyFieldError( 1989 resolveCompanyField(nameConfig), 1990 getI18n(nameConfig.messageKey, nameConfig.fallback) 1991 ); 1992 } 1993 1994 if (errors.email_invalid) { 1995 const emailConfig = companyFieldConfig('email_invalid'); 1996 setCompanyFieldError( 1997 resolveCompanyField(emailConfig), 1998 getI18n(emailConfig.messageKey, emailConfig.fallback) 1999 ); 2000 } else if (errors.email) { 2001 const emailConfig = companyFieldConfig('email'); 2002 setCompanyFieldError( 2003 resolveCompanyField(emailConfig), 2004 getI18n(emailConfig.messageKey, emailConfig.fallback) 2005 ); 2006 } 2007 2008 2009 COMPANY_FIELD_VALIDATION.forEach(function (config) { 2010 if ( 2011 config.key === 'name' || 2012 config.key === 'name_invalid' || 2013 config.key === 'email' || 2014 config.key === 'email_invalid' 2015 ) { 2016 return; 2017 } 2018 if (!errors[config.key]) { 2019 return; 2020 } 2021 setCompanyFieldError( 2022 resolveCompanyField(config), 2023 getI18n(config.messageKey, config.fallback) 2024 ); 2025 }); 2026 2027 focusFirstCompanyFieldError(); 2028 return true; 2029 } 2030 2031 function clearCompanyFieldErrorForInput($field) { 2032 if (!$field || !$field.length) { 2033 return; 2034 } 2035 2036 $field.removeClass('is-invalid'); 2037 const $wrap = $field.closest('.new-onboarding__company-field'); 2038 const $host = $wrap.length ? $wrap : $field.parent(); 2039 dismissCompanyFieldError($host.find('.new-onboarding__field-error')); 2040 } 2041 2042 function updateQuoteSubmitUi() { 2043 const $cloudBtn = $('#create-order-button'); 2044 const $quoteBtn = $('#quote-submit-button'); 2045 2046 updateQuoteFlowFieldsVisibility(); 2047 $root.toggleClass('is-quote-request-mode', !!state.isQuoteRequest); 2048 2049 if (state.isQuoteRequest) { 2050 if ($quoteBtn.length) { 2051 $quoteBtn.text(getI18n('submitQuote', 'Submit')); 2052 } 2053 return; 2054 } 2055 2056 if ($cloudBtn.length) { 2057 $cloudBtn.text(getI18n('wizardNext', 'Next')); 2058 } 2059 } 2060 2061 function getQuotePlanDetails() { 2062 const dcId = $('#dc').val(); 2063 return getCloudPlansForDc(dcId); 2064 } 2065 2066 function getQuoteUsersCount(forCalculation) { 2067 const raw = ($('.js-quote-users-input').val() || '').trim(); 2068 const users = parseInt(raw, 10); 2069 if (!users || users < 1) { 2070 return forCalculation ? 1 : 0; 2071 } 2072 return users; 2073 } 2074 2075 function commitQuoteUsersToForm() { 2076 const users = getQuoteUsersCount(); 2077 if (!users) { 2078 return false; 2079 } 2080 state.selectedUsers = users; 2081 $('#UsersNum').val(String(users)); 2082 $('.js-quote-users-input').val(String(users)); 2083 renderSummary(); 2084 return true; 2085 } 2086 2087 function updateQuoteUsersPrice() { 2088 const users = getQuoteUsersCount(true); 2089 state.selectedUsers = users; 2090 $('#UsersNum').val(String(users)); 2091 2092 const $price = $('.js-quote-users-price'); 2093 if (!$price.length) { 2094 return; 2095 } 2096 2097 const planDetails = getQuotePlanDetails(); 2098 if (!planDetails || !planDetails.Plans || !planDetails.Plans[0]) { 2099 $price.text(''); 2100 return; 2101 } 2102 2103 const currency = resolvePlanCurrency(planDetails); 2104 const pricePerUser = resolvePrepaidPlanPricePerUser(planDetails, 'BUSINESS'); 2105 if (pricePerUser === null) { 2106 $price.text(''); 2107 return; 2108 } 2109 2110 const annualTotal = pricePerUser * users * 12; 2111 const formatted = formatPlanPrice(annualTotal, currency); 2112 const perYear = getI18n('quotePricePerYear', '/year'); 2113 $price.text(formatted + ' ' + perYear); 2114 } 2115 2116 function validateQuoteContactForm() { 2117 syncQuoteContactToForm(); 2118 syncQuoteNameToForm(); 2119 syncQuoteCompanyToForm(); 2120 syncQuoteSolutionToForm(); 2121 2122 if (!commitQuoteUsersToForm()) { 2123 setFeedback('quote-contact-feedback', getI18n('quoteUsersInvalid', 'Enter the number of users.'), true); 2124 return false;
2125 } 2126 2127 const name = ($('#name').val() || '').trim(); 2128 if (!name || name.indexOf(' ') === -1) { 2129 setFeedback('quote-contact-feedback', getI18n('quoteNameInvalid', 'Enter your full name (first and last name).'), true); 2130 return false; 2131 } 2132 2133 const email = ($('#email').val() || '').trim(); 2134 if (!email || !isCompanyEmailValid(email)) { 2135 const msg = getI18n('quoteEmailInvalid', 'Enter a valid email address.'); 2136 setCompanyFieldError($('.js-quote-email-input'), msg); 2137 setFeedback('quote-contact-feedback', msg, true); 2138 return false; 2139 } 2140 2141 const country = ($('#country').val() || '').trim(); 2142 if (!country) { 2143 setFeedback('quote-contact-feedback', getI18n('quoteCountryInvalid', 'Select your country.'), true); 2144 return false; 2145 } 2146 2147 const phone = ($('#phone').val() || '').trim(); 2148 if (!phone) { 2149 setFeedback('quote-contact-feedback', getI18n('quotePhoneInvalid', 'Enter your phone number.'), true); 2150 return false; 2151 } 2152 2153 if (!($('#company').val() || '').trim()) { 2154 $('#company').val($('#name').val()); 2155 } 2156 2157 setFeedback('quote-contact-feedback', ''); 2158 return true; 2159 } 2160 2161 function submitQuoteRequest() { 2162 if (!validateQuoteContactForm()) { 2163 return; 2164 } 2165 2166 setFeedback('quote-contact-feedback', ''); 2167 const $btn = $('#quote-submit-button'); 2168 $btn.prop('disabled', true); 2169 2170 $.ajax({ 2171 url: config.endpoints.createQuoteRequest, 2172 type: 'POST', 2173 data: serializeForm(), 2174 success: function (resp) { 2175 if (resp && resp.success) { 2176 if (window.dataLayer) { 2177 window.dataLayer.push({ event: 'quote request' }); 2178 } 2179 showQuoteSuccessStep(); 2180 return; 2181 } 2182 2183 if (resp && resp.error === 'email') { 2184 const msg = getI18n('quoteEmailInvalid', 'Enter a valid email address.'); 2185 setCompanyFieldError($('.js-quote-email-input'), msg); 2186 setFeedback('quote-contact-feedback', msg, true); 2187 $btn.prop('disabled', false); 2188 return; 2189 } 2190 2191 setFeedback( 2192 'quote-contact-feedback', 2193 getI18n('quoteSubmitError', 'Unable to submit your request. Please check your details and try again.'), 2194 true 2195 ); 2196 $btn.prop('disabled', false); 2197 }, 2198 error: function () { 2199 setFeedback( 2200 'quote-contact-feedback', 2201 getI18n('quoteSubmitError', 'Unable to submit your request. Please check your details and try again.'), 2202 true 2203 ); 2204 $btn.prop('disabled', false); 2205 }, 2206 }); 2207 } 2208 2209 function cancelDeferredWizardReset() { 2210 if (landingWizardResetTimer) { 2211 window.clearTimeout(landingWizardResetTimer); 2212 landingWizardResetTimer = null; 2213 } 2214 } 2215 2216 function scheduleDeferredWizardReset() { 2217 cancelDeferredWizardReset(); 2218 landingWizardResetTimer = window.setTimeout(function () { 2219 landingWizardResetTimer = null; 2220 resetWizardState(); 2221 }, LANDING_WIZARD_RESET_DELAY_MS); 2222 } 2223 2224 function resetWizardState() { 2225 cancelDeferredWizardReset(); 2226 2227 if ($form.length && $form[0] && typeof $form[0].reset === 'function') { 2228 $form[0].reset(); 2229 } 2230 2231 state.email = ''; 2232 state.checkoutMounted = false; 2233 state.flow = state.isQuoteRequest ? FLOW_QUOTE.slice() : getFlow(state.selectedPackage); 2234 state.currentStep = ''; 2235 state.maxReachedStepIndex = -1; 2236 state.seenSteps = {}; 2237 setQuoteSuccessLayout(false); 2238 2239 $('#dc_hidden').val(getBillingDcId()); 2240 $('#package').val(getBackendPackageName(state.selectedPackage)); 2241 $('#UsersNum').val(String(state.selectedUsers)); 2242 $('#subscriptionPeriod').val(''); 2243 $('#csession').val(''); 2244 $('#checkout').empty(); 2245 $('.verify-digit').val(''); 2246 $('.new-onboarding__verify').removeClass('is-invalid'); 2247 $('#verify-error').prop('hidden', true); 2248 2249 resetOptionalValues(); 2250 clearFeedbacks(); 2251 clearResendCodeCooldown(); 2252 syncChoices(); 2253 renderSummary(); 2254 } 2255 2256 const VIEW_TRANSITION_OUT_MS = 360; 2257 const VIEW_TRANSITION_IN_MS = 310; 2258 const VIEW_TRANSITION_IN_DELAY_MS = Math.round(VIEW_TRANSITION_OUT_MS * 0.8); 2259 const VIEW_TRANSITION_MS = VIEW_TRANSITION_IN_DELAY_MS + VIEW_TRANSITION_IN_MS + 80; 2260 const LANDING_WIZARD_RESET_DELAY_MS = 700; 2261 const SCROLL_BEFORE_WIZARD_PAUSE_MS = 100; 2262 const SCROLL_TO_TOP_DURATION_MS = 520; 2263 let viewTransitionLock = false;
2264 2265 function prefersReducedMotion() { 2266 return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 2267 } 2268 2269 function scrollToTopBeforeTransition(done) { 2270 const currentTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 2271 if (currentTop <= 2) { 2272 done(false); 2273 return; 2274 } 2275 2276 let finished = false; 2277 const complete = function () { 2278 if (finished) { 2279 return; 2280 } 2281 finished = true; 2282 done(true); 2283 }; 2284 2285 $('html, body').stop(true).animate({ scrollTop: 0 }, SCROLL_TO_TOP_DURATION_MS, complete); 2286 window.setTimeout(complete, SCROLL_TO_TOP_DURATION_MS + 120); 2287 } 2288 2289 function runViewTransition(transitionClass, onComplete) { 2290 if (viewTransitionLock) { 2291 return; 2292 } 2293 2294 if (prefersReducedMotion()) { 2295 onComplete(); 2296 return; 2297 } 2298 2299 viewTransitionLock = true; 2300 2301 let finished = false; 2302 let pendingAnimations = 2; 2303 const finish = function () { 2304 if (finished) { 2305 return; 2306 } 2307 finished = true; 2308 $landing.add($form).off('.newOnboardingViewTransition'); 2309 $landing.css({ opacity: '', transform: '' }); 2310 $form.css({ opacity: '', transform: '' }); 2311 onComplete(); 2312 $root.removeClass(transitionClass); 2313 viewTransitionLock = false; 2314 }; 2315 2316 const onAnimationEnd = function (event) { 2317 if (event.target !== $landing[0] && event.target !== $form[0]) { 2318 return; 2319 } 2320 pendingAnimations -= 1; 2321 if (pendingAnimations <= 0) { 2322 finish(); 2323 } 2324 }; 2325 2326 $landing.add($form).on('animationend.newOnboardingViewTransition', onAnimationEnd); 2327 window.setTimeout(finish, VIEW_TRANSITION_MS + 80); 2328 } 2329 2330 function setSummaryPanelSticky(isSticky) { 2331 $('.new-onboarding__summary-panel').toggleClass('is-sticky', !!isSticky); 2332 } 2333 2334 function resetMobileSummaryPanel() { 2335 const $panel = $('.new-onboarding__summary-panel');
2336 $panel.removeClass('is-mobile-peek-visible is-mobile-open'); 2337 $('.new-onboarding__summary-toggle').attr('aria-expanded', 'false'); 2338 } 2339 2340 function revealMobileSummaryPeek() { 2341 const $panel = $('.new-onboarding__summary-panel'); 2342 $panel.removeClass('is-mobile-open'); 2343 $('.new-onboarding__summary-toggle').attr('aria-expanded', 'false'); 2344 2345 if (!window.matchMedia('(max-width: 767px)').matches) { 2346 $panel.addClass('is-mobile-peek-visible'); 2347 return; 2348 } 2349 2350 $panel.removeClass('is-mobile-peek-visible'); 2351 2352 if (prefersReducedMotion()) { 2353 $panel.addClass('is-mobile-peek-visible'); 2354 return; 2355 } 2356 2357 window.requestAnimationFrame(function () { 2358 window.requestAnimationFrame(function () { 2359 $panel.addClass('is-mobile-peek-visible'); 2360 }); 2361 }); 2362 } 2363 2364 function applyWizardMode() { 2365 $landing.addClass('is-hidden'); 2366 $form.removeClass('is-hidden'); 2367 $root.addClass('is-wizard-mode'); 2368 prepareWizardTopMenu({ animate: false }); 2369 setSummaryPanelSticky(true); 2370 revealMobileSummaryPeek(); 2371 } 2372 2373 function applyLandingMode() { 2374 state.isQuoteRequest = false; 2375 setQuoteSuccessLayout(false); 2376 updateQuoteSubmitUi(); 2377 $root.removeClass('is-wizard-mode'); 2378 resetMobileSummaryPanel(); 2379 setSummaryPanelSticky(false); 2380 restoreLandingTopMenu({ animate: false }); 2381 $landing.removeClass('is-hidden'); 2382 $form.addClass('is-hidden'); 2383 window.requestAnimationFrame(function () { 2384 updateModeSwitchThumb(); 2385 }); 2386 } 2387 2388 function showWizardMode(options) { 2389 const opts = options || {}; 2390 const animate = opts.animate !== false; 2391 2392 if (!animate || prefersReducedMotion()) { 2393 window.scrollTo(0, 0); 2394 applyWizardMode(); 2395 return; 2396 } 2397 2398 scrollToTopBeforeTransition(function (didScroll) { 2399 const startWizardTransition = function () { 2400 prepareWizardTopMenu({ animate: true }); 2401 $root.removeClass('is-wizard-mode'); 2402 $root.addClass('is-switching-to-wizard'); 2403 $landing.removeClass('is-hidden'); 2404 $form.removeClass('is-hidden'); 2405 runViewTransition('is-switching-to-wizard', applyWizardMode); 2406 }; 2407 2408 if (didScroll) { 2409 window.setTimeout(startWizardTransition, SCROLL_BEFORE_WIZARD_PAUSE_MS); 2410 return; 2411 } 2412 2413 startWizardTransition(); 2414 }); 2415 } 2416 2417 function showLandingMode(options) { 2418 const opts = options || {}; 2419 const animate = opts.animate !== false; 2420 if (!animate || prefersReducedMotion()) { 2421 window.scrollTo(0, 0); 2422 applyLandingMode(); 2423 return; 2424 } 2425 2426 scrollToTopBeforeTransition(function () { 2427 restoreLandingTopMenu({ animate: true }); 2428 $root.addClass('is-switching-to-landing'); 2429 $landing.removeClass('is-hidden'); 2430 $form.removeClass('is-hidden'); 2431 runViewTransition('is-switching-to-landing', applyLandingMode); 2432 }); 2433 } 2434 2435 function selectPlan(uiPackageName, users, planName, planPrice) { 2436 state.isQuoteRequest = false; 2437 state.selectedPackage = normalizePackageName(uiPackageName); 2438 state.selectedUsers = parseInt(users, 10) || 20; 2439 state.selectedPlanName = planName || uiPackageName; 2440 state.selectedPlanPrice = planPrice || ''; 2441 state.flow = getFlow(state.selectedPackage); 2442 2443 resetWizardState(); 2444 updateQuoteSubmitUi(); 2445 updatePlanHiddenFields(); 2446 warnIfWizardDomOutdated(); 2447 showStep(state.flow[0], { scroll: false, animate: false }); 2448 showWizardMode(); 2449 } 2450 2451 async function renderEmbeddedCheckout() { 2452 if (state.checkoutMounted) return true; 2453 2454 const dcId = getBillingDcId(); 2455 const tokenResponse = await $.get(config.endpoints.getStripeToken + '?dc=' + dcId); 2456 if (!tokenResponse.token) { 2457 setFeedback('checkout-feedback', 'Unable to load Stripe token for selected datacenter.', true); 2458 return false; 2459 } 2460 2461 const stripe = Stripe(tokenResponse.token); 2462 const sessionResponse = await fetch(config.endpoints.createStripeCheckoutSession + '?dc=' + dcId, { method: 'POST' }); 2463 const sessionData = await sessionResponse.json(); 2464 if (!sessionData.clientSecret || !sessionData.csession) { 2465 setFeedback('checkout-feedback', 'Unable to start Stripe checkout session.', true); 2466 return false;
2467 } 2468 2469 $('#csession').val(sessionData.csession); 2470 2471 // persist csession to the order before mounting Stripe â handle-stripe-card-details 2472 // loads the order by csession after the embedded checkout redirects 2473 await $.post(config.endpoints.updateStripeOrder, serializeForm()); 2474 2475 const checkout = await stripe.initEmbeddedCheckout({ clientSecret: sessionData.clientSecret }); 2476 checkout.mount('#checkout'); 2477 state.checkoutMounted = true; 2478 return true; 2479 } 2480 2481 const MODE_TEXTS = { 2482 cloud: (config.i18n && config.i18n.mode && config.i18n.mode.cloud) || { 2483 title: 'Cloud Pricing', 2484 subtitle: 'Find your best solution and try IceWarp for 30 days free.', 2485 }, 2486 'self-hosted': (config.i18n && config.i18n.mode && config.i18n.mode['self-hosted']) || { 2487 title: 'Self-hosted Pricing', 2488 subtitle: 'Deploy and run IceWarp on your own infrastructure with full control over your data and environment.', 2489 }, 2490 }; 2491 2492 const MODE_TEXT_OUT_MS = 180; 2493 const MODE_TEXT_IN_MS = 360; 2494 const PRICING_FLIP_MS
2494= 520; 2495 const PRICING_EASE = 'cubic-bezier(0.22, 0.61, 0.36, 1)'; 2496 let modeSwapTimer = null; 2497 let modeDcEnterTimer = null; 2498 let modeSwitchThumbResizeTimer = null; 2499 let pricingFlipCleanupTimer = null; 2500 let pricingFlipRaf1 = null; 2501 let pricingFlipRaf2 = null; 2502 2503 function getPricingCollapseEls() { 2504 return $landing.find( 2505 '.new-onboarding__plan-pricing, .new-onboarding__compare-plan-price, .new-onboarding__compare-plan-meta, .new-onboarding__plan-highlight--cloud-only' 2506 ); 2507 } 2508 2509 function getPricingFlipMovers() { 2510 return $landing.find( 2511 '.new-onboarding__plan-card > .new-onboarding__button, .new-onboarding__plan-card > .new-onboarding__plan-highlights, .new-onboarding__compare-plan-cta' 2512 ); 2513 } 2514 2515 function clearPricingFlipTimers() { 2516 if (pricingFlipCleanupTimer) { 2517 window.clearTimeout(pricingFlipCleanupTimer); 2518 pricingFlipCleanupTimer = null; 2519 } 2520 if (pricingFlipRaf1) { 2521 window.cancelAnimationFrame(pricingFlipRaf1); 2522 pricingFlipRaf1 = null; 2523 } 2524 if (pricingFlipRaf2) { 2525 window.cancelAnimationFrame(pricingFlipRaf2); 2526 pricingFlipRaf2 = null; 2527 } 2528 } 2529 2530 function clearFlipInlineStyles($els) { 2531 $els.each(function () { 2532 this.style.transition = ''; 2533 this.style.transform = ''; 2534 this.style.opacity = ''; 2535 this.style.willChange = ''; 2536 }); 2537 } 2538 2539 function runAfterPaint(fn) { 2540 pricingFlipRaf1 = window.requestAnimationFrame(function () { 2541 pricingFlipRaf1 = null; 2542 pricingFlipRaf2 = window.requestAnimationFrame(function () { 2543 pricingFlipRaf2 = null; 2544 fn(); 2545 }); 2546 }); 2547 } 2548 2549 // Safari: FIRST â LAST â one layout flush â INVERT (pre-paint) â double rAF â PLAY 2550 // No fade-then-snap (that tears the first FLIP frame on Safari). 2551 function setPricingCollapsed(collapsed, animate) { 2552 const $collapseEls = getPricingCollapseEls(); 2553 const $fadeEls = $landing.find( 2554 '.new-onboarding__plan-pricing, .new-onboarding__compare-plan-price, .new-onboarding__compare-plan-meta' 2555 ); 2556 const $movers = getPricingFlipMovers(); 2557 2558 clearPricingFlipTimers(); 2559 clearFlipInlineStyles($movers); 2560 clearFlipInlineStyles($fadeEls); 2561 2562 if (!animate || prefersReducedMotion()) { 2563 $collapseEls.toggleClass('is-collapsing', collapsed); 2564 return; 2565 } 2566 2567 const firstTops = []; 2568 $movers.each(function (i) { 2569 firstTops[i] = this.getBoundingClientRect().top; 2570 }); 2571 2572 $collapseEls.toggleClass('is-collapsing', collapsed); 2573 // one flush so LAST rects are real before invert (avoid per-node offsetWidth) 2574 void document.body.offsetHeight; 2575 2576 $movers.each(function (i) { 2577 const dy = firstTops[i] - this.getBoundingClientRect().top; 2578 this.style.transition = 'none'; 2579 if (Math.abs(dy) < 0.5) { 2580 this.style.transform = ''; 2581 return; 2582 } 2583 this.style.willChange = 'transform'; 2584 this.style.transform = 'translate3d(0, ' + dy + 'px, 0)'; 2585 }); 2586 2587 if (!collapsed) { 2588 $fadeEls.each(function () { 2589 this.style.transition = 'none'; 2590 this.style.opacity = '0'; 2591 }); 2592 } 2593 2594 runAfterPaint(function () { 2595 $movers.each(function () { 2596 if (!this.style.transform) { 2597 return; 2598 } 2599 this.style.transition = 'transform ' + PRICING_FLIP_MS + 'ms ' + PRICING_EASE; 2600 this.style.transform = 'translate3d(0, 0, 0)'; 2601 }); 2602 2603 if (!collapsed) { 2604 $fadeEls.each(function () { 2605 this.style.willChange = 'opacity'; 2606 this.style.transition = 'opacity 0.28s ease'; 2607 this.style.opacity = '1'; 2608 }); 2609 } 2610 2611 pricingFlipCleanupTimer = window.setTimeout(function () { 2612 clearFlipInlineStyles($movers); 2613 clearFlipInlineStyles($fadeEls); 2614 pricingFlipCleanupTimer = null; 2615 }, PRICING_FLIP_MS + 40); 2616 }); 2617 } 2618 2619 function updateModeSwitchThumb() { 2620 const $switch = $landing.find('.new-onboarding__mode-switch'); 2621 if (!$switch.length) { 2622 return; 2623 } 2624 2625 let $active = $switch.find('.js-mode-switch.is-active').first(); 2626 if (!$active.length) { 2627 const fallbackMode = $landing.attr('data-current-mode') || 'cloud'; 2628 const safeMode = MODE_TEXTS[fallbackMode] ? fallbackMode : 'cloud'; 2629 $switch.find('.js-mode-switch').removeClass('is-active').attr('aria-selected', 'false'); 2630 $active = $switch 2631 .find('.js-mode-switch[data-mode="' + safeMode + '"]') 2632 .addClass('is-active') 2633 .attr('aria-selected', 'true'); 2634 } 2635 2636 if ($landing.hasClass('is-hidden') || !$active.length) { 2637 return; 2638 } 2639 2640 const activeEl = $active[0]; 2641 if (!activeEl.offsetWidth) { 2642 return; 2643 } 2644 2645 $switch.css({ 2646 '--mode-switch-thumb-left': activeEl.offsetLeft + 'px', 2647 '--mode-switch-thumb-width': activeEl.offsetWidth + 'px', 2648 }); 2649 } 2650 2651 function initOnboardingModeSwitch() { 2652 const safeMode = MODE_TEXTS[$landing.attr('data-current-mode')] ? $landing.attr('data-current-mode') : 'cloud'; 2653 setOnboardingModePills(safeMode); 2654 setOnboardingModeContent(safeMode); 2655 updateModeSwitchThumb(); 2656 } 2657 2658 function setOnboardingModePills(safeMode, options) { 2659 const opts = options || {}; 2660 const $pills = $landing.find('.js-mode-switch'); 2661 const $target = $pills.filter('[data-mode="' + safeMode + '"]'); 2662 const resolvedMode = $target.length ? safeMode : 'cloud'; 2663 const $resolvedTarget = $pills.filter('[data-mode="' + resolvedMode + '"]'); 2664 const $currentActive = $pills.filter('.is-active'); 2665 2666 if ($currentActive.length === 1 && $currentActive.is($resolvedTarget)) { 2667 updateModeSwitchThumb(); 2668 return; 2669 } 2670 2671 $pills.removeClass('is-active').attr('aria-selected', 'false'); 2672 $resolvedTarget.addClass('is-active').attr('aria-selected', 'true'); 2673 updateModeSwitchThumb(); 2674 } 2675 2676 function setCompareFeatureModeActive(safeMode) { 2677 const $modes = $landing.find('.new-onboarding__compare-feature-mode') 2678 .add($('[data-step="welcome"] .new-onboarding__compare-feature-mode')); 2679 $modes.removeClass('is-mode-active is-mode-leaving is-mode-entering'); 2680 $modes.filter('.new-onboarding__compare-feature-mode--' + safeMode).addClass('is-mode-active'); 2681 } 2682 2683 function getModeAnimatedTexts() { 2684 // ponytail: A/B â prices collapse via max-height, not mode text swap 2685 return $landing.find( 2686 '[data-mode-title], [data-mode-subtitle], .new-onboarding__compare-feature-mode.is-mode-active' 2687 ); 2688 } 2689 2690 function setOnboardingModeContent(safeMode, options) { 2691 const opts = options || {}; 2692 const texts = MODE_TEXTS[safeMode]; 2693 const $dcSelect = $landing.find('.new-onboarding__dc-select'); 2694 2695 $landing 2696 .removeClass('is-mode-cloud is-mode-self-hosted') 2697 .addClass('is-mode-' + safeMode) 2698 .attr('data-current-mode', safeMode); 2699 2700 $landing.find('[data-mode-title]').text(texts.title); 2701 $landing.find('[data-mode-subtitle]').text(texts.subtitle); 2702 setCompareFeatureModeActive(safeMode); 2703 2704 if (modeDcEnterTimer) { 2705 window.clearTimeout(modeDcEnterTimer); 2706 modeDcEnterTimer = null; 2707 } 2708 2709 if (safeMode === 'cloud') { 2710 $dcSelect.removeClass('is-hidden is-mode-leaving'); 2711 2712 if (opts.animate && opts.fromMode && opts.fromMode !== 'cloud' && !prefersReducedMotion()) { 2713 $dcSelect.removeClass('is-mode-entering'); 2714 // Force reflow so repeated toggles always restart entering animation. 2715 void $dcSelect[0].offsetWidth; 2716 $dcSelect.addClass('is-mode-entering'); 2717 modeDcEnterTimer = window.setTimeout(function () { 2718 $dcSelect.removeClass('is-mode-entering'); 2719 modeDcEnterTimer = null; 2720 }, MODE_TEXT_IN_MS); 2721 } else { 2722 $dcSelect.removeClass('is-mode-entering'); 2723 } 2724 } else { 2725 $dcSelect.removeClass('is-mode-entering is-mode-leaving').addClass('is-hidden'); 2726 } 2727 } 2728 2729 function refreshPricesForOnboardingMode() { 2730 updatePlanCardPrices($('#dc').val() || ''); 2731 if (state.selectedPackage && !$landing.hasClass('is-hidden')) { 2732 const $selectedCard = $('.js-start-onboarding[data-package="' + state.selectedPackage + '"]') 2733 .closest('.new-onboarding__plan-card'); 2734 if ($selectedCard.length) { 2735 state.selectedPlanPrice = ($selectedCard.find('.new-onboarding__plan-price').first().text() || '').trim(); 2736 } 2737 } 2738 if (!$form.hasClass('is-hidden')) { 2739 renderSummary(); 2740 } 2741 } 2742 2743 function applyOnboardingMode(mode, options) { 2744 const opts = options || {}; 2745 const safeMode = MODE_TEXTS[mode] ? mode : 'cloud'; 2746 const currentMode = $landing.attr('data-current-mode'); 2747 const $dcSelect = $landing.find('.new-onboarding__dc-select'); 2748 2749 if (currentMode === safeMode) { 2750 updateModeSwitchThumb(); 2751 setOnboardingModeContent(safeMode); 2752 return; 2753 } 2754 2755 if (opts.animate === false || prefersReducedMotion() || !currentMode) { 2756 setOnboardingModePills(safeMode); 2757 setOnboardingModeContent(safeMode); 2758 setPricingCollapsed(safeMode === 'self-hosted', false); 2759 refreshPricesForOnboardingMode(); 2760 return; 2761 } 2762 2763 if (modeSwapTimer) { 2764 window.clearTimeout(modeSwapTimer); 2765 modeSwapTimer = null; 2766 } 2767 2768 setOnboardingModePills(safeMode); 2769 2770 if (safeMode === 'self-hosted') { 2771 // next frame: don't fight mode-text leave on same Safari paint 2772 window.requestAnimationFrame(function () { 2773 setPricingCollapsed(true, true); 2774 }); 2775 } 2776 2777 const $texts = getModeAnimatedTexts(); 2778 $texts.removeClass('is-mode-entering').addClass('is-mode-leaving'); 2779 if (safeMode !== 'cloud' && currentMode === 'cloud') { 2780 $dcSelect.removeClass('is-mode-entering').addClass('is-mode-leaving'); 2781 } 2782 2783 modeSwapTimer = window.setTimeout(function () { 2784 setOnboardingModeContent(safeMode, { animate: true, fromMode: currentMode }); 2785 updatePlanCardPrices($('#dc').val() || ''); 2786 if (safeMode === 'cloud') { 2787 window.requestAnimationFrame(function () { 2788 setPricingCollapsed(false, true); 2789 }); 2790 } 2791 const $enteringTexts = getModeAnimatedTexts(); 2792 $enteringTexts.removeClass('is-mode-leaving').addClass('is-mode-entering'); 2793 modeSwapTimer = window.setTimeout(function () { 2794 $enteringTexts.removeClass('is-mode-entering'); 2795 modeSwapTimer = null; 2796 if (state.selectedPackage) { 2797 const $selectedCard = $('.js-start-onboarding[data-package="' + state.selectedPackage + '"]') 2798 .closest('.new-onboarding__plan-card'); 2799 if ($selectedCard.length) { 2800 state.selectedPlanPrice = ($selectedCard.find('.new-onboarding__plan-price').first().text() || '').trim(); 2801 } 2802 } 2803 if (!$form.hasClass('is-hidden')) { 2804 renderSummary(); 2805 } 2806 }, MODE_TEXT_IN_MS); 2807 }, MODE_TEXT_OUT_MS); 2808 } 2809 2810 $(document).on('click', '.js-mode-switch', function () { 2811 const mode = $(this).data('mode'); 2812 if (!mode) { 2813 return; 2814 } 2815 $landing.find('.new-onboarding__mode-switch').addClass('is-mode-switch-ready'); 2816 applyOnboardingMode(mode); 2817 }); 2818 2819 initOnboardingModeSwitch(); 2820 2821 const DC_FLAG_BASE = '/img/redesign/pricing/2026/country-flags/'; 2822 const DC_PANEL_TRANSITION_MS = 240; 2823 let dcPanelCloseTimer = null; 2824 2825 function escapeHtml(value) { 2826 return $('<div>').text(value || '').html(); 2827 } 2828 2829 function buildDcDropdownValueHtml(label, flagCode) { 2830 let html = ''; 2831 if (flagCode) { 2832 html += '<img class="new-onboarding__dc-dropdown-flag" src="' + DC_FLAG_BASE + flagCo
2832de + '.svg" alt="" width="24" height="24">'; 2833 } else { 2834 html += '<span class="new-onboarding__dc-dropdown-flag new-onboarding__dc-dropdown-flag--placeholder" aria-hidden="true"></span>'; 2835 } 2836 html += '<span class="new-onboarding__dc-dropdown-label-text">' + escapeHtml(label) + '</span>'; 2837 return html; 2838 } 2839 2840 function setDcDropdownOpen(isOpen) { 2841 const $dropdown = $('#dc-dropdown'); 2842 const $panel = $('#dc-dropdown-panel'); 2843 const $trigger = $('#dc-dropdown-trigger'); 2844 const panel = $panel[0]; 2845 if (!$dropdown.length || !$panel.length || !$trigger.length || !panel) { 2846 return; 2847 } 2848 2849 if (dcPanelCloseTimer) { 2850 window.clearTimeout(dcPanelCloseTimer); 2851 dcPanelCloseTimer = null; 2852 } 2853 2854 $trigger.attr('aria-expanded', isOpen ? 'true' : 'false'); 2855 2856 if (prefersReducedMotion()) { 2857 $dropdown.toggleClass('is-open', isOpen); 2858 $panel.toggleClass('is-open', isOpen); 2859 if (isOpen) { 2860 $panel.removeAttr('hidden'); 2861 } else { 2862 $panel.attr('hidden', 'hidden'); 2863 } 2864 return; 2865 } 2866 2867 if (isOpen) { 2868 $dropdown.addClass('is-open'); 2869 $panel.removeAttr('hidden'); 2870 void panel.offsetWidth; 2871 $panel.addClass('is-open'); 2872 return; 2873 } 2874 2875 $panel.removeClass('is-open'); 2876 dcPanelCloseTimer = window.setTimeout(function () { 2877 if (!$panel.hasClass('is-open')) { 2878 $panel.attr('hidden', 'hidden'); 2879 $dropdown.removeClass('is-open'); 2880 } 2881 dcPanelCloseTimer = null; 2882 }, DC_PANEL_TRANSITION_MS); 2883 } 2884 2885 function syncDcDropdownSelection() { 2886 const $select = $('#dc'); 2887 const $panel = $('#dc-dropdown-panel'); 2888 if (!$select.length) { 2889 return; 2890 } 2891 2892 const selectedId = String($select.val() || ''); 2893 const $option = $select.find('option[value="' + selectedId + '"]'); 2894 const label = $option.attr('data-dropdown-label') || $option.text(); 2895 const flagCode = $option.attr('data-flag-code') || ''; 2896 2897 $('#dc-dropdown .new-onboarding__dc-dropdown-value').html(buildDcDropdownValueHtml(label, flagCode)); 2898 2899 $panel.find('.js-dc-dropdown-option').each(function () { 2900 const $btn = $(this); 2901 const isSelected = String($btn.attr('data-dc-id')) === selectedId; 2902 $btn.toggleClass('is-selected', isSelected).attr('aria-selected', isSelected ? 'true' : 'false'); 2903 }); 2904 } 2905 2906 function initDcDropdown() { 2907 const $dropdown = $('#dc-dropdown'); 2908 if (!$dropdown.length) { 2909 return; 2910 } 2911 2912 syncDcDropdownSelection(); 2913 setDcDropdownOpen(false); 2914 2915 $('#dc-dropdown-trigger').on('click', function (event) { 2916 event.stopPropagation(); 2917 const isOpen = $('#dc-dropdown-panel').hasClass('is-open'); 2918 setDcDropdownOpen(!isOpen); 2919 }); 2920 2921 $dropdown.on('click', '.js-dc-dropdown-option', function (event) { 2922 event.preventDefault(); 2923 event.stopPropagation(); 2924 2925 const dcId = String($(this).attr('data-dc-id') || ''); 2926 if (!dcId) { 2927 return; 2928 } 2929 2930 $('#dc').val(dcId).trigger('change'); 2931 syncDcDropdownSelection(); 2932 setDcDropdownOpen(false); 2933 }); 2934 2935 $(document).on('click', function (event) { 2936 if ($(event.target).closest('#dc-dropdown').length) { 2937 return; 2938 } 2939 setDcDropdownOpen(false); 2940 }); 2941 2942 $(document).on('keydown', function (event) { 2943 if (event.key === 'Escape') { 2944 setDcDropdownOpen(false); 2945 } 2946 }); 2947 } 2948 2949 initDcDropdown(); 2950 2951 $(window).on('resize', function () { 2952 if ($landing.hasClass('is-hidden')) { 2953 return; 2954 } 2955 if (modeSwitchThumbResizeTimer) { 2956 window.clearTimeout(modeSwitchThumbResizeTimer); 2957 } 2958 modeSwitchThumbResizeTimer = window.setTimeout(function () { 2959 updateModeSwitchThumb(); 2960 modeSwitchThumbResizeTimer = null; 2961 }, 100); 2962 }); 2963 2964 $root.on('click', '.js-compare-scroll', function () { 2965 scrollToCompareFeature($(this).data('compare-scroll')); 2966 }); 2967 2968 let contactFormPlan = ''; 2969 2970 function renderContactFormPlan() { 2971 const $row = $('.js-contact-plan-row'); 2972 const $select = $('.js-contact-plan-select'); 2973 if (!$row.length || !$select.length) { 2974 return; 2975 } 2976 const show = !!contactFormPlan && $('input[name="deployment"]:checked').val() === 'self-hosted'; 2977 // disabled select is not submitted â no stray plan on the cloud / generic contact form
2978 $select.prop('disabled', !show).val(show ? contactFormPlan : ''); 2979 $row.css('display', show ? '' : 'none'); 2980 } 2981 2982 function setContactFormPlan(packageName) { 2983 const plan = String(packageName || ''); 2984 contactFormPlan = $('.js-contact-plan-select').find('option[value="' + plan + '"]').length ? plan : ''; 2985 renderContactFormPlan(); 2986 } 2987 2988 $(document).on('change', 'input[name="deployment"]', renderContactFormPlan); 2989 2990 $root.on('click', '.js-start-onboarding', function () { 2991 if (viewTransitionLock) { 2992 return; 2993 } 2994 2995 // ponytail: A/B self-hosted â contact form; uncomment selectPlan path below to restore flow 2996 if (getOnboardingMode() === 'self-hosted') { 2997 var mode = getOnboardingMode(); 2998 $('input[name="deployment"]').prop('checked', false) 2999 .filter('[value="' + mode + '"]').prop('checked', true); 3000 setContactFormPlan($(this).data('package')); 3001 toggleContactWindow(); 3002 return; 3003 } 3004 3005 const $btn = $(this); 3006 const $card = $btn.closest('.new-onboarding__plan-card'); 3007 let planName = ''; 3008 let planPrice = ''; 3009 3010 if ($card.length) { 3011 planName = ($card.find('h3').first().text() || '').trim(); 3012 planPrice = ($card.find('.new-onboarding__plan-price').first().text() || '').trim(); 3013 } else { 3014 const $head = $btn.closest('.new-onboarding__compare-plan-head'); 3015 planName = ($head.find('.new-onboarding__compare-plan-name').first().text() || $btn.data('package') || '').trim(); 3016 planPrice = ($head.find('[data-compare-plan]').first().text() || '').trim(); 3017 } 3018 3019 selectPlan($btn.data('package'), $btn.data('users'), planName, planPrice); 3020 }); 3021 3022 $('.js-open-quote').on('click', function () { 3023 var mode = getOnboardingMode(); 3024 $('input[name="deployment"]').prop('checked', false) 3025 .filter('[value="' + mode + '"]').prop('checked', true); 3026 setContactFormPlan(''); 3027 toggleContactWindow(); 3028 }); 3029 3030 $form.on('keydown', 'input, select, textarea', handleWizardFieldEnter); 3031 3032 $(document).on('keydown', function (event) { 3033 if (event.key !== 'Enter' || event.isComposing) return; 3034 if (!isWizardEnterNavigationEnabled()) return; 3035 if (state.currentStep !== 'welcome') return; 3036 event.preventDefault(); 3037 clickWizardStepPrimaryAction(findStepElement('welcome')); 3038 }); 3039 3040 $(document).on('input change', '.js-quote-users-input', function () { 3041 updateQuoteUsersPrice(); 3042 }); 3043 3044 $(document).on('input change', '.js-desktop-client-users', function () { 3045 const users = parseInt(($(this).val() || '').trim(), 10); 3046 if (users > 0) { 3047 $('#optional_desktop_suite_users').val(String(users)); 3048 $('#DesktopNum').val(String(users)); 3049 } 3050 updateDesktopClientUsersPrice(); 3051 renderSummary(); 3052 }); 3053 3054 $(document).on('input change', '.js-desktop-office-users', function () { 3055 const users = parseInt(($(this).val() || '').trim(), 10); 3056 if (users > 0) { 3057 $('#optional_ai_assistant_users').val(String(users)); 3058 } 3059 updateDesktopOfficeUsersPrice(); 3060 renderSummary(); 3061 }); 3062 3063 $(document).on('click', '.js-wizard-next', function () { 3064 const $btn = $(this); 3065 if (!canUseWizardNext($btn)) { 3066 return; 3067 } 3068 3069 const stepName = getWizardStepNameFromElement($btn); 3070 3071 if (stepName === 'welcome') { 3072 advanceWizardFromButton($btn); 3073 return; 3074 } 3075 3076 if (stepName === 'subscription') { 3077 if (!commitSubscriptionUsersToForm()) { 3078 findStepElement('subscription').find('.js-subscription-users-input').trigger('focus'); 3079 return; 3080 } 3081 advanceWizardFromButton($btn); 3082 return; 3083 } 3084 3085 if (stepName === 'desktop_client') { 3086 if (getSubscriptionPeriod() !== 'annually') { 3087 $('#optional_desktop_suite').val('included'); 3088 $('#optional_desktop_suite_users').val('0'); 3089 $('#DesktopNum').val('0'); 3090 syncChoices(); 3091 renderSummary(); 3092 pulseAndAdvance($btn, function () { 3093 advanceWizardFromButton($btn); 3094 }); 3095 return; 3096 } 3097 3098 const users = parseInt(($('.js-desktop-client-users').val() || '').trim(), 10); 3099 if (!users || users < 1) { 3100 return; 3101 } 3102 $('#optional_desktop_suite').val('included'); 3103 $('#optional_desktop_suite_users').val(String(users)); 3104 $('#DesktopNum').val(String(users)); 3105 syncChoices(); 3106 renderSummary(); 3107 pulseAndAdvance($btn, function () { 3108 advanceWizardFromButton($btn); 3109 }); 3110 return; 3111 } 3112 3113 if (stepName === 'desktop_office') { 3114 if (getSubscriptionPeriod() !== 'annually') { 3115 $('#optional_ai_assistant').val('included'); 3116 $('#optional_ai_assistant_users').val('0'); 3117 syncChoices(); 3118 renderSummary(); 3119 pulseAndAdvance($btn, function () { 3120 advanceWizardFromButton($btn); 3121 }); 3122 return; 3123 } 3124 3125 const users = parseInt(($('.js-desktop-office-users').val() || '').trim(), 10); 3126 if (!users || users < 1) { 3127 return; 3128 } 3129 $('#optional_ai_assistant').val('included'); 3130 $('#optional_ai_assistant_users').val(String(users)); 3131 syncChoices(); 3132 renderSummary(); 3133 pulseAndAdvance($btn, function () { 3134 advanceWizardFromButton($btn); 3135 }); 3136 } 3137 }); 3138 3139 $(document).on('input', '.js-quote-name-input', function () { 3140 syncQuoteNameToForm(); 3141 setFeedback('quote-contact-feedback', ''); 3142 }); 3143 3144 $(document).on('input', '.js-quote-company-input', function () { 3145 syncQuoteCompanyToForm(); 3146 setFeedback('quote-contact-feedback', ''); 3147 }); 3148 3149 $(document).on('change', '.js-quote-solution-input', function () { 3150 syncQuoteSolutionToForm(); 3151 }); 3152 3153 function updateQuoteCountrySelectStyle() { 3154 const $country = $('.js-quote-country-input'); 3155 if (!$country.length) { 3156 return; 3157 }
3158 $country.toggleClass('is-placeholder', !($country.val() || '').trim()); 3159 } 3160 3161 $(document).on('change', '.js-quote-country-input', function () { 3162 const iso2 = ($(this).val() || '').trim(); 3163 applyPhonePrefixForCountry(iso2, $('.js-quote-phone-prefix-input')); 3164 syncQuoteContactToForm(); 3165 updateQuoteCountrySelectStyle(); 3166 setFeedback('quote-contact-feedback', ''); 3167 }); 3168 3169 if ($('.js-quote-country-input').length) { 3170 initVisitorCountryAndPrefix({ 3171 $country: $('.js-quote-country-input'), 3172 $prefix: $('.js-quote-phone-prefix-input'), 3173 onAfter: function () { 3174 syncQuoteContactToForm(); 3175 updateQuoteCountrySelectStyle(); 3176 }, 3177 }); 3178 } 3179 3180 $(document).on('change', '.js-company-country-input', function () { 3181 applyPhonePrefixForCountry(($(this).val() || '').trim(), $('.js-company-phone-prefix-input')); 3182 syncCloudContactToForm(); 3183 updateCompanyCountrySelectStyle(); 3184 }); 3185 3186 $(document).on( 3187 'input change', 3188 '.js-company-form-fields input, .js-company-form-fields select', 3189 function () { 3190 clearCompanyFieldErrorForInput($(this)); 3191 syncCloudContactToForm(); 3192 syncCompanyNameToForm(); 3193 syncCloudCompanyToForm(); 3194 } 3195 ); 3196 3197 $(document).on('input change', '.js-quote-email-input, .js-quote-phone-input, .js-quote-phone-prefix-input', function () { 3198 syncQuoteContactToForm(); 3199 setFeedback('quote-contact-feedback', ''); 3200 }); 3201 3202 $(document).on('blur', '.js-quote-email-input, .js-company-email-input', function () { 3203 const $input = $(this); 3204 const email = ($input.val() || '').trim(); 3205 if (!email) { 3206 return; 3207 } 3208 if (!isCompanyEmailValid(email)) { 3209 setCompanyFieldError($input, getI18n('quoteEmailInvalid', 'Enter a valid email address.')); 3210 } else { 3211 dismissCompanyFieldError($input.closest('.new-onboarding__company-field').find('.new-onboarding__field-error')); 3212 $input.removeClass('is-invalid'); 3213 } 3214 }); 3215 3216 $(document).on('input', '.js-quote-email-input', function () { 3217 const $input = $(this); 3218 if ($input.hasClass('is-invalid')) { 3219 dismissCompanyFieldError($input.closest('.new-onboarding__company-field').find('.new-onboarding__field-error')); 3220 $input.removeClass('is-invalid'); 3221 } 3222 }); 3223 3224 $(document).on('click', '.new-onboarding__summary-header', function () { 3225 if (window.innerWidth <= 767) { 3226 const $panel = $('.new-onboarding__summary-panel'); 3227 const isOpen = $panel.hasClass('is-mobile-open'); 3228 if (isOpen) { 3229 $panel[0].scrollTop = 0; 3230 } 3231 $panel.toggleClass('is-mobile-open'); 3232 $('.new-onboarding__summary-toggle').attr( 3233 'aria-expanded', 3234 !isOpen ? 'true' : 'false' 3235 ); 3236 } 3237 }); 3238 3239 $(document).on('click', '.js-back-to-pricing', function () { 3240 if (viewTransitionLock) { 3241 return; 3242 } 3243 showLandingMode(); 3244 scheduleDeferredWizardReset(); 3245 }); 3246 3247 $(document).on('click', '.js-subscription-choice', function () { 3248 const $btn = $(this).closest('.js-subscription-choice'); 3249 const period = String($btn.attr('data-period') || '').trim(); 3250 $('#subscriptionPeriod').val(period); 3251 const $all = findStepElement('subscription').find('.js-subscription-choice'); 3252 $all.removeClass('is-selected').attr('aria-pressed', 'false'); 3253 $btn.addClass('is-selected').attr('aria-pressed', 'true'); 3254 updateDesktopClientUsersPrice(); 3255 updateDesktopOfficeUsersPrice(); 3256 3257 if (period === 'annually') { 3258 setSubscriptionUsersWrapOpen(true); 3259 } else { 3260 setSubscriptionUsersWrapOpen(false); 3261 } 3262 3263 renderSummary(); 3264 3265 if (period === 'monthly') { 3266 pulseAndAdvance($btn, function () { 3267 goToNextStep(getWizardStepNameFromElement($btn)); 3268 }); 3269 return; 3270 } 3271 3272 const $input = findStepElement('subscription').find('.js-subscription-users-input'); 3273 if ($input.length) { 3274 window.setTimeout(function () { 3275 focusWizardField($input); 3276 }, 120); 3277 } 3278 }); 3279 3280 $(document).on('input change', '.js-subscription-users-input', function () { 3281 updateSubscriptionUsersPrice(); 3282 const users = getSubscriptionUsersCount(true); 3283 state.selectedUsers = users; 3284 $('#UsersNum').val(String(users)); 3285 renderSummary(); 3286 }); 3287 3288 $(document).on('click', '.js-mobile-sync-choice', function () { 3289 const $btn = $(this); 3290 $('#optional_mobile_sync').val($btn.data('value')); 3291 syncChoices(); 3292 renderSummary(); 3293 pulseAndAdvance($btn, function () { 3294 goToNextStep(getWizardStepNameFromElement($btn)); 3295 }); 3296 }); 3297 3298 $(document).on('click', '.js-support-choice', function () { 3299 const $btn = $(this); 3300 $('#support_option').val($btn.data('value')); 3301 syncChoices(); 3302 renderSummary(); 3303 pulseAndAdvance($btn, function () { 3304 goToNextStep(getWizardStepNameFromElement($btn)); 3305 }); 3306 }); 3307 3308 $(document).on('click', '.js-desktop-client-choice', function () { 3309 const $btn = $(this); 3310 const value = $btn.data('value'); 3311 $('#optional_desktop_suite').val(value); 3312 if (value === 'none') { 3313 $('#optional_desktop_suite_users').val('0'); 3314 $('#DesktopNum').val('0'); 3315 syncChoices(); 3316 renderSummary(); 3317 pulseAndAdvance($btn, function () { 3318 goToNextStep(getWizardStepNameFromElement($btn)); 3319 }); 3320 return; 3321 } 3322 syncChoices(); 3323 renderSummary(); 3324 if (getSubscriptionPeriod() !== 'annually') { 3325 pulseAndAdvance($btn, function () { 3326 goToNextStep(getWizardStepNameFromElement($btn)); 3327 }); 3328 } 3329 }); 3330 3331 $(document).on('click', '.js-desktop-office-choice', function () { 3332 const $btn = $(this); 3333 const value = $btn.data('value'); 3334 $('#optional_ai_assistant').val(value); 3335 if (value === 'none') { 3336 $('#optional_ai_assistant_users').val('0'); 3337 syncChoices(); 3338 renderSummary(); 3339 pulseAndAdvance($btn, function () { 3340 goToNextStep(getWizardStepNameFromElement($btn)); 3341 }); 3342 return; 3343 } 3344 syncChoices(); 3345 renderSummary(); 3346 if (getSubscriptionPeriod() !== 'annually') { 3347 pulseAndAdvance($btn, function () { 3348 goToNextStep(getWizardStepNameFromElement($btn)); 3349 }); 3350 } 3351 }); 3352 3353 $('.new-onboarding__features-trigger').on('mouseleave', function () { 3354 $(this).removeClass('is-dismissed'); 3355 }); 3356 3357 $(document).on('click', '.js-summary-change', function () { 3358 if (viewTransitionLock) { 3359 return; 3360 } 3361 if ($(this).data('step') !== 'subscription') { 3362 return; 3363 } 3364 3365 const current = $('#subscriptionPeriod').val(); 3366 if (!current) { 3367 return; 3368 } 3369 3370 $('#subscriptionPeriod').val(current === 'annually' ? 'monthly' : 'annually'); 3371 syncChoices(); 3372 updateSubscriptionUsersPrice(); 3373 renderSummary(); 3374 }); 3375 3376 $(document).on('click', '.js-summary-remove', function () { 3377 removeSummaryItem($(this).data('remove')); 3378 }); 3379 3380 $(document).on('click', '.js-summary-edit', function () { 3381 if (viewTransitionLock) { 3382 return; 3383 } 3384 const stepName = $(this).data('step'); 3385 if (!stepName || state.flow.indexOf(stepName) === -1) { 3386 return; 3387 } 3388 showStep(stepName, { forceScroll: true }); 3389 }); 3390 3391 $('#dc').on('change', function () { 3392 state.checkoutMounted = false;
3393 $('#checkout').empty(); 3394 updatePlanCardPrices($(this).val()); 3395 updatePlanHiddenFields(); 3396 renderSummary(); 3397 }); 3398 3399 updatePlanCardPrices($('#dc').val()); 3400 3401 $('#create-order-button').on('click', function () { 3402 if (state.currentStep !== 'company') return; 3403 3404 if (!updatePlanHiddenFields()) return; 3405 if (!validateCompanyStepForm()) return; 3406 3407 const $btn = $(this); 3408 setButtonLoading($btn, true); 3409 3410 $.ajax({ 3411 url: config.endpoints.createCloudOrder, 3412 type: 'POST', 3413 data: serializeForm(), 3414 complete: function () { 3415 setButtonLoading($btn, false); 3416 }, 3417 success: function (resp) { 3418 const orderCreated = resp && ( 3419 resp.success === true || 3420 !!resp.email_verification_string || 3421 !!resp.onice_domain || 3422 resp.status === 'processing' 3423 ); 3424 if (orderCreated) { 3425 clearCompanyFieldErrors(); 3426 state.email = resp.email; 3427 if (resp.onice_domain) { 3428 state.oniceDomain = resp.onice_domain; 3429 } 3430 advanceActiveWizardStep(); 3431 return; 3432 } 3433 3434 if (resp && (resp.mail || resp.error === 'mail')) { 3435 setFeedback( 3436 'create-order-feedback', 3437 getI18n('createOrderMailError', 'Unable to send verification email. Please try again.'), 3438 true 3439 ); 3440 return; 3441 } 3442 3443 if (applyServerCompanyErrors(resp)) { 3444 return; 3445 } 3446 3447 setFeedback( 3448 'create-order-feedback', 3449 getI18n('createOrderRetryError', 'Unable to create order. Please retry.'), 3450 true 3451 ); 3452 }, 3453 error: function (xhr) { 3454 if (xhr.responseJSON && applyServerCompanyErrors(xhr.responseJSON)) { 3455 return; 3456 } 3457 setFeedback( 3458 'create-order-feedback', 3459 getI18n('createOrderRetryError', 'Unable to create order. Please retry.'), 3460 true 3461 ); 3462 }, 3463 }); 3464 }); 3465 3466 function setVerifyInvalid(isInvalid) { 3467 $('.new-onboarding__verify').toggleClass('is-invalid', !!isInvalid); 3468 $('#verify-error').prop('hidden', !isInvalid); 3469 } 3470 3471 function getVerifyCode() { 3472 return $('.verify-digit').map(function () { 3473 return $(this).val(); 3474 }).get().join(''); 3475 } 3476 3477 function submitVerifyCode() { 3478 if (state.currentStep !== 'verify') return; 3479 if (state.verifyInProgress) return; 3480 3481 const code = getVerifyCode(); 3482 if (code.length !== 4 || !state.email) { 3483 setVerifyInvalid(true); 3484 return; 3485 } 3486 3487 state.verifyInProgress = true; 3488 $.ajax({ 3489 url: config.endpoints.verifyCode, 3490 type: 'POST', 3491 data: { email: state.email, code: code }, 3492 success: function (resp) { 3493 state.verifyInProgress = false; 3494 if (resp && resp.success) { 3495 setVerifyInvalid(false); 3496 $('.verify-digit').prop('disabled', true); 3497 advanceActiveWizardStep(); 3498 return; 3499 } 3500 setVerifyInvalid(true); 3501 }, 3502 error: function () { 3503 state.verifyInProgress = false; 3504 setVerifyInvalid(true); 3505 }, 3506 }); 3507 } 3508 3509 $('#resend-code-button').on('click', function () { 3510 if (state.currentStep !== 'verify') return; 3511 if (isResendCodeCooldownActive()) return; 3512 if (!state.email) { 3513 setVerifyInvalid(true); 3514 return; 3515 } 3516 3517 setVerifyInvalid(false); 3518 $('.verify-digit').prop('disabled', false).val(''); 3519 $('.verify-digit').first().trigger('focus'); 3520 startResendCodeCooldown(); 3521 3522 $.ajax({ 3523 url: config.endpoints.resendCode, 3524 type: 'POST', 3525 data: { email: state.email }, 3526 }); 3527 }); 3528 3529 var iwDomainCheckTimer = null; 3530 3531 function setDomainAvailabilityMsg(text) { 3532 const $msg = $('#domain-availability-msg'); 3533 if (text) { 3534 $msg.text(text).addClass('is-visible'); 3535 } else { 3536 $msg.removeClass('is-visible'); 3537 } 3538 } 3539 3540 function checkIwDomainAvailability() { 3541 const domain = ($('#iwEmailDomain').val() || '').trim(); 3542 const $btn = $('#check-domain-and-continue'); 3543 setDomainAvailabilityMsg(''); 3544 $btn.prop('disabled', true); 3545 if (!domain || domain.length <= 4) return; 3546 $btn.addClass('is-loading'); 3547 $.ajax({ 3548 url: config.endpoints.checkDomain, 3549 type: 'POST', 3550 data: { domain: domain }, 3551 success: function (resp) { 3552 $btn.removeClass('is-loading'); 3553 if (resp && resp.available) { 3554 $btn.prop('disabled', false); 3555 } else { 3556 setDomainAvailabilityMsg(getI18n('domainNotAvailable', 'Domain is not available.')); 3557 } 3558 }, 3559 error: function () { 3560 $btn.removeClass('is-loading'); 3561 setDomainAvailabilityMsg(getI18n('domainCheckFailed', 'Domain check failed.')); 3562 }, 3563 }); 3564 } 3565 3566 $(document).on('input', '#iwEmailDomain', function () { 3567 $(this).val(($(this).val() || '').replace(/[^A-Za-z0-9]/g, '')); 3568 setDomainAvailabilityMsg(''); 3569 $('#check-domain-and-continue').prop('disabled', true).removeClass('is-loading'); 3570 clearTimeout(iwDomainCheckTimer); 3571 iwDomainCheckTimer = setTimeout(checkIwDomainAvailability, 400); 3572 }); 3573 3574 $(document).on('input', '#iwEmNa', function () { 3575 $(this).val(($(this).val() || '').replace(/[^A-Za-z0-9]/g, '')); 3576 if (!$(this).val()) { 3577 $('#check-domain-and-continue').prop('disabled', true); 3578 } 3579 }); 3580 3581 $('#check-domain-and-continue').on('click', async function () { 3582 if (state.currentStep !== 'domain') return; 3583 const domain = ($('#iwEmailDomain').val() || '').trim(); 3584 const user = ($('#iwEmNa').val() || '').trim(); 3585 if (!domain || !user) { 3586 setFeedback('domain-feedback', 'Please fill email name and domain.', true); 3587 return; 3588 } 3589 const $btn = $(this); 3590 setButtonLoading($btn, true); 3591 const mounted = await renderEmbeddedCheckout(); 3592 setButtonLoading($btn, false); 3593 if (!mounted) return; 3594 advanceActiveWizardStep(); 3595 }); 3596 3597 $root.on('click', '.js-verify-change-email', function (event) { 3598 event.preventDefault(); 3599 goBackFromVerifyToCompanyEmail(); 3600 }); 3601 3602 $('.verify-digit').on('input', function () {
3603 setVerifyInvalid(false); 3604 const val = $(this).val(); 3605 if (val.length > 1) { 3606 $(this).val(val.substring(0, 1)); 3607 } 3608 if ($(this).val() && $(this).next('.verify-digit').length) { 3609 $(this).next('.verify-digit').focus(); 3610 } 3611 if (getVerifyCode().length === 4) { 3612 submitVerifyCode(); 3613 } 3614 }); 3615 3616 $('.verify-digit').on('paste', function (e) { 3617 const clipboard = (e.originalEvent || e).clipboardData || window.clipboardData; 3618 if (!clipboard) return; 3619 const digits = (clipboard.getData('text') || '').replace(/\D/g, ''); 3620 if (!digits) return; 3621 3622 e.preventDefault(); 3623 setVerifyInvalid(false); 3624 3625 const $inputs = $('.verify-digit'); 3626 const startIndex = $inputs.index(this); 3627 for (let i = 0; i < digits.length && startIndex + i < $inputs.length; i++) { 3628 $inputs.eq(startIndex + i).val(digits.charAt(i)); 3629 } 3630 3631 const lastFilled = Math.min(startIndex + digits.length, $inputs.length) - 1; 3632 $inputs.eq(Math.max(lastFilled, 0)).focus(); 3633 3634 if (getVerifyCode().length === 4) { 3635 submitVerifyCode(); 3636 } 3637 }); 3638 3639 $('.verify-digit').on('keydown', function (e) { 3640 if (e.key === 'Backspace' && !$(this).val() && $(this).prev('.verify-digit').length) { 3641 $(this).prev('.verify-digit').focus(); 3642 return; 3643 } 3644 3645 if (e.key === 'Enter') { 3646 e.preventDefault(); 3647 const $next = $(this).next('.verify-digit'); 3648 if ($next.length) { 3649 $next.trigger('focus'); 3650 return; 3651 } 3652 submitVerifyCode(); 3653 } 3654 }); 3655 3656 function getHeaderOffset() { 3657 const root = $root[0]; 3658 if (!root) { 3659 return 80; 3660 } 3661 const raw = window.getComputedStyle(root).getPropertyValue('--new-onboarding-header-offset').trim(); 3662 const parsed = parseInt(raw, 10); 3663 return Number.isFinite(parsed) ? parsed : 80; 3664 } 3665 3666 function syncCompareHeaderPricesFromCards() { 3667 $('.new-onboarding__plan-card').each(function () { 3668 const packageName = String($(this).find('.js-start-onboarding').first().data('package') || ''); 3669 if (!packageName) { 3670 return; 3671 } 3672 3673 const price = ($(this).find('.new-onboarding__plan-price').first().text() || '').trim(); 3674 if (!price) { 3675 return; 3676 } 3677 3678 $('[data-compare-plan="' + packageName + '"]').text(price); 3679 }); 3680 } 3681 3682 function getCompareHeaderOffset($compare) { 3683 const compareEl = $compare && $compare[0]; 3684 if (!compareEl) { 3685 return 56; 3686 } 3687 const raw = window.getComputedStyle(compareEl).getPropertyValue('--new-onboarding-compare-header-offset').trim(); 3688 const parsed = parseInt(raw, 10); 3689 return Number.isFinite(parsed) ? parsed : 56; 3690 } 3691 3692 function scrollToCompareFeature(scrollTarget) { 3693 const target = (scrollTarget || '').trim(); 3694 if (!target) { 3695 return; 3696 } 3697 3698 const $row = $('#onboarding-compare-table tbody tr[data-compare-scroll="' + target + '"]').first(); 3699 if (!$row.length) { 3700 return; 3701 } 3702 3703 const $compare = $root.find('.new-onboarding__compare'); 3704 const $fixedHeader = $('#onboarding-compare-header-fixed'); 3705 const headerOffset = getCompareHeaderOffset($compare); 3706 const pinnedHeaderHeight = $fixedHeader.outerHeight() || 0; 3707 const scrollPadding = headerOffset + pinnedHeaderHeight + 12; 3708 const top = $row.offset().top - scrollPadding; 3709 const duration = prefersReducedMotion() ? 0 : 520; 3710 3711 $('html, body').stop(true).animate( 3712 { scrollTop: Math.max(0, top) }, 3713 { duration: duration, easing: 'swing', complete: function () { 3714 $(window).trigger('resize.compareTableHeader'); 3715 } } 3716 ); 3717 } 3718 3719 function initCompareFeatureModal() { 3720 const modals = config.compareFeatureModals || {}; 3721 const $modal = $('#onboarding-compare-feature-modal'); 3722 if (!$modal.length) { 3723 return; 3724 } 3725 3726 // preload feature images so the modal doesn't reflow on first open 3727 Object.keys(modals).forEach(function (id) { 3728 if (modals[id] && modals[id].image) { 3729 new Image().src = modals[id].image; 3730 } 3731 }); 3732 3733 const $modalBox = $modal.find('.new-onboarding__feature-modal-box'); 3734 const $modalIcon = $modal.find('.new-onboarding__feature-modal-icon'); 3735 const $modalTitle = $modal.find('.new-onboarding__feature-modal-title'); 3736 const $modalSubtitle = $modal.find('.new-onboarding__feature-modal-subtitle'); 3737 const $modalImage = $modal.find('.new-onboarding__feature-modal-image');
3738 let closeTimer = null; 3739 3740 const resetModal = function () { 3741 $modalIcon.attr('src', '').hide(); 3742 $modalTitle.text(''); 3743 $modalSubtitle.html(''); 3744 $modalImage.attr('src', '').show(); 3745 $modalBox.removeClass('is-layout-side-by-side'); 3746 }; 3747 3748 const closeFeatureModal = function () { 3749 $modal.removeClass('is-visible'); 3750 $modalBox.removeClass('is-visible'); 3751 closeTimer = window.setTimeout(function () { 3752 resetModal(); 3753 $modal.attr('hidden', 'hidden').hide(); 3754 closeTimer = null; 3755 }, 300); 3756 }; 3757 3758 const openFeatureModal = function (modalId, iconSrc) { 3759 const data = modals[modalId]; 3760 if (!data) { 3761 return; 3762 } 3763 3764 if (closeTimer) { 3765 window.clearTimeout(closeTimer); 3766 closeTimer = null; 3767 } 3768 3769 if (iconSrc) { 3770 $modalIcon.attr('src', iconSrc).show(); 3771 } else { 3772 $modalIcon.attr('src', '').hide(); 3773 } 3774 $modalTitle.text(data.title || ''); 3775 $modalSubtitle.html(data.subtitle || ''); 3776 3777 if (data.image) { 3778 $modalImage.attr('src', data.image).show(); 3779 } else { 3780 $modalImage.attr('src', '').hide(); 3781 } 3782 3783 if (data.layout === 'side-by-side') { 3784 $modalBox.addClass('is-layout-side-by-side'); 3785 } 3786 3787 $modal.removeAttr('hidden').css('display', 'flex'); 3788 if ($modal[0]) { 3789 $modal[0].offsetHeight; 3790 } 3791 $modal.addClass('is-visible'); 3792 $modalBox.addClass('is-visible'); 3793 }; 3794 3795 $root.on('click', '.js-compare-feature-modal', function (event) { 3796 event.preventDefault(); 3797 const modalId = $(this).data('modal'); 3798 const iconSrc = $(this) 3799 .closest('.new-onboarding__compare-feature') 3800 .find('.new-onboarding__compare-feature-icon') 3801 .attr('src'); 3802 openFeatureModal(modalId, iconSrc); 3803 }); 3804 3805 $modal.on('click', function () { 3806 closeFeatureModal(); 3807 }); 3808 3809 $modalBox.on('click', function (event) { 3810 event.stopPropagation(); 3811 }); 3812 3813 $modal.find('.new-onboarding__feature-modal-close').on('click', function (event) { 3814 event.preventDefault(); 3815 event.stopPropagation(); 3816 closeFeatureModal(); 3817 }); 3818 } 3819 3820 function isMobileCompareView() { 3821 return window.matchMedia('(max-width: 767px)').matches; 3822 } 3823 3824 const compareCarouselState = { 3825 index: 0, 3826 programmaticScroll: false, 3827 scrollEndTimer: null, 3828 }; 3829 3830 function getCompareCarouselViewport() { 3831 return document.getElementById('onboarding-compare-carousel-viewport'); 3832 } 3833 3834 function getCompareCarouselSlideWidth(viewportEl) { 3835 if (!viewportEl) { 3836 return 0; 3837 } 3838 3839 const slide = viewportEl.querySelector('.new-onboarding__compare-carousel-slide'); 3840 if (!slide) { 3841 return 0; 3842 } 3843 3844 return slide.getBoundingClientRect().width || viewportEl.clientWidth || 0; 3845 } 3846 3847 function getCompareCarouselIndexFromScroll(viewportEl) { 3848 const width = getCompareCarouselSlideWidth(viewportEl); 3849 if (!width) { 3850 return 0; 3851 } 3852 3853 return Math.round(viewportEl.scrollLeft / width); 3854 } 3855 3856 function scrollCompareCarouselToIndex(index, animate) { 3857 const viewportEl = getCompareCarouselViewport(); 3858 if (!viewportEl) { 3859 return; 3860 } 3861 3862 const $slides = $('#onboarding-compare-carousel .new-onboarding__compare-carousel-slide'); 3863 const total = $slides.length; 3864 if (!total) { 3865 return; 3866 } 3867 3868 const safeIndex = ((index % total) + total) % total; 3869 const width = getCompareCarouselSlideWidth(viewportEl); 3870 if (!width) { 3871 return; 3872 } 3873 3874 compareCarouselState.programmaticScroll = true; 3875 viewportEl.scrollTo({ 3876 left: safeIndex * width, 3877 behavior: animate && !prefersReducedMotion() ? 'smooth' : 'auto', 3878 }); 3879 3880 window.clearTimeout(compareCarouselState.scrollEndTimer); 3881 compareCarouselState.scrollEndTimer = window.setTimeout(function () { 3882 compareCarouselState.programmaticScroll = false; 3883 }, animate && !prefersReducedMotion() ? 420 : 0); 3884 } 3885 3886 function setComparePlanIndex(index, options) { 3887 const opts = options || {}; 3888 const $compare = $('#onboarding-compare'); 3889 const $carousel = $('#onboarding-compare-carousel'); 3890 const $slides = $carousel.find('.new-onboarding__compare-carousel-slide'); 3891 const total = $slides.length; 3892 if (!$compare.length || !total) { 3893 return; 3894 } 3895 3896 const safeIndex = ((index % total) + total) % total; 3897 compareCarouselState.index = safeIndex; 3898 $compare.removeClass('is-compare-plan-0 is-compare-plan-1 is-compare-plan-2'); 3899 $compare.addClass('is-compare-plan-' + safeIndex); 3900 $slides.removeClass('is-active').eq(safeIndex).addClass('is-active'); 3901 $carousel.find('.js-compare-carousel-dot').each(function () { 3902 const $dot = $(this); 3903 const isActive = parseInt($dot.data('comparePlanIndex'), 10) === safeIndex; 3904 $dot.toggleClass('is-active', isActive).attr('aria-selected', isActive ? 'true' : 'false'); 3905 }); 3906 $carousel.toggleClass('is-at-start', safeIndex === 0).toggleClass('is-at-end', safeIndex === total - 1); 3907 3908 if (!opts.skipScroll && isMobileCompareView()) { 3909 scrollCompareCarouselToIndex(safeIndex, opts.animate !== false); 3910 } 3911 3912 $(window).trigger('resize.compareTableHeader'); 3913 } 3914 3915 function initComparePlanCarousel() { 3916 const viewportEl = getCompareCarouselViewport(); 3917 const $carousel = $('#onboarding-compare-carousel'); 3918 if (!viewportEl || !$carousel.length) { 3919 return; 3920 } 3921 3922 const syncIndexFromScroll = function () { 3923 if (compareCarouselState.programmaticScroll) { 3924 return; 3925 } 3926 3927 const nextIndex = getCompareCarouselIndexFromScroll(viewportEl); 3928 if (nextIndex !== compareCarouselState.index) { 3929 setComparePlanIndex(nextIndex, { skipScroll: true, animate: false }); 3930 } 3931 }; 3932 3933 viewportEl.addEventListener( 3934 'scroll', 3935 function () { 3936 window.requestAnimationFrame(syncIndexFromScroll); 3937 }, 3938 { passive: true } 3939 ); 3940 3941 $root.on('click', '.js-compare-carousel-prev', function () { 3942 setComparePlanIndex(compareCarouselState.index - 1, { animate: true }); 3943 }); 3944 3945 $root.on('click', '.js-compare-carousel-next', function () { 3946 setComparePlanIndex(compareCarouselState.index + 1, { animate: true }); 3947 }); 3948 3949 $root.on('click', '.js-compare-carousel-dot', function () { 3950 const index = parseInt($(this).data('comparePlanIndex'), 10); 3951 if (!Number.isFinite(index)) { 3952 return; 3953 } 3954 setComparePlanIndex(index, { animate: true }); 3955 }); 3956 3957 $(window).on('resize.compareCarousel', function () { 3958 if (!isMobileCompareView()) { 3959 return; 3960 } 3961 scrollCompareCarouselToIndex(compareCarouselState.index, false); 3962 }); 3963 3964 setComparePlanIndex(0, { skipScroll: true, animate: false }); 3965 window.requestAnimationFrame(function () { 3966 scrollCompareCarouselToIndex(0, false); 3967 }); 3968 } 3969 3970 function initCompareTableHeader() { 3971 const $compare = $root.find('.new-onboarding__compare'); 3972 const $table = $('#onboarding-compare-table'); 3973 const $fixedHeader = $('#onboarding-compare-header-fixed'); 3974 if (!$compare.length || !$table.length || !$fixedHeader.length) { 3975 return; 3976 } 3977 3978 const $fixedInner = $fixedHeader.find('.new-onboarding__compare-header-fixed-inner'); 3979 const $fixedThs = $fixedHeader.find('thead th'); 3980 const tableEl = $table[0]; 3981 const fixedEl = $fixedHeader[0]; 3982 const fixedInnerEl = $fixedInner[0]; 3983 const earlyRevealPx = 300; 3984 let ticking = false;
3985 3986 const syncCompareHeaderLayout = function () { 3987 if (isMobileCompareView()) { 3988 $fixedHeader.removeClass('is-visible'); 3989 fixedEl.setAttribute('aria-hidden', 'true'); 3990 ticking = false; 3991 return; 3992 } 3993 3994 const headerOffset = getCompareHeaderOffset($compare); 3995 const tableRect = tableEl.getBoundingClientRect(); 3996 const headerHeight = $fixedHeader.outerHeight() || 0; 3997 const pinLine = headerOffset + earlyRevealPx; 3998 const shouldPin = tableRect.top <= pinLine && tableRect.bottom > headerOffset + headerHeight; 3999 4000 fixedEl.style.top = headerOffset + 'px'; 4001 fixedEl.style.left = '0'; 4002 fixedEl.style.right = '0'; 4003 fixedEl.style.width = '100%'; 4004 4005 $fixedHeader.toggleClass('is-visible', shouldPin); 4006 fixedEl.setAttribute('aria-hidden', shouldPin ? 'false' : 'true'); 4007 4008 if (!shouldPin) { 4009 ticking = false; 4010 return; 4011 } 4012 4013 if (fixedInnerEl) { 4014 fixedInnerEl.style.left = tableRect.left + 'px'; 4015 fixedInnerEl.style.width = tableRect.width + 'px'; 4016 fixedInnerEl.style.marginLeft = ''; 4017 } 4018 4019 const $columns = $table.find('tbody tr').first().children('td'); 4020 $columns.each(function (index) { 4021 const width = $(this).outerWidth(); 4022 const $th = $fixedThs.eq(index); 4023 if (!$th.length || !width) { 4024 return; 4025 } 4026 $th.css({ 4027 width: width + 'px', 4028 minWidth: width + 'px', 4029 maxWidth: width + 'px', 4030 }); 4031 }); 4032 4033 ticking = false; 4034 }; 4035 4036 const scheduleCompareHeaderLayout = function () { 4037 if (ticking) { 4038 return; 4039 } 4040 ticking = true; 4041 window.requestAnimationFrame(syncCompareHeaderLayout); 4042 }; 4043 4044 $(window).on('scroll.compareTableHeader resize.compareTableHeader', scheduleCompareHeaderLayout); 4045 4046 if (typeof window.ResizeObserver === 'function') { 4047 const resizeObserver = new window.ResizeObserver(scheduleCompareHeaderLayout); 4048 resizeObserver.observe(tableEl); 4049 if ($root[0]) { 4050 resizeObserver.observe($root[0]); 4051 } 4052 } 4053 4054 scheduleCompareHeaderLayout(); 4055 } 4056 4057 state.selectedPackage = normalizePackageName(state.selectedPackage); 4058 state.flow = getFlow(state.selectedPackage); 4059 resetWizardState(); 4060 updatePlanHiddenFields(); 4061 showStep(state.flow[0], { scroll: false }); 4062 renderSummary(); 4063 showLandingMode({ animate: false }); 4064 window.requestAnimationFrame(function () { 4065 updateModeSwitchThumb(); 4066 }); 4067 initComparePlanCarousel(); 4068 initCompareTableHeader(); 4069 initCompareFeatureModal(); 4070})();
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.