waitlist-invite.js 3.44 KB
import { EmailError } from '../map-error.js'

const COPY = Object.freeze({
  en: {
    subject: 'Your VOKO invite',
    heading: 'Your VOKO invite is ready',
    intro: 'Use the link below to finish registration.',
    action: 'Finish registration',
    warning: 'This invite link expires and should not be shared.',
  },
  zh: {
    subject: '您的 VOKO 邀请',
    heading: '您的 VOKO 邀请已准备就绪',
    intro: '请使用下方链接完成注册。',
    action: '完成注册',
    warning: '该邀请链接会过期,请勿分享给他人。',
  },
  ja: {
    subject: 'VOKOへの招待',
    heading: 'VOKOへの招待の準備ができました',
    intro: '以下のリンクから登録を完了してください。',
    action: '登録を完了',
    warning: 'この招待リンクには有効期限があります。他の人と共有しないでください。',
  },
  ko: {
    subject: 'VOKO 초대',
    heading: 'VOKO 초대가 준비되었습니다',
    intro: '아래 링크를 사용해 가입을 완료하세요.',
    action: '가입 완료',
    warning: '이 초대 링크는 만료되며 다른 사람과 공유하면 안 됩니다.',
  },
  es: {
    subject: 'Tu invitación a VOKO',
    heading: 'Tu invitación a VOKO está lista',
    intro: 'Usa el siguiente enlace para completar el registro.',
    action: 'Completar registro',
    warning: 'Este enlace de invitación caduca y no debe compartirse.',
  },
  pt: {
    subject: 'Seu convite para o VOKO',
    heading: 'Seu convite para o VOKO está pronto',
    intro: 'Use o link abaixo para concluir o cadastro.',
    action: 'Concluir cadastro',
    warning: 'Este link de convite expira e não deve ser compartilhado.',
  },
})

/**
 * @param {{ locale: string, inviteUrl: string }} data
 */
export function render(data) {
  if (!data || typeof data !== 'object') fail('data is required')
  const copy = COPY[data.locale]
  if (!copy) fail(`unsupported locale: ${String(data.locale)}`)
  const inviteUrl = requiredHttpsUrl(data.inviteUrl)
  const text = [copy.heading, '', copy.intro, inviteUrl, '', copy.warning].join('\n')
  const html = [
    '<!doctype html><html><body style="font-family:Arial,Helvetica,sans-serif;color:#1a1a1a;line-height:1.6;">',
    `<h2 style="margin:0 0 16px;">${escapeHtml(copy.heading)}</h2>`,
    `<p style="margin:0 0 16px;">${escapeHtml(copy.intro)}</p>`,
    `<p style="margin:0 0 16px;"><a href="${escapeHtml(inviteUrl)}">${escapeHtml(copy.action)}</a></p>`,
    `<p style="margin:0;color:#666;">${escapeHtml(copy.warning)}</p>`,
    '</body></html>',
  ].join('')
  return { subject: copy.subject, text, html }
}

function requiredHttpsUrl(value) {
  if (typeof value !== 'string' || value.trim() === '') fail('missing field "inviteUrl"')
  const url = value.trim()
  try {
    const parsed = new URL(url)
    // 本地开发的前端是 http://localhost,其余环境必须 https。
    if (parsed.protocol === 'https:' || parsed.hostname === 'localhost') return url
  } catch (error) {
    fail(`inviteUrl is invalid: ${error instanceof Error ? error.message : String(error)}`)
  }
  fail('inviteUrl must use https')
}

function fail(message) {
  throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: `waitlist-invite: ${message}` })
}

function escapeHtml(value) {
  return String(value)
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;')
}