{"version":3,"file":"js/52279-5ce2c5a5db982540.js","sources":["webpack://cropwise-operations/./app/packs/cropwise_accounts/helpers/dom_utils.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/analytics.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/available_products_card_icons.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/form_submit_guard.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/client_id_param.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/cookie_banner_cleanup.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/language_selector.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/country_flag.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/department_job_title_filter.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/department_job_title_picker.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/phone_dial_code.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/password_strength.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/product_card_icons.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/signup_field_events.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/name_capitalization.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/required_fields_gate.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/campaign_origin.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/track_auth.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/password_recovery_events.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/legal_docs_gate.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/expiry_countdown.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/segmented_code_input.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/two_factor_events.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/mfa_notice_events.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/user_error_events.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/legal_documents_viewer.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/legal_documents_drawer.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/recaptcha_guard.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/application.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/index.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/cookie_settings.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/signup_step_transition.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/password_toggle.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/native_app_handoff.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/legaldoc_links.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/login_error_modal.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/password_confirmation_match.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/two_factor_countdown.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/two_factor_code_input.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/channel_options.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/password_recovery_otp_code_input.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/password_recovery_otp_countdown.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/workspace_selector.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/available_products_modal.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/login/internal/available_products_submit_guard.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/views/change_idp/idp_popup_result.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/helpers/viewport_height.js","webpack://cropwise-operations/./app/packs/cropwise_accounts/locales/i18n-config.js"],"sourcesContent":["/**\n * Runs callback once when an element matching selector exists in the DOM.\n * jQuery-free replacement for helpers/jquery/onElementExistRunOnce.\n *\n * @param {string} selector - CSS selector\n * @param {function} callback - callback function\n */\nexport const onElementExistRunOnce = (selector, callback) => {\n const run = () => {\n if (document.querySelector(selector)) {\n callback();\n }\n };\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', run);\n } else {\n run();\n }\n};\n","import { initAnalytics as cwInitAnalytics } from '@cw-elements/analytics-provider';\n\nconst getMetaContent = (name) => {\n const selector = `meta[name=\"${CSS.escape(name)}\"]`;\n return document.querySelector(selector)?.content ?? null;\n};\n\nconst logInitError = (error) => {\n // Analytics must never interfere with account access.\n console.error('[Analytics] init failed:', error); // eslint-disable-line no-console\n};\n\n// Static RUM options, merged with the per-env values read from meta tags. IP\n// suppression is NOT set here on purpose: with `consent.enabled: true` the\n// provider disables IP shipping itself (CWP071), so it would be redundant.\nexport const DATADOG_OPTIONS = {\n sessionSampleRate: 100,\n sessionReplaySampleRate: 100,\n trackUserInteractions: true,\n trackLongTasks: true,\n trackResources: true,\n defaultPrivacyLevel: 'mask-user-input',\n useSecureSessionCookie: true,\n};\n\n// The accounts pages fire events on the very click/submit that navigates away\n// (sign-in, recover password). The default fetch transport gets cancelled\n// mid-request on navigation, dropping those events; beacon transport survives\n// the unload instead. Cookies are secure-only. `transport`/`cookieOptions` are\n// raw Amplitude SDK init options, so they go under the provider's `options` prop.\n// The SDK-level IP suppression above (CWP071) stays forced on regardless:\n// `sendIpAddressOnConsent` is a provider prop, not an SDK one.\nexport const AMPLITUDE_OPTIONS = {\n autocapture: false,\n sendIpAddressOnConsent: true,\n options: {\n transport: 'beacon',\n cookieOptions: { secure: true },\n },\n};\n\n// The full RUM configuration is forwarded verbatim to the provider's `datadog`\n// block (typed as `@datadog/browser-rum`'s RumInitConfiguration). `service`, `env`\n// and `version` are mandatory: the provider logs and skips RUM initialization\n// entirely when any of them is blank, taking session replay and every\n// `datadogAddAction` down with it.\nexport const buildDatadogConfig = () => {\n const clientToken = getMetaContent('dd-rum-client-token');\n if (!clientToken) return null;\n\n return {\n clientToken,\n applicationId: getMetaContent('dd-rum-application-id'),\n site: getMetaContent('dd-rum-site'),\n service: getMetaContent('dd-rum-service'),\n env: getMetaContent('dd-rum-env'),\n version: getMetaContent('dd-rum-version'),\n ...DATADOG_OPTIONS,\n };\n};\n\nexport const buildAmplitudeConfig = () => {\n const clientId = getMetaContent('amplitude-api-key');\n return clientId ? { clientId, ...AMPLITUDE_OPTIONS } : null;\n};\n\n// Only pass the blocks we actually have config for; the provider treats a missing\n// block as \"don't init that SDK\". Consent is always enabled — the provider reads\n// the CookieBot `statistics` category (the layout still loads CookieBot's uc.js)\n// and stamps every event with the resolved `analytics consent`. That property name\n// carries a space, not an underscore, since provider 6.1.1 renamed it.\nconst buildAnalyticsConfig = () => {\n const amplitude = buildAmplitudeConfig();\n const datadog = buildDatadogConfig();\n if (!amplitude && !datadog) return null;\n\n return {\n ...(amplitude && { amplitude }),\n ...(datadog && { datadog }),\n consent: { enabled: true },\n };\n};\n\n// Amplitude has no built-in unload flush, so a tracked-then-navigate event would\n// sit queued until the flush timer — long after the page is gone. Flush only\n// once the page is actually hidden (the navigation has committed).\nexport const shouldFlushOnVisibility = (visibilityState) => visibilityState === 'hidden';\n\n// The flush runs from unload-path event listeners, outside init's error path, so\n// a throwing flush would surface as an uncaught error. Analytics stays non-critical.\n// `client` is the Amplitude BrowserClient the provider returns from initAnalytics.\nexport const safeFlush = (client) => {\n try {\n client?.flush();\n } catch (error) {\n console.error('[Analytics] Amplitude flush failed:', error); // eslint-disable-line no-console\n }\n};\n\nconst flushAmplitudeWhileAlive = (client) => {\n document.addEventListener('visibilitychange', () => {\n if (shouldFlushOnVisibility(document.visibilityState)) safeFlush(client);\n });\n // pagehide also covers Safari and the bfcache, where visibilitychange may not fire.\n window.addEventListener('pagehide', () => safeFlush(client));\n};\n\nlet markAnalyticsReady;\n\n// The provider drops every call made before init resolves, without buffering, so\n// whatever reports on page load waits on this. Resolved in a `finally`: a failed\n// init makes the provider a no-op, while a pending promise would strand reports.\n// eslint-disable-next-line unicorn/prefer-promise-with-resolvers -- needs Chrome 119\nexport const analyticsReady = new Promise((resolve) => {\n markAnalyticsReady = resolve;\n});\n\n// The provider's initAnalytics is async and returns the Amplitude BrowserClient\n// it created. We await it to wire the unload flush to that client, but the caller\n// (DOMContentLoaded) leaves it fire-and-forget — page init never blocks on\n// analytics, and a rejected init must never break account access.\nexport const initAnalytics = async () => {\n try {\n const config = buildAnalyticsConfig();\n if (!config) return;\n\n const client = await cwInitAnalytics(config);\n if (client) flushAmplitudeWhileAlive(client);\n } catch (error) {\n logInitError(error);\n } finally {\n markAnalyticsReady();\n }\n};\n","/**\n * Swaps a broken app icon on the available-products cards for the Cropwise\n * favicon, so an app whose S3 icon is missing degrades to the brand mark\n * instead of the browser's broken-image glyph.\n */\nconst FALLBACK_ICON_SRC = '/cropwise-accounts-assets/favicon.svg';\n\nconst applyIconFallback = (img) => {\n // Reassigning the same src restarts the request, so a failing fallback would\n // loop between the error listener and the already-failed check below.\n if (img.getAttribute('src') === FALLBACK_ICON_SRC) return;\n\n img.setAttribute('src', FALLBACK_ICON_SRC);\n};\n\nexport const initAvailableProductsCardIcons = () => {\n for (const img of document.querySelectorAll('img.available-products__card-icon')) {\n // The listener alone misses icons that failed before init (error won't\n // re-fire); the immediate check alone misses icons still in flight.\n img.addEventListener('error', () => applyIconFallback(img), { once: true });\n if (img.getAttribute('src') && img.complete && img.naturalWidth === 0) {\n applyIconFallback(img);\n }\n }\n};\n","const LOADING_CLASS = 'devise-form-wrapper__sign-in-button--loading';\n// Keyframe attached to `.devise-form-wrapper__input:-webkit-autofill` in the\n// login layout stylesheet. Chromium fills the field without firing 'input',\n// but it does start this animation, so 'animationstart' is our autofill hook.\nconst AUTOFILL_ANIMATION_NAME = 'cwa-input-autofill';\n\nconst setDisabled = (button, value) => {\n const el = button;\n el.disabled = value;\n};\n\n// Spinner + disabled the moment a submit fires, so a slow connection gives\n// feedback and a second click cannot double-submit. `loadingClass` lets a page\n// outside the login layout reuse this with its own block's modifier.\nexport const startButtonLoading = (button, loadingClass = LOADING_CLASS) => {\n if (!button) return;\n\n const el = button;\n el.classList.add(loadingClass);\n el.setAttribute('aria-busy', 'true');\n el.disabled = true;\n};\n\nconst gateSubmitOnInput = (input, submitButton) => {\n const update = () => {\n if (submitButton.classList.contains(LOADING_CLASS)) return;\n setDisabled(submitButton, input.value.trim() === '');\n };\n input.addEventListener('input', update);\n input.addEventListener('change', update);\n // Firefox fires 'input' on autofill; Chromium does not, so we also re-evaluate\n // the gate when the autofill CSS animation starts (see AUTOFILL_ANIMATION_NAME).\n input.addEventListener('animationstart', (event) => {\n if (event.animationName === AUTOFILL_ANIMATION_NAME) update();\n });\n update();\n};\n\nconst initSignInGuard = () => {\n const form = document.querySelector('.devise-form-login form');\n if (!form) return;\n\n const activeInput = form.querySelector('.devise-form-wrapper__input:not([readonly])');\n const submitButton = form.querySelector('[type=\"submit\"]');\n if (!activeInput || !submitButton) return;\n\n gateSubmitOnInput(activeInput, submitButton);\n\n form.addEventListener('submit', () => startButtonLoading(submitButton));\n};\n\nconst initRecoveryGuard = () => {\n const form = document.querySelector('.devise-form-recovery-password form');\n if (!form) return;\n\n const emailInput = form.querySelector('.devise-form-wrapper__input');\n const submitButton = form.querySelector('[type=\"submit\"]');\n if (!emailInput || !submitButton) return;\n\n gateSubmitOnInput(emailInput, submitButton);\n\n form.addEventListener('submit', () => startButtonLoading(submitButton));\n};\n\nexport const initFormSubmitGuard = () => {\n initSignInGuard();\n initRecoveryGuard();\n};\n","const CLIENT_ID_KEY = 'client_id';\n\nconst storeClientIdFromUrl = () => {\n const params = new URLSearchParams(location.search);\n if (!params.has(CLIENT_ID_KEY)) return;\n const clientId = params.get(CLIENT_ID_KEY);\n if (clientId) {\n sessionStorage.setItem(CLIENT_ID_KEY, clientId);\n return;\n }\n sessionStorage.removeItem(CLIENT_ID_KEY);\n};\n\nconst injectClientIdIntoForms = () => {\n const clientId = sessionStorage.getItem(CLIENT_ID_KEY);\n if (!clientId) return;\n\n for (const form of document.querySelectorAll('form')) {\n if (!form.querySelector(`input[name=\"${CSS.escape(CLIENT_ID_KEY)}\"]`)) {\n const input = document.createElement('input');\n input.type = 'hidden';\n input.name = CLIENT_ID_KEY;\n input.value = clientId;\n form.append(input);\n }\n }\n};\n\n/**\n * Reads client_id from the host app URL query param, persists it in\n * sessionStorage, and injects it as a hidden field into all forms so it\n * is forwarded to POST /signup and POST /v2/accounts/recovery calls.\n */\nexport const initClientIdParam = () => {\n storeClientIdFromUrl();\n injectClientIdIntoForms();\n};\n","/**\n * Cookiebot runs in blocking mode (data-blockingmode=\"auto\"), which injects a\n * full-viewport fixed backdrop (#CybotCookiebotDialogBodyUnderlay) to intercept\n * clicks until consent is given. On small screens the banner renders as a\n * full-screen dialog over that backdrop; after the visitor accepts or declines,\n * Cookiebot hides the dialog but the backdrop can linger, leaving the whole page\n * unclickable. We force-remove the backdrop and clear any scroll lock once the\n * consent choice settles — mirroring how login_error_modal.js removes its own\n * fixed overlay so it can't block interaction.\n */\nconst COOKIEBOT_UNDERLAY_ID = 'CybotCookiebotDialogBodyUnderlay';\n\nconst removeCookiebotBlockingOverlay = () => {\n document.getElementById(COOKIEBOT_UNDERLAY_ID)?.remove();\n\n // Blocking mode can leave the document scroll-locked on small screens; drop\n // any inline overflow lock so the page scrolls again after dismissal.\n document.documentElement.style.removeProperty('overflow');\n document.body.style.removeProperty('overflow');\n};\n\nexport const initCookieBannerCleanup = () => {\n addEventListener('CookiebotOnAccept', removeCookiebotBlockingOverlay);\n addEventListener('CookiebotOnDecline', removeCookiebotBlockingOverlay);\n};\n","/**\n * Initializes the language selector dropdown.\n *\n * - Navigates to the same page with an updated `locale` query param on change.\n * - On browsers without CSS `field-sizing` (Firefox, Chrome < 123), sizes the\n * , but the sign-in\n// form — where the forgot-password link lives — renders the email as\n// . Without the suffixed [email]\n// match, \"user recover password\" finds no email and is skipped entirely.\nexport const SIGN_IN_EMAIL_SELECTOR =\n 'input[type=\"email\"], input[name=\"email\"], input[name$=\"[email]\"]';\n\nconst emailFrom = (root) => normalizeEmail(root?.querySelector(SIGN_IN_EMAIL_SELECTOR)?.value);\n\nconst trackRecoverPassword = (email) => {\n if (!email) return;\n amplitudeTrack('user recover password', {\n component: COMPONENT,\n email,\n action: 'Recovery password',\n });\n};\n\nconst trackResetPassword = (email) => {\n if (!email) return;\n amplitudeTrack('user reset password', {\n component: COMPONENT,\n email,\n action: 'Send email',\n });\n};\n\n// Fire-and-forget and let the link/form navigate normally: analytics.js inits\n// Amplitude with beacon transport and flushes on page hide, so the queued event\n// survives the navigation without us holding up (and breaking) it.\nexport const initPasswordRecoveryEvents = () => {\n // \"user recover password\": the forgot-password link on the sign-in page,\n // carrying the email currently typed into the sign-in form (mirrors the SPA).\n const forgotLink = document.querySelector('.devise-form-wrapper__forgot-password a');\n forgotLink?.addEventListener('click', () => {\n trackRecoverPassword(emailFrom(forgotLink.closest('form') ?? document));\n });\n\n // \"user reset password\": submitting the email on the recovery page.\n const recoveryForm = document.querySelector('.devise-form-recovery-password form');\n recoveryForm?.addEventListener('submit', () => {\n trackResetPassword(emailFrom(recoveryForm));\n });\n};\n","/**\n * Disables the legal-documents submit button until every consent checkbox is\n * checked, matching the Figma design (a tooltip explains why while disabled)\n * instead of only surfacing an error after the user tries to continue.\n */\nexport const initLegalDocsGate = () => {\n const button = document.getElementById('legal-docs-submit-button');\n const checkboxes = document.querySelectorAll('.js-legal-doc-checkbox');\n if (!button || checkboxes.length === 0) return;\n\n const syncDisabledState = () => {\n button.disabled = [...checkboxes].some((checkbox) => !checkbox.checked);\n };\n\n for (const checkbox of checkboxes) {\n checkbox.addEventListener('change', syncDisabledState);\n }\n syncDisabledState();\n};\n","/**\n * Client-side expiration countdown, shared by the 2FA and password-recovery\n * code-verification screens. Renders the remaining MM:SS once per second and\n * enables the resend link when the deadline passes. Each tick recomputes the\n * remainder from an absolute deadline because background tabs throttle\n * (Safari: suspend) timers — a decrementing counter would freeze while the\n * tab is hidden.\n */\n\nconst DEFAULT_DURATION_SECONDS = 600;\n\n/**\n * Formats a second count as MM:SS, clamping negatives to 00:00.\n *\n * @param {number} totalSeconds - remaining seconds\n * @returns {string} zero-padded MM:SS\n */\nexport const formatSeconds = (totalSeconds) => {\n const clamped = Math.max(0, totalSeconds);\n const minutes = String(Math.floor(clamped / 60)).padStart(2, '0');\n const seconds = String(clamped % 60).padStart(2, '0');\n return `${minutes}:${seconds}`;\n};\n\n/**\n * Parses the countdown's starting duration, defaulting when missing or\n * non-numeric. A genuine 0 (already expired) is preserved rather than\n * treated as falsy.\n *\n * @param {string|undefined} rawValue - the data-duration-seconds attribute value\n * @returns {number} whole seconds to count down from\n */\nexport const parseDurationSeconds = (rawValue) => {\n const numeric = Number(rawValue);\n return Number.isFinite(numeric) ? Math.trunc(numeric) : DEFAULT_DURATION_SECONDS;\n};\n\n/**\n * Whole seconds left until the deadline, rounded up so a partial second still\n * counts as remaining and the code never reads as expired early. Recomputing\n * from the absolute deadline keeps a late (throttled) tick on the right value.\n * Zero at the deadline, negative once past it; the caller treats <= 0 as expired.\n *\n * @param {number} deadlineMs - deadline as an epoch millisecond timestamp\n * @param {number} nowMs - current epoch millisecond timestamp\n * @returns {number} seconds remaining\n */\nexport const remainingSeconds = (deadlineMs, nowMs) => Math.ceil((deadlineMs - nowMs) / 1000);\n\n/**\n * @param {object} options\n * @param {string} options.timeElementId - id of the element showing MM:SS\n * @param {string} options.resendElementId - id of the resend button/link to enable at zero\n */\nexport const initExpiryCountdown = ({ timeElementId, resendElementId }) => {\n const timeElement = document.getElementById(timeElementId);\n if (!timeElement) return;\n\n const enableResend = () => {\n const resendLink = document.getElementById(resendElementId);\n if (!resendLink) return;\n\n resendLink.disabled = false;\n };\n\n const durationSeconds = parseDurationSeconds(timeElement.dataset.durationSeconds);\n timeElement.textContent = formatSeconds(durationSeconds);\n\n // Already expired on load — enable resend now instead of after the first tick.\n if (durationSeconds <= 0) {\n enableResend();\n return;\n }\n\n const deadline = Date.now() + durationSeconds * 1000;\n let timer;\n const teardown = new AbortController();\n\n function tick() {\n const remaining = remainingSeconds(deadline, Date.now());\n timeElement.textContent = formatSeconds(remaining);\n if (remaining > 0) return;\n\n clearInterval(timer);\n teardown.abort();\n enableResend();\n }\n\n timer = setInterval(tick, 1000);\n // Snap the display when the tab becomes visible again instead of waiting for\n // the next (possibly still-throttled) interval tick; the AbortController\n // detaches this listener the moment the countdown ends.\n document.addEventListener('visibilitychange', () => {\n if (!document.hidden) tick();\n }, { signal: teardown.signal });\n};\n","/**\n * Wires a segmented digit-box code input: digit-only filtering, auto-advance,\n * backspace focus-back, distribution of multi-character input (paste or fast\n * typing) across the boxes, enabling the Continue button once every box holds\n * a digit, and mirroring the assembled code into the hidden field the form\n * submits. Shared by the 2FA and password-recovery code-verification screens.\n */\n\n/**\n * Strips every non-digit character.\n *\n * @param {string} value - raw input value\n * @returns {string} the digits contained in value, in order\n */\nexport const onlyDigits = (value) => (value || '').replaceAll(/\\D/g, '');\n\n/**\n * @param {string[]} values - current box values\n * @returns {boolean} true when every box holds exactly one character\n */\nexport const allFilled = (values) => values.every((value) => value.length === 1);\n\n/**\n * Index of the box to focus after digits were entered into the box at index:\n * the box after the last one filled, clamped to the last box; the same box\n * when nothing was entered.\n *\n * @param {number} index - box the input event fired on\n * @param {number} digitCount - digits distributed from that event\n * @param {number} boxCount - total number of boxes\n * @returns {number} index of the box to focus\n */\nexport const nextFocusIndex = (index, digitCount, boxCount) => (\n Math.min(index + digitCount, boxCount - 1)\n);\n\n/**\n * @param {object} options\n * @param {string} options.boxSelector - CSS selector matching the digit boxes\n * @param {string} options.continueButtonId - id of the Continue button to gate\n * @param {string} options.codeFieldId - id of the hidden field to mirror the code into\n */\nexport const initSegmentedCodeInput = ({ boxSelector, continueButtonId, codeFieldId }) => {\n const boxes = Array.from(document.querySelectorAll(boxSelector));\n if (!boxes.length) return;\n\n const continueButton = document.getElementById(continueButtonId);\n const codeField = document.getElementById(codeFieldId);\n\n const syncContinue = () => {\n const values = boxes.map((box) => box.value);\n if (codeField) codeField.value = values.join('');\n if (!continueButton) return;\n\n continueButton.disabled = !allFilled(values);\n };\n\n const handleInput = (index) => {\n const digits = onlyDigits(boxes[index].value).slice(0, boxes.length - index);\n boxes[index].value = '';\n for (const [offset, digit] of [...digits].entries()) {\n boxes[index + offset].value = digit;\n }\n boxes[nextFocusIndex(index, digits.length, boxes.length)].focus();\n syncContinue();\n };\n\n for (const [index, box] of boxes.entries()) {\n box.addEventListener('input', () => handleInput(index));\n box.addEventListener('keydown', (event) => {\n if (event.key !== 'Backspace' || box.value !== '' || index === 0) return;\n\n boxes[index - 1].focus();\n });\n }\n};\n","import { amplitudeTrack } from '@cw-elements/analytics-provider';\nimport { getProviderType, persistedParam } from './track_auth';\n\n/**\n * Amplitude events for the OTP code-verification screen: \"verify user\" (form\n * submit), \"resend verification code\" (Resend click), and \"change\n * verification method\" (switch-channel click, e.g. \"use email instead\").\n * \"verify user\" hooks the form's submit rather than the Continue button's\n * click: pressing Enter inside a filled code box submits the form directly,\n * bypassing the button (see the \"pressing Enter\" system spec), so a\n * click-only listener would miss that path.\n * Fires before the resulting navigation — mirrors track_auth.js's\n * fire-before-navigate pattern so the beacon transport can flush the queued\n * event on the resulting pagehide.\n */\n\n// screen_state.channel is lowercase \"sms\"/\"email\"; screen_state.switch_target\n// is uppercase \"SMS\"/\"EMAIL\" — both map to the same Amplitude enum values.\nconst CHANNEL_TO_METHOD = { sms: 'phone', email: 'email' };\n\nexport const verificationMethod = (channel) => CHANNEL_TO_METHOD[channel?.toLowerCase()];\n\nconst commonProperties = () => ({\n appId: persistedParam('client_id'),\n type: getProviderType(),\n});\n\nexport const initTwoFactorEvents = () => {\n const form = document.getElementById('two-factor-verify-form');\n if (!form) return;\n\n const currentMethod = verificationMethod(form.dataset.channel);\n\n form.addEventListener('submit', () => {\n amplitudeTrack('verify user', { ...commonProperties(), 'verification method': currentMethod });\n });\n\n document.getElementById('two-factor-resend')?.addEventListener('click', () => {\n amplitudeTrack('resend verification code', {\n ...commonProperties(),\n 'verification method': currentMethod,\n });\n });\n\n const switchButton = document.getElementById('two-factor-switch-channel');\n switchButton?.addEventListener('click', () => {\n amplitudeTrack('change verification method', {\n ...commonProperties(),\n 'verification method': verificationMethod(switchButton.dataset.channel),\n });\n });\n};\n","import { amplitudeTrack } from '@cw-elements/analytics-provider';\nimport { analyticsReady } from 'cropwise_accounts/analytics';\nimport { getProviderType, persistedParam } from './track_auth';\n\n/**\n * Amplitude events for the MFA notice screen: \"verification banner viewed\" on\n * load, and \"dismiss verification banner\" for its two dismiss paths (the\n * later/verify-email form and the phone-confirmation form). dismiss_action is\n * read off each form's data-dismiss-action key (set server-side from\n * MfaNoticePagesController#dismiss_action_key / the hardcoded \"phone\" key),\n * not the localized button label — the label text varies by locale and copy\n * changes, which would otherwise fragment the same action across Amplitude\n * groupings.\n */\n\nconst DISMISS_ACTION_LABELS = new Map([\n ['later', \"I'll do it later\"],\n ['verify_email', \"I'll verify my email instead\"],\n ['phone', 'Confirm my number'],\n]);\n\nconst commonProperties = () => ({\n appId: persistedParam('client_id'),\n type: getProviderType(),\n});\n\n// Amplitude's contracted property name for this event is snake_case, unlike\n// the space-separated keys ('client ID') the rest of this domain uses.\nconst trackDismiss = (dismissActionKey) => amplitudeTrack('dismiss verification banner', {\n ...commonProperties(),\n // eslint-disable-next-line camelcase -- dismiss_action is the contracted Amplitude property name\n dismiss_action: DISMISS_ACTION_LABELS.get(dismissActionKey),\n});\n\n// Fired on page load, so it waits for the provider: anything sent before the\n// orchestrator finishes starting is dropped without buffering.\nconst trackBannerViewed = async () => {\n await analyticsReady;\n\n amplitudeTrack('verification banner viewed', commonProperties());\n};\n\nexport const initMfaNoticeEvents = () => {\n const card = document.querySelector('.mfa-notice-card');\n if (!card) return;\n\n void (async () => {\n try {\n await trackBannerViewed();\n } catch (error) {\n // Background failures must be reported without rejecting the event handler.\n console.error(error); // eslint-disable-line no-console\n }\n })();\n\n for (const formSelector of ['.mfa-notice-card__later-form', '.mfa-notice-card__phone-form']) {\n const form = document.querySelector(formSelector);\n form?.addEventListener('submit', () => {\n trackDismiss(form.dataset.dismissAction);\n });\n }\n};\n","import { datadogAddAction, getDatadogInstance } from '@cw-elements/analytics-provider';\nimport { analyticsReady } from 'cropwise_accounts/analytics';\n\n/**\n * Reports the user-facing alerts the Cropwise Accounts pages render server-side.\n *\n * Since these pages became server-rendered there is no XHR for RUM to flag: a\n * rejected form is a 302 followed by a normal page view, so a grower blocked by\n * validation looks exactly like one who succeeded. The server names the cause in\n * `data-error-reason` (see WithSignupErrorRedirects) and this forwards it.\n *\n * Validation the visitor can correct is reported as an action — it is the product\n * working as designed, and belongs in funnels rather than in Error Tracking, where\n * it would open issues, inflate session error counts, and eventually be muted.\n * Only an unexpected failure is reported as an error.\n */\n\nexport const USER_ERROR_ACTION = 'accounts user error';\n// Any element may declare the reason: signup hangs it on the shared alert banner,\n// while sign-in renders its failures in the form and uses a carrier of its own.\nconst REASON_SELECTOR = '[data-error-reason]';\nconst DEFECT_REASONS = new Set(['generic_error']);\n\n// Alerts outside the signup flow carry no reason yet, and reporting them as\n// \"undefined\" would be a facet nobody can act on.\nexport const reportTargetFor = (reason) => {\n if (!reason) return null;\n\n return DEFECT_REASONS.has(reason) ? 'error' : 'action';\n};\n\n// Error Tracking fingerprints on the message, so it carries the reason rather\n// than the alert text, which differs across 35 locales.\nexport const defectMessage = (reason) => `${USER_ERROR_ACTION}: ${reason}`;\n\nconst report = async (target, reason) => {\n await analyticsReady;\n\n if (target === 'error') {\n getDatadogInstance()?.addError(new Error(defectMessage(reason)), { reason });\n return;\n }\n\n datadogAddAction(USER_ERROR_ACTION, { reason });\n};\n\n// The DOM is read now — the alert is on the page this instant — while the report\n// waits for the provider, which drops anything sent before it finishes starting.\nexport const initUserErrorEvents = () => {\n const reason = document.querySelector(REASON_SELECTOR)?.dataset.errorReason;\n const target = reportTargetFor(reason);\n if (!target) return;\n\n void (async () => {\n try {\n await report(target, reason);\n } catch (error) {\n // Background failures must be reported without rejecting the event handler.\n console.error(error); // eslint-disable-line no-console\n }\n })();\n};\n","/**\n * Standalone full-page legal-documents viewer (CWS-21215): one URL per\n * document type, opening straight to that document for the :country in the\n * path (see config/routes.rb / legal_documents_viewer_controller.rb). Every\n * document button already carries its own data (file path, name, location,\n * effective date, and its own URL) rendered server-side, so switching\n * between the two document types is a pure client-side pane swap — no fetch\n * involved. PDFs render via the browser's native / viewer\n * (the same pattern as service_legal_agreements/_pdf_view.slim).\n *\n * Changing location is a full navigation (there is no single \"browse this\n * location\" URL to update in place); switching document type instead swaps\n * the pane and updates the URL via history.pushState so the address bar\n * always matches what's on screen without a page reload.\n *\n * Also reused, injected as a fragment, inside the in-app drawer\n * (legal_documents_drawer.js) — enableHistory: false there, since the\n * drawer's host page owns the address bar and must not have it rewritten\n * out from under an unrelated in-progress page (e.g. the signup form).\n * Changing location is a real navigation only on the standalone page;\n * the drawer instead passes onLocationChange to re-fetch and re-render\n * the new country's document inside itself, since a real navigation\n * there would defeat the whole point of not leaving the host page.\n */\nconst PDF_VIEW_HASH = 'toolbar=0&navpanes=0&view=FitH';\n\nexport const initLegalDocumentsViewer = (\n root = document.getElementById('legal-documents-viewer'),\n { enableHistory = true, onLocationChange, initialUrl } = {},\n) => {\n if (!root) return;\n\n const menu = root.querySelector('#legal-documents-viewer-menu');\n const content = root.querySelector('#legal-documents-viewer-content');\n const backButton = root.querySelector('#legal-documents-viewer-back');\n const contentTitleEl = root.querySelector('#legal-documents-viewer-content-title');\n const contentSubtitleEl = root.querySelector('#legal-documents-viewer-content-subtitle');\n const downloadLink = root.querySelector('#legal-documents-viewer-download');\n const locationSelect = root.querySelector('#legal-documents-viewer-location');\n const cookiePolicyButton = root.querySelector('#legal-documents-viewer-cookie-policy');\n const pdfContainer = root.querySelector('#legal-documents-viewer-pdf-container');\n const declarationContainer = root.querySelector('#legal-documents-viewer-cookie-declaration');\n const loading = root.querySelector('#legal-documents-viewer-loading');\n const errorMessage = root.querySelector('#legal-documents-viewer-error');\n const docButtons = [...root.querySelectorAll('[data-legal-doc-type]')];\n const rowButtons = [...docButtons, cookiePolicyButton];\n const cookiePolicyName = cookiePolicyButton\n .querySelector('.cropwise-accounts-legal-viewer__item-label-name').textContent;\n\n let activeRowButton = null;\n // Tracks the active document's own viewer URL so the location select can\n // rebuild a destination from it. location.href works for that only when\n // enableHistory keeps it in sync with the viewer's own route; injected\n // into the drawer, location.href is still the host page's own URL, so\n // rewriting its last segment would build a broken, non-viewer route.\n // initialUrl (the drawer's own fetched href) seeds this for a country\n // with no current document of the requested type — no button opens to\n // overwrite it there, so without this seed, changing location away\n // from and back to that empty country would have no URL to rebuild\n // from and silently do nothing on the way back.\n let currentDocumentUrl = initialUrl\n ?? (enableHistory ? `${location.pathname}${location.search}` : null);\n // Bumped on every pane switch so an async Cookie Policy load/error handler\n // that resolves after the user has since opened a different pane can tell\n // it is stale and skip touching the shared loading/error elements.\n let paneGeneration = 0;\n\n // The back button and its own header live inside content-header, which is\n // only visible at all when .content itself is — no separate hidden\n // toggling needed here, just the menu/content pane swap.\n const showMenu = () => {\n paneGeneration += 1;\n menu.hidden = false;\n content.hidden = true;\n activeRowButton?.focus();\n };\n\n const enterContentView = (title, subtitle, { focus = true } = {}) => {\n menu.hidden = true;\n content.hidden = false;\n contentTitleEl.textContent = title;\n contentSubtitleEl.textContent = subtitle;\n if (focus) backButton.focus();\n };\n\n const markActiveRow = (activeButton) => {\n activeRowButton = activeButton;\n for (const button of rowButtons) {\n const isActive = button === activeButton;\n button.classList.toggle('is-active', isActive);\n button.setAttribute('aria-pressed', isActive);\n }\n };\n\n // Mirrors service_legal_agreements/_pdf_view.slim's / pair\n // (the browser's native PDF viewer renders the , falling back to\n // the nested when it can't) instead of a JS PDF-rendering library.\n const buildPdfObject = (filePath) => {\n const url = new URL(filePath);\n url.hash = PDF_VIEW_HASH;\n const src = url.href;\n\n const embed = document.createElement('embed');\n embed.className = 'cropwise-accounts-legal-viewer__pdf-object';\n embed.type = 'application/pdf';\n embed.src = src;\n\n const object = document.createElement('object');\n object.className = 'cropwise-accounts-legal-viewer__pdf-object';\n object.type = 'application/pdf';\n object.data = src;\n object.append(embed);\n return object;\n };\n\n const docFromButton = (button) => ({\n filePath: button.dataset.filePath,\n name: button.dataset.name,\n locationName: button.dataset.locationName,\n effectiveDate: button.dataset.effectiveDate,\n });\n\n const showContent = (doc, { focus = true } = {}) => {\n const subtitle = [doc.locationName, doc.effectiveDate].filter(Boolean).join(' • ');\n enterContentView(doc.name, subtitle, { focus });\n downloadLink.hidden = false;\n downloadLink.href = doc.filePath;\n declarationContainer.hidden = true;\n declarationContainer.replaceChildren();\n loading.hidden = true;\n errorMessage.hidden = true;\n pdfContainer.hidden = false;\n pdfContainer.replaceChildren(buildPdfObject(doc.filePath));\n };\n\n const showCookieDeclaration = () => {\n enterContentView(cookiePolicyName, '');\n downloadLink.hidden = true;\n pdfContainer.hidden = true;\n pdfContainer.replaceChildren();\n declarationContainer.hidden = false;\n declarationContainer.replaceChildren();\n loading.hidden = false;\n errorMessage.hidden = true;\n };\n\n // The desktop two-pane layout keeps .content visually shown regardless of\n // its hidden attribute, so a stale document/title/PDF would otherwise\n // linger there once no row is active — e.g. navigating back past the\n // initial history entry for a country missing the requested type.\n const clearActiveDocument = () => {\n markActiveRow(null);\n contentTitleEl.textContent = '';\n contentSubtitleEl.textContent = '';\n downloadLink.hidden = true;\n pdfContainer.hidden = true;\n pdfContainer.replaceChildren();\n declarationContainer.hidden = true;\n declarationContainer.replaceChildren();\n loading.hidden = true;\n errorMessage.hidden = true;\n };\n\n // pushUrl is skipped for the initial render (the address bar already\n // matches), for popstate restores (the browser already changed it), and\n // whenever enableHistory is off (the drawer must not touch the host\n // page's address bar at all).\n // focus is skipped only for the initial render, so page load doesn't\n // steal focus from wherever the browser would otherwise put it.\n // The pushed/replaced URL keeps location.search (e.g. ?locale=de-AT) so a\n // refresh or share doesn't drop it, but the stored state.url stays\n // path-only, matching dataset.url, for the popstate lookup below.\n const openDocumentButton = (button, { pushUrl = true, focus = true } = {}) => {\n paneGeneration += 1;\n markActiveRow(button);\n showContent(docFromButton(button), { focus });\n currentDocumentUrl = enableHistory\n ? `${button.dataset.url}${location.search}`\n : button.dataset.url;\n if (pushUrl && enableHistory) {\n history.pushState({ url: button.dataset.url }, '', currentDocumentUrl);\n }\n };\n\n const openCookieDeclaration = async () => {\n paneGeneration += 1;\n const generation = paneGeneration;\n const isActive = () => generation === paneGeneration;\n markActiveRow(cookiePolicyButton);\n showCookieDeclaration();\n try {\n const { renderCookieDeclaration } = await import('./cookie_declaration');\n if (isActive()) renderCookieDeclaration({ isActive });\n } catch {\n if (isActive()) {\n loading.hidden = true;\n errorMessage.hidden = false;\n }\n }\n };\n\n for (const button of docButtons) {\n button.addEventListener('click', () => openDocumentButton(button));\n }\n\n cookiePolicyButton.addEventListener('click', openCookieDeclaration);\n backButton.addEventListener('click', showMenu);\n\n locationSelect.addEventListener('change', () => {\n if (!currentDocumentUrl) return;\n const url = new URL(currentDocumentUrl, location.origin);\n url.pathname = url.pathname.replace(/(?[A-Z]{2})$/, locationSelect.value);\n if (onLocationChange) {\n onLocationChange(url.href);\n } else {\n location.href = url.href;\n }\n });\n\n if (enableHistory) {\n addEventListener('popstate', (event) => {\n const matchingButton = docButtons.find((button) => button.dataset.url === event.state?.url);\n if (matchingButton) {\n openDocumentButton(matchingButton, { pushUrl: false });\n } else {\n // showMenu runs first so it still has the previous activeRowButton\n // to focus — clearActiveDocument nulls that reference afterward.\n showMenu();\n clearActiveDocument();\n currentDocumentUrl = `${location.pathname}${location.search}`;\n }\n });\n }\n\n const initialButton = docButtons\n .find((button) => button.dataset.legalDocType === root.dataset.initialDocumentType);\n if (initialButton) {\n if (enableHistory) {\n const replacedUrl = `${initialButton.dataset.url}${location.search}`;\n history.replaceState({ url: initialButton.dataset.url }, '', replacedUrl);\n }\n openDocumentButton(initialButton, { pushUrl: false, focus: false });\n }\n};\n","/**\n * In-app drawer for the legal-documents viewer (CWS-21215), gated behind the\n * cup-legal-documents-in-app-drawer flag (WithLegalDocumentsDrawer). Every\n * [data-legal-documents-drawer-trigger] link already points at the standalone\n * viewer's own route (legal_documents_viewer_controller.rb) — clicking one\n * fetches that same route as a fragment (layout: false, triggered by the\n * X-Requested-With header below) and re-runs initLegalDocumentsViewer against\n * the injected subtree, with enableHistory off so the drawer never rewrites\n * this page's own address bar. With JS disabled, or if the fetch fails, the\n * link is a real href and still navigates to the full standalone page.\n */\nimport { initLegalDocumentsViewer } from './legal_documents_viewer';\nimport { initLanguageSelector } from './language_selector';\n\nconst FOCUSABLE_SELECTOR = 'button, [href], select, input, [tabindex]:not([tabindex=\"-1\"])';\n\nexport const initLegalDocumentsDrawer = () => {\n const drawer = document.getElementById('legal-documents-drawer');\n if (!drawer) return;\n\n const panel = drawer.querySelector('.cropwise-accounts-legal-drawer__panel');\n const closeButton = document.getElementById('legal-documents-drawer-close');\n const scrim = drawer.querySelector('[data-legal-documents-drawer-dismiss]');\n const slot = document.getElementById('legal-documents-drawer-slot');\n const loadingMarkup = slot.innerHTML;\n\n let lastTrigger = null;\n let requestToken = 0;\n\n // data-state tracks the slide animation itself (not just hidden, which\n // flips at the *start* of the transition) so callers — including system\n // specs — can wait for the drawer to have actually finished moving before\n // interacting with anything inside it.\n const openDrawer = () => {\n drawer.hidden = false;\n drawer.dataset.state = 'opening';\n requestAnimationFrame(() => requestAnimationFrame(() => {\n drawer.classList.add('is-open');\n panel.addEventListener('transitionend', () => {\n drawer.dataset.state = 'open';\n }, { once: true });\n }));\n };\n\n const closeDrawer = () => {\n // Invalidates any in-flight fragment request so it cannot finish after\n // the close, initializing the now-detached/hidden content and stealing\n // focus back from lastTrigger via load's own closeButton.focus() call.\n requestToken += 1;\n drawer.classList.remove('is-open');\n drawer.dataset.state = 'closing';\n panel.addEventListener('transitionend', () => {\n drawer.hidden = true;\n drawer.dataset.state = 'closed';\n slot.innerHTML = loadingMarkup;\n lastTrigger?.focus();\n }, { once: true });\n };\n\n const showError = () => {\n slot.replaceChildren();\n const message = document.createElement('p');\n message.className = 'cropwise-accounts-legal-drawer__error';\n message.setAttribute('role', 'alert');\n message.textContent = slot.dataset.errorText;\n slot.append(message);\n };\n\n const load = async (href) => {\n requestToken += 1;\n const token = requestToken;\n slot.innerHTML = loadingMarkup;\n try {\n const response = await fetch(href, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });\n if (token !== requestToken) return;\n if (!response.ok) throw new Error(`Failed to load legal documents: ${response.status}`);\n const html = await response.text();\n if (token !== requestToken) return;\n slot.innerHTML = html;\n const injectedRoot = slot.querySelector('#legal-documents-viewer');\n // Changing location inside the drawer re-fetches and re-renders the\n // new country's document through this same load, instead of load's\n // caller (open) real-navigating away like the standalone page does.\n // initialUrl seeds the viewer's own URL tracking with the href just\n // fetched, so a country with no current document (no button opens\n // to set this itself) can still be navigated away from and back to.\n initLegalDocumentsViewer(injectedRoot, {\n enableHistory: false,\n onLocationChange: load,\n initialUrl: href,\n });\n initLanguageSelector(injectedRoot, '#legal-documents-viewer-language-selector');\n closeButton.focus();\n } catch {\n if (token === requestToken) showError();\n }\n };\n\n const open = (trigger) => {\n lastTrigger = trigger;\n openDrawer();\n void load(trigger.href);\n };\n\n const trapFocus = (event) => {\n if (event.key !== 'Tab') return;\n const focusable = [...panel.querySelectorAll(FOCUSABLE_SELECTOR)];\n if (focusable.length === 0) return;\n\n const first = focusable[0];\n const last = focusable.at(-1);\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault();\n first.focus();\n }\n };\n\n document.addEventListener('click', (event) => {\n const trigger = event.target.closest('[data-legal-documents-drawer-trigger]');\n if (!trigger || trigger.getAttribute('aria-disabled') === 'true') return;\n\n event.preventDefault();\n open(trigger);\n });\n\n closeButton.addEventListener('click', closeDrawer);\n scrim.addEventListener('click', closeDrawer);\n panel.addEventListener('keydown', trapFocus);\n document.addEventListener('keydown', (event) => {\n if (event.key === 'Escape' && !drawer.hidden) closeDrawer();\n });\n addEventListener('popstate', () => {\n if (!drawer.hidden) closeDrawer();\n });\n};\n","/**\n * Renders a visible reCAPTCHA Enterprise checkbox challenge on any form that\n * carries a challenge container (sign-in once repeated failures have added it,\n * sign-up on every render), and blocks submission until the visitor completes\n * it.\n *\n * Google does not support self-hosting enterprise.js — it must be fetched\n * live from their CDN for the fraud-scoring model to stay current — so this\n * bundled module injects the