{"version":3,"file":"sign-up-section.min.js","mappings":"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","sources":["webpack://lange/./lib/attentive-service.ts","webpack://lange/./sections/sign-up/sign-up-form/index.ts","webpack://lange/../node_modules/@integrabeauty/person/dist/src/email.js"],"sourcesContent":["export interface SubscribeInput {\n  email?: string;\n  first_name?: string;\n  phone?: string;\n  shopify_customer_id?: string;\n  sign_up_source_id: string;\n}\n\nexport interface RequestBody {\n  email?: string;\n  first_name?: string;\n  phone?: string;\n  shop_domain?: string;\n  shopify_customer_id?: string;\n  sign_up_source_id?: string;\n}\n\nexport type ResponseBody = Record<string, any>;\n\ninterface ErrorResponseBody {\n  error: string;\n}\n\n/**\n * This uses the no-store cache property because we never want this request to use the cache.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Request/cache\n */\nexport async function subscribe(input: SubscribeInput) {\n  const url = 'https://langehair.com/apps/attentive-relay/prod/v1/subscribe';\n\n  const requestBody: RequestBody = {\n    email: input.email,\n    first_name: input.first_name,\n    phone: input.phone,\n    shop_domain: 'lange-beauty.myshopify.com',\n    shopify_customer_id: input.shopify_customer_id,\n    sign_up_source_id: input.sign_up_source_id\n  };\n\n  let response;\n  try {\n    response = await fetch(url, {\n      body: JSON.stringify(requestBody),\n      cache: 'no-store',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      method: 'POST',\n      mode: 'cors'\n    });\n  } catch (error) {\n    const ae = new AttentiveError('There was a problem subscribing', { cause: error });\n    ae.request_body = requestBody;\n    ae.url = url;\n    throw ae;\n  }\n\n  // On success, we have a 204 No Content, so we do not attempt to fetch the response body. Calling\n  // await response.json() can throw for 204s. So we either have a non-204 indicating an error or we\n  // succeeded.\n\n  if (response.status !== 204) {\n    const error = new AttentiveError('There was a problem subscribing');\n    error.request_body = requestBody;\n\n    // If there is an issue in the backend subscribing, then we might have some information about\n    // the error in the response body, so we have to try to extract the message.\n\n    try {\n      const responseBody = await response.json();\n      error.response_body = responseBody;\n\n      if (isErrorResponseBody(responseBody) && typeof responseBody.error === 'string' &&\n        responseBody.error.length > 0) {\n        error.message = responseBody.error;\n      }\n    } catch (error) {\n      // ignore\n    }\n\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n}\n\nfunction isErrorResponseBody(value: any): value is ErrorResponseBody {\n  return typeof value?.error === 'string' && value.error.length > 0;\n}\n\nexport class AttentiveError extends Error {\n  public declare request_body: any;\n  public declare response_body: any;\n  public declare status: number;\n  public declare url: string;\n\n  constructor(message?: string, options?: ErrorOptions) {\n    super(message || 'Unknown', options);\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { isValidEmailAddress } from '@integrabeauty/person';\nimport { AttentiveError, subscribe } from '../../../lib/attentive-service.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * A sign up form asking for a name and an email. It signs people up the same way the footer form\n * does, through the same Attentive call, minus the phone number and its texting terms.\n *\n * On a successful sign up it dispatches the same identity-captured and form-submit events the\n * footer form does, both bubbling from this element.\n */\nclass SignUpForm extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * The ISO code of the visitor's currency. All uppercase.\n     *\n     * @example \"USD\"\n     */\n    currencyCode: string;\n\n    /**\n     * The id of the signed in Shopify customer. Empty for a guest.\n     *\n     * @example \"6283928207460\"\n     */\n    customerId: string;\n  };\n\n  private onSubmitBound = this.onSubmit.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const form = this.shadowRoot.querySelector('form');\n    form.addEventListener('submit', this.onSubmitBound);\n\n    const inputs = this.shadowRoot.querySelectorAll('input');\n    for (const input of inputs) {\n      input.addEventListener('focus', onFieldFocus);\n    }\n  }\n\n  public disconnectedCallback() {\n    const form = this.shadowRoot.querySelector('form');\n    form.removeEventListener('submit', this.onSubmitBound);\n\n    const inputs = this.shadowRoot.querySelectorAll('input');\n    for (const input of inputs) {\n      input.removeEventListener('focus', onFieldFocus);\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onSubmit(event: SubmitEvent) {\n    event.preventDefault();\n\n    // In the theme editor a merchant is not actually subscribing, so we do not submit the form.\n\n    if (Shopify.designMode) {\n      return;\n    }\n\n    const nameInput = this.shadowRoot.querySelector<HTMLInputElement>('#name');\n    const emailInput = this.shadowRoot.querySelector<HTMLInputElement>('#email');\n    const name = nameInput.value;\n    const email = emailInput.value;\n\n    const nameValid = isValidPersonName(name);\n    markField(nameInput, nameValid ? '' : 'Please enter a name');\n\n    const emailValid = isValidEmailAddress(email);\n    markField(emailInput, emailValid ? '' : 'Please enter a valid email address');\n\n    if (!nameValid || !emailValid) {\n      return;\n    }\n\n    const button = this.shadowRoot.querySelector('button');\n    button.disabled = true;\n    button.textContent = 'Submitting...';\n\n    // The Attentive sign up source id below is dedicated to the Panthers landing page. That is fine\n    // while the Panthers page is the only place using this section, but if we reuse the section on\n    // another page we should make the id configurable in the Shopify admin, so each page sends its\n    // own source.\n\n    subscribe({\n      email,\n      first_name: name,\n      shopify_customer_id: this.dataset.customerId,\n      sign_up_source_id: '1486156'\n    })\n      .then(() => this.onSubscribeSuccess(name, email))\n      .catch((error) => this.onSubscribeError(error));\n  }\n\n  private onSubscribeSuccess(name: string, email: string) {\n    // Once someone is signed up there is nothing left for them to do in the form, so we hide the\n    // fields and the button and leave only the thank you message.\n\n    const fields = this.shadowRoot.querySelectorAll<HTMLElement>('.field');\n    for (const field of fields) {\n      field.hidden = true;\n    }\n\n    const button = this.shadowRoot.querySelector('button');\n    button.hidden = true;\n\n    this.showResult('success', 'Thank you for registering!');\n\n    this.dispatchIdentityCaptured(name, email);\n\n    type FormSubmitDetail = WindowEventMap['form-submit']['detail'];\n    const submitEvent = new CustomEvent<FormSubmitDetail>('form-submit', {\n      bubbles: true,\n      detail: {\n        currency_code: this.dataset.currencyCode || 'USD',\n        email_provided: true,\n        form_name: 'sign-up',\n        form_location: location.href,\n        form_type: 'marketing-subscribe',\n        phone_provided: false\n      }\n    });\n    this.dispatchEvent(submitEvent);\n  }\n\n  private dispatchIdentityCaptured(name: string, email: string) {\n    // The name field takes a full name, so we split it the same way the footer form does. One part\n    // is the first name. Otherwise the last part is the last name and the rest is the first name.\n\n    type Detail = WindowEventMap['identity-captured']['detail'];\n    const detail: Detail = { email };\n\n    const parts = name.split(' ');\n    if (parts.length === 1) {\n      detail.first_name = parts[0];\n    } else {\n      detail.last_name = parts.pop();\n      detail.first_name = parts.join(' ');\n    }\n\n    const event = new CustomEvent<Detail>('identity-captured', {\n      bubbles: true,\n      detail\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onSubscribeError(error: unknown) {\n    const button = this.shadowRoot.querySelector('button');\n    button.disabled = false;\n    button.textContent = 'Join Now';\n\n    let message = 'There was a problem submitting your request, please try again later';\n    if (error instanceof AttentiveError) {\n      message = error.message || message;\n    } else {\n      console.warn(error, 'Failed to subscribe in Attentive');\n    }\n\n    this.showResult('error', message);\n  }\n\n  private showResult(result: 'error' | 'success', message: string) {\n    const response = this.shadowRoot.querySelector<HTMLElement>('#response');\n    response.dataset.result = result;\n    response.textContent = message;\n  }\n}\n\nfunction isValidPersonName(name: string) {\n  // Some browsers do not tolerate using an empty space in the character class so we use \\s\n  // explicitly, even though it matches multiple kinds of spaces.\n  return /^[a-z\\s]{2,30}$/i.test(name);\n}\n\nfunction markField(input: HTMLInputElement, message: string) {\n  const field = input.closest<HTMLElement>('.field');\n  field.dataset.validity = message ? 'invalid' : 'valid';\n  field.querySelector('.message').textContent = message;\n}\n\nfunction clearField(input: HTMLInputElement) {\n  const field = input.closest<HTMLElement>('.field');\n  delete field.dataset.validity;\n  field.querySelector('.message').textContent = '';\n}\n\nfunction onFieldFocus(event: FocusEvent) {\n  if (event.currentTarget instanceof HTMLInputElement) {\n    clearField(event.currentTarget);\n  }\n}\n\nif (!customElements.get('sign-up-form')) {\n  customElements.define('sign-up-form', SignUpForm);\n}\n","/**\n * Validates an email address.\n *\n * Based on the regex used by Meta's pixel.\n */\nexport function isValidEmailAddress(email) {\n    // eslint-disable-next-line @stylistic/max-len\n    return /^[\\w!#\\$%&\\'\\*\\+\\/\\=\\?\\^`\\{\\|\\}~\\-]+(:?\\.[\\w!#\\$%&\\'\\*\\+\\/\\=\\?\\^`\\{\\|\\}~\\-]+)*@(?:[a-z0-9](?:[a-z0-9\\-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9\\-]*[a-z0-9])?$/i.test(email);\n}\n"],"names":["AttentiveError","Error","constructor","message","options","super","SignUpForm","HTMLElement","onSubmitBound","this","onSubmit","bind","connectedCallback","initializeShadowRoot","shadowRoot","querySelector","addEventListener","inputs","querySelectorAll","input","onFieldFocus","disconnectedCallback","removeEventListener","attachShadow","mode","innerHTML","event","preventDefault","Shopify","designMode","nameInput","emailInput","name","value","email","nameValid","test","isValidPersonName","markField","emailValid","isValidEmailAddress","button","disabled","textContent","async","url","requestBody","first_name","phone","shop_domain","shopify_customer_id","sign_up_source_id","response","fetch","body","JSON","stringify","cache","headers","method","error","ae","cause","request_body","status","responseBody","json","response_body","length","subscribe","dataset","customerId","then","onSubscribeSuccess","catch","onSubscribeError","fields","field","hidden","showResult","dispatchIdentityCaptured","submitEvent","CustomEvent","bubbles","detail","currency_code","currencyCode","email_provided","form_name","form_location","location","href","form_type","phone_provided","dispatchEvent","parts","split","last_name","pop","join","console","warn","result","closest","validity","currentTarget","HTMLInputElement","clearField","customElements","get","define"],"sourceRoot":""}