waitlist.js 5.26 KB
import * as v from 'valibot'

export const WAITLIST_INVITE_CODE_LENGTH = 8
export const WAITLIST_INVITE_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
export const WAITLIST_INVITE_CODE_PATTERN = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$/

export const WaitlistInviteCode = v.pipe(
  v.string(),
  v.regex(WAITLIST_INVITE_CODE_PATTERN, 'invite_token must be an 8-character invite code'),
)

export const WaitlistStatus = v.picklist(['joined', 'invite_pending', 'invited', 'registered', 'rejected'])

export const WaitlistInviteDeliveryStatus = v.picklist(['none', 'pending', 'sent', 'failed', 'expired', 'claimed', 'closed'])

export const WaitlistDeliveryStatus = v.picklist(['pending', 'sent', 'failed'])

export const WaitlistSource = v.picklist(['landing_hero', 'landing_footer', 'pricing', 'manual_admin'])

export const WaitlistPublicSource = v.picklist(['landing_hero', 'landing_footer', 'pricing'])

export const WaitlistEventType = v.picklist([
  'joined',
  'duplicate_joined',
  'note_added',
  'invite_requested',
  'invite_email_sent',
  'invite_email_failed',
  'registered',
  'rejected',
  'exported',
])

export const WaitlistActorType = v.picklist(['public', 'operator', 'system'])

export const WaitlistJoinRequest = v.object({
  email: v.string(),
  display_name: v.optional(v.string()),
  locale: v.string(),
  source: WaitlistPublicSource,
  use_case: v.optional(v.string(), ''),
  note: v.optional(v.string()),
  consent_marketing: v.literal(true),
})

export const WaitlistJoinResponse = v.object({
  ok: v.literal(true),
  message: v.literal('joined'),
})

export const WaitlistInviteValidateResponse = v.object({
  ok: v.literal(true),
  email: v.nullable(v.string()),
  expires_at: v.string(),
})

export const WaitlistSendRegisterCodeResponse = v.object({
  ok: v.literal(true),
  email: v.string(),
})

export const WaitlistEntry = v.object({
  id: v.number(),
  email: v.nullable(v.string()),
  display_name: v.nullable(v.string()),
  locale: v.string(),
  source: WaitlistSource,
  use_case: v.string(),
  note: v.nullable(v.string()),
  marketing_consent_accepted_at: v.nullable(v.string()),
  marketing_consent_revoked_at: v.nullable(v.string()),
  status: WaitlistStatus,
  invite_state: WaitlistInviteDeliveryStatus,
  invite_token: v.nullable(WaitlistInviteCode),
  invite_url: v.nullable(v.string()),
  priority_score: v.number(),
  invite_expires_at: v.nullable(v.string()),
  invited_at: v.nullable(v.string()),
  invited_by: v.nullable(v.string()),
  last_invite_delivery_id: v.nullable(v.number()),
  last_invite_delivery_status: v.nullable(WaitlistDeliveryStatus),
  last_invite_email_log_id: v.nullable(v.string()),
  registered_user_id: v.nullable(v.string()),
  registered_at: v.nullable(v.string()),
  rejected_at: v.nullable(v.string()),
  rejected_by: v.nullable(v.string()),
  rejected_reason: v.nullable(v.string()),
  created_at: v.string(),
  updated_at: v.string(),
})

export const WaitlistEvent = v.object({
  id: v.number(),
  entry_id: v.nullable(v.number()),
  event_type: WaitlistEventType,
  actor_type: WaitlistActorType,
  actor_id: v.nullable(v.string()),
  metadata: v.nullable(v.unknown()),
  created_at: v.string(),
})

export const WaitlistInviteDelivery = v.object({
  id: v.number(),
  entry_id: v.number(),
  email_log_id: v.nullable(v.string()),
  status: WaitlistDeliveryStatus,
  invite_token_hash: v.string(),
  provider: v.nullable(v.string()),
  message_id: v.nullable(v.string()),
  error_message: v.nullable(v.string()),
  created_at: v.string(),
  sent_at: v.nullable(v.string()),
  failed_at: v.nullable(v.string()),
})

export const WaitlistEntryListResponse = v.object({
  items: v.array(WaitlistEntry),
  next_cursor: v.nullable(v.string()),
})

export const WaitlistEntryDetailResponse = v.object({
  entry: WaitlistEntry,
  events: v.array(WaitlistEvent),
  deliveries: v.array(WaitlistInviteDelivery),
})

export const WaitlistCreateManualEntryRequest = v.object({
  email: v.string(),
  display_name: v.optional(v.string()),
  locale: v.string(),
  use_case: v.string(),
  note: v.optional(v.string()),
  marketing_consent_accepted_at: v.string(),
})

export const WaitlistNoteRequest = v.object({
  note: v.string(),
})

export const WaitlistInviteRequest = v.object({
  note: v.optional(v.string()),
})

export const WaitlistRejectRequest = v.object({
  reason: v.string(),
})

export const WaitlistActionResponse = WaitlistEntry

export const WaitlistExportResponse = v.string()

/**
 * @typedef {import('valibot').InferOutput<typeof WaitlistStatus>} WaitlistStatusT
 * @typedef {import('valibot').InferOutput<typeof WaitlistInviteDeliveryStatus>} WaitlistInviteDeliveryStatusT
 * @typedef {import('valibot').InferOutput<typeof WaitlistJoinRequest>} WaitlistJoinRequestT
 * @typedef {import('valibot').InferOutput<typeof WaitlistJoinResponse>} WaitlistJoinResponseT
 * @typedef {import('valibot').InferOutput<typeof WaitlistInviteValidateResponse>} WaitlistInviteValidateResponseT
 * @typedef {import('valibot').InferOutput<typeof WaitlistSendRegisterCodeResponse>} WaitlistSendRegisterCodeResponseT
 * @typedef {import('valibot').InferOutput<typeof WaitlistEntry>} WaitlistEntryT
 * @typedef {import('valibot').InferOutput<typeof WaitlistEntryListResponse>} WaitlistEntryListResponseT
 * @typedef {import('valibot').InferOutput<typeof WaitlistEntryDetailResponse>} WaitlistEntryDetailResponseT
 */