PageSourceSearch

https://icewarp.com.br/js/redesign/new-onboarding.js?v=1.20260923115057

js icewarp.com.br collected 2026-09-26 13:05:49 UTC 125,319 bytes, 4,070 lines download raw bytes

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.