JezK
Edit File: sc-customer-email.cjs.entry.js.map
{"file":"sc-customer-email.entry.cjs.js","mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,MAAM,kBAAkB,GAAG,87CAA87C,CAAC;AAC19C,8BAAe,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BCmKnB,UAAE;;;8BAEF,UAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFAsEmB,UAAE;;;;;;;;;;;;;;;;;;;;;;;0BA4BzB,UAAE;;;;;;;;;;qtBAuBuC,UAAE,iLAKxC,UAAE;;;;;;;;;qBAsBJ,UAAE;;sGAQC,UAAE;;;;;;;;;;;;;;;;;;;;;;;;;","names":[],"sources":["src/components/controllers/checkout-form/customer-email/sc-customer-email.scss?tag=sc-customer-email&encapsulation=shadow","src/components/controllers/checkout-form/customer-email/sc-customer-email.tsx"],"sourcesContent":[":host {\n display: block;\n position: relative;\n}\n\na {\n color: var(--sc-color-primary-500);\n}\n\n.email-preview {\n display: flex;\n align-items: center;\n justify-content: space-between;\n position: relative;\n width: 100%;\n box-sizing: border-box;\n font-family: var(--sc-input-font-family);\n font-weight: var(--sc-input-font-weight);\n letter-spacing: var(--sc-input-letter-spacing);\n background-color: var(--sc-input-background-color);\n border: solid 1px var(--sc-input-border-color, var(--sc-input-border));\n vertical-align: middle;\n box-shadow: var(--sc-input-box-shadow);\n transition: var(--sc-input-transition, var(--sc-transition-medium)) color, var(--sc-input-transition, var(--sc-transition-medium)) border,\n var(--sc-input-transition, var(--sc-transition-medium)) box-shadow;\n border-radius: var(--sc-input-border-radius-medium);\n padding: var(--sc-input-spacing-small);\n font-size: var(--sc-font-size-small);\n\n sc-avatar {\n --sc-avatar-size: 38px;\n }\n\n &__info {\n display: flex;\n align-items: center;\n gap: 1em;\n }\n\n &__text {\n line-height: var(--sc-line-height-dense);\n\n :last-child:not(:first-child) {\n color: var(--sc-input-help-text-color);\n }\n }\n\n &__name {\n font-weight: var(--sc-font-weight-bold);\n }\n}\n\na.customer-email {\n &__login-link {\n color: var(--sc-customer-login-link-color, var(--sc-input-placeholder-color));\n text-decoration: none;\n font-size: var(--sc-font-size-small);\n }\n}\n\n.tracking-confirmation-message {\n font-size: var(--sc-font-size-xx-small);\n\n span {\n opacity: 0.75;\n }\n}\n\n.account-loader {\n line-height: 0;\n}\n","import { Component, Event, EventEmitter, Fragment, h, Method, Prop, State, Watch } from '@stencil/core';\nimport { __ } from '@wordpress/i18n';\nimport { speak } from '@wordpress/a11y';\nimport apiFetch from '@wordpress/api-fetch';\n\nimport { createOrUpdateCheckout } from '../../../../services/session';\nimport { Checkout, Customer } from '../../../../types';\nimport { getValueFromUrl, isRateLimited } from '../../../../functions/util';\nimport { getBlockedDuplicateSeconds, resendAnchorFrom, VerificationCodeResponse } from '../../../../functions/verification';\nimport { state as userState, onChange as onChangeUser, resetUser, CODE_SENT, UNVERIFIED, VERIFYING, CODE_EXPIRED } from '@store/user';\nimport { state as checkoutState, onChange } from '@store/checkout';\n\n@Component({\n tag: 'sc-customer-email',\n styleUrl: 'sc-customer-email.scss',\n shadow: true,\n})\nexport class ScCustomerEmail {\n private input: HTMLScInputElement;\n\n private removeCheckoutListener: () => void;\n private removeUserListener: () => void;\n\n /** A message for tracking confirmation. */\n @Prop() trackingConfirmationMessage: string;\n\n /** The input's size. */\n @Prop({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';\n\n /** The input's value attribute. */\n @Prop({ mutable: true }) value = getValueFromUrl('email');\n\n /** Draws a pill-style input with rounded edges. */\n @Prop({ reflect: true }) pill = false;\n\n /** The input's label. */\n @Prop() label: string;\n\n /** Should we show the label */\n @Prop() showLabel: boolean = true;\n\n /** The input's help text. */\n @Prop() help: string = '';\n\n /** The input's placeholder text. */\n @Prop() placeholder: string;\n\n /** Disables the input. */\n @Prop({ reflect: true }) disabled: boolean = false;\n\n /** Makes the input readonly. */\n @Prop({ reflect: true }) readonly: boolean = false;\n\n /** Makes the input a required field. */\n @Prop({ reflect: true }) required = false;\n\n /**\n * This will be true when the control is in an invalid state. Validity is determined by props such as `type`,\n * `required`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API.\n */\n @Prop({ mutable: true, reflect: true }) invalid = false;\n\n /** The input's autofocus attribute. */\n @Prop() autofocus: boolean;\n\n /** Inputs focus */\n @Prop({ mutable: true, reflect: true }) hasFocus: boolean;\n\n /** Is busy or not eg: email checking */\n @State() busy: boolean;\n\n /** Is logout in progress */\n @State() logoutBusy: boolean = false;\n\n /** Error */\n @State() error: string = '';\n\n /** Initial mode for sc-customer-login. Flips to 'password' on 429. */\n @State() loginMode: 'code' | 'password' = 'code';\n\n /** Emitted when the control's value changes. */\n @Event({ composed: true }) scChange: EventEmitter<void>;\n\n /** Emitted when the clear button is activated. */\n @Event() scClear: EventEmitter<void>;\n\n /** Emitted when the control receives input. */\n @Event() scInput: EventEmitter<void>;\n\n /** Emitted when the control gains focus. */\n @Event() scFocus: EventEmitter<void>;\n\n /** Emitted when the control loses focus. */\n @Event() scBlur: EventEmitter<void>;\n\n /** Update the order state. */\n @Event() scUpdateOrderState: EventEmitter<Checkout>;\n\n /** Update the abandoned cart. */\n @Event() scUpdateAbandonedCart: EventEmitter<boolean>;\n\n /** Prompt for login. */\n @Event() scLoginPrompt: EventEmitter<void>;\n\n async handleChange() {\n this.value = this.input.value;\n this.scChange.emit();\n\n try {\n checkoutState.checkout = (await createOrUpdateCheckout({ id: checkoutState.checkout.id, data: { email: this.input.value } })) as Checkout;\n } catch (error) {\n console.log(error);\n } finally {\n this.busy = false;\n }\n }\n\n private loginCodeDebounce: number;\n\n @Watch('value')\n handleValueChange() {\n if (this.loginCodeDebounce) {\n clearTimeout(this.loginCodeDebounce);\n }\n this.loginCodeDebounce = window.setTimeout(() => {\n this.createLoginCode();\n this.loginCodeDebounce = null;\n }, 800);\n }\n\n async createLoginCode() {\n if (!this.value) return;\n if (userState.loggedIn) return;\n\n // Opt-out merchant setting: skip proactive code-send when disabled.\n if (!checkoutState.showLoginPrompt) return;\n\n // Check if a valid email using regex, if not return.\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(this.value)) {\n return;\n }\n\n try {\n this.busy = true;\n this.error = '';\n this.loginMode = 'code';\n const response = await apiFetch<VerificationCodeResponse>({\n method: 'POST',\n path: 'surecart/v1/verification_codes',\n data: {\n login: this.value,\n checkout_mode: checkoutState.mode,\n },\n });\n userState.email = this.value;\n userState.verificationStatus = CODE_SENT;\n\n // Anchor the resend cooldown to the platform's window (may be a resumed\n // one when no fresh email was sent), so reload/tab switch stay accurate.\n // Falls back to the default window if the platform omits the value.\n userState.resendAvailableAt = resendAnchorFrom(response?.resend_available_in);\n\n // An in-window request resumes the existing code — don't announce a new email.\n if (response?.email_sent === false) {\n speak(__('A verification code was already sent to your email. Please check your email.', 'surecart'), 'assertive');\n } else {\n speak(__('Verification code is sent to your email. Please check your email.', 'surecart'), 'assertive');\n }\n } catch (e) {\n this.handleCodeSendError(e);\n } finally {\n this.busy = false;\n }\n }\n\n @Method()\n async reportValidity() {\n // if user is logged in, no need to validate.\n if (userState.loggedIn) return true;\n\n return this.input?.reportValidity?.();\n }\n\n /** Sync customer email with session if it's updated by other means */\n handleSessionChange() {\n // we already have a value and we are not yet logged in.\n if (this.value && !userState.loggedIn) return;\n\n // we are logged in already.\n if (userState.loggedIn) {\n // get email from user state fist.\n this.value = userState.email || (checkoutState?.checkout?.customer as Customer)?.email || checkoutState?.checkout?.email;\n return;\n }\n\n const fromUrl = getValueFromUrl('email');\n if (!userState.loggedIn && !!fromUrl) {\n this.value = fromUrl;\n return;\n }\n\n this.value = checkoutState?.checkout?.email || (checkoutState?.checkout?.customer as Customer)?.email;\n\n if (!!this.value && !userState.loggedIn) {\n userState.email = this.value;\n }\n }\n\n /** Listen to checkout. */\n componentWillLoad() {\n this.handleSessionChange();\n this.removeCheckoutListener = onChange('checkout', () => this.handleSessionChange());\n this.removeUserListener = onChangeUser('email', val => {\n this.value = val;\n });\n }\n\n handleCodeSendError(error: any) {\n // 429: silently switch to password mode. Header + input is enough context — no red error.\n if (isRateLimited(error)) {\n userState.email = this.input?.value || '';\n userState.verificationStatus = UNVERIFIED;\n this.loginMode = 'password';\n return;\n }\n\n const blockedSeconds = getBlockedDuplicateSeconds(error);\n\n (error?.additional_errors || []).forEach((e: any) => {\n if (e?.code === 'verification_code.email.blocked_duplicate') {\n userState.email = this.input?.value || '';\n userState.verificationStatus = CODE_SENT;\n // Resume the countdown from the platform's reported backoff window\n // (default window if the platform didn't include seconds).\n userState.resendAvailableAt = resendAnchorFrom(blockedSeconds);\n } else {\n this.error = e?.message || __('Verification code is not valid. Please try again.', 'surecart');\n }\n });\n }\n\n /** Remove listener. */\n disconnectedCallback() {\n this.removeCheckoutListener();\n this.removeUserListener();\n }\n\n async logout() {\n try {\n this.logoutBusy = true;\n checkoutState.checkout = (await createOrUpdateCheckout({ id: checkoutState.checkout.id, data: { email: '' } })) as Checkout;\n\n const response = (await apiFetch({\n method: 'POST',\n path: 'surecart/v1/logout',\n })) as any;\n\n // @ts-ignore - nonceMiddleware is set in fetch.ts but not in @wordpress/api-fetch types.\n if (response?.nonce && apiFetch.nonceMiddleware) {\n // @ts-ignore\n apiFetch.nonceMiddleware.nonce = response.nonce;\n }\n\n resetUser();\n speak(__('Logged out successfully.', 'surecart'), 'assertive');\n } catch (e) {\n console.error(e);\n } finally {\n this.logoutBusy = false;\n }\n }\n\n renderLoggedIn() {\n return (\n <div class=\"email-preview\">\n <div class=\"email-preview__info\">\n <sc-avatar\n image={userState.avatarUrl}\n initials={(userState?.name || userState?.email).charAt(0)}\n />\n <div class=\"email-preview__text\">\n <div class=\"email-preview__name\">{userState.name}</div>\n <div class=\"email-preview__email\">{userState.email}</div>\n </div>\n </div>\n <sc-dropdown placement=\"bottom-end\">\n <sc-button type=\"text\" slot=\"trigger\" loading={this.logoutBusy}>\n <sc-icon name=\"chevron-down\" aria-label={__('Account options', 'surecart')}></sc-icon>\n </sc-button>\n <sc-menu>\n <sc-menu-item onClick={() => this.logout()}>\n <sc-icon slot=\"prefix\" name=\"log-out\"></sc-icon>\n {__('Logout', 'surecart')}\n </sc-menu-item>\n </sc-menu>\n </sc-dropdown>\n </div>\n );\n }\n\n renderOptIn() {\n if (!this.trackingConfirmationMessage) return null;\n\n if (checkoutState.abandonedCheckoutEnabled !== false) {\n return (\n <div class=\"tracking-confirmation-message\">\n <span>{this.trackingConfirmationMessage}</span>{' '}\n <a\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n this.scUpdateAbandonedCart.emit(false);\n }}\n >\n {__('No Thanks', 'surecart')}\n </a>\n </div>\n );\n }\n\n return (\n <div class=\"tracking-confirmation-message\">\n <span> {__(\"You won't receive further emails from us.\", 'surecart')}</span>\n </div>\n );\n }\n\n render() {\n if (userState.loggedIn) {\n return this.renderLoggedIn();\n }\n\n if (\n userState.verificationStatus === CODE_SENT ||\n userState.verificationStatus === VERIFYING ||\n userState.verificationStatus === UNVERIFIED ||\n userState.verificationStatus === CODE_EXPIRED\n ) {\n return <sc-customer-login initialMode={this.loginMode} codeError={this.error}></sc-customer-login>;\n }\n\n return (\n <Fragment>\n <sc-input\n exportparts=\"base, input, form-control, label, help-text, prefix, suffix\"\n type=\"email\"\n name=\"email\"\n ref={el => (this.input = el as HTMLScInputElement)}\n value={this.value}\n help={this.help}\n label={this.label}\n autocomplete={'email'}\n placeholder={this.placeholder}\n disabled={this.disabled || (!!userState.loggedIn && !!this.value?.length && !this.invalid)}\n readonly={this.readonly}\n required={true}\n invalid={this.invalid}\n autofocus={this.autofocus}\n hasFocus={this.hasFocus}\n onScChange={() => this.handleChange()}\n onScInput={() => {\n this.scInput.emit();\n }}\n onScFocus={() => this.scFocus.emit()}\n onScBlur={() => this.scBlur.emit()}\n >\n {this.busy && <sc-spinner slot=\"suffix\" class=\"account-loader\" />}\n </sc-input>\n\n {this.renderOptIn()}\n </Fragment>\n );\n }\n}\n"],"version":3}