export interface Customer { id: number; email: string; // Nullable because customers who registered before these were required have // neither. Registration demands both from anyone new. first_name: string | null; last_name: string | null; email_verified: boolean; marketing_consent: boolean; favorite_alerts: boolean; created_at: string; } export interface OrderHistoryItem { id: number; processor: string; amount_cents: number; status: string; created_at: string; item_name: string; } async function handle(res: Response): Promise { if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'Request failed'); } return res.json(); } export function registerCustomer( email: string, password: string, firstName: string, lastName: string, marketingConsent: boolean ): Promise { return fetch('/api/customers/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password, firstName, lastName, marketingConsent }) }).then(res => handle(res)); } export function loginCustomer(email: string, password: string): Promise { return fetch('/api/customers/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }).then(res => handle(res)); } export function verifyEmail(token: string): Promise<{ status: string }> { return fetch('/api/customers/verify-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }) }).then(res => handle<{ status: string }>(res)); } export function logoutCustomer(): Promise { // Not routed through handle(): logout answers 204 with no body, so parsing // JSON would throw on success. The failure case still has to reject — the // session cookie survives a failed logout, so reporting success would leave // the customer logged in and silently signed back in on their next reload. return fetch('/api/customers/logout', { method: 'POST' }).then(async (res) => { if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'Request failed'); } }); } export function fetchMe(): Promise { return fetch('/api/customers/me').then(res => (res.ok ? res.json() : null)); } export function updateConsent(marketingConsent: boolean): Promise { return fetch('/api/customers/me/consent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ marketingConsent }) }).then(() => undefined); } export function fetchMyOrders(): Promise { return fetch('/api/customers/me/orders').then(res => handle(res)); } export function deleteMyAccount(): Promise { return fetch('/api/customers/me', { method: 'DELETE' }).then(() => undefined); } export function exportMyData(): void { window.location.href = '/api/customers/me/export'; } export function requestPasswordReset(email: string): Promise<{ status: string }> { return fetch('/api/customers/request-password-reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) }).then(res => handle<{ status: string }>(res)); } export function resetPassword(token: string, password: string): Promise { return fetch('/api/customers/reset-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, password }) }).then(res => handle(res)); } export function updateMyName(firstName: string, lastName: string): Promise { return fetch('/api/customers/me', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ firstName, lastName }) }).then(res => handle(res)); } export function changeMyPassword(currentPassword: string, newPassword: string): Promise { // Answers 204 with no body, so handle() would throw parsing JSON on success. // The failure case still has to reject: the server's message names which of // the two passwords was wrong, and that is the only useful thing to show. return fetch('/api/customers/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword, newPassword }) }).then(async (res) => { if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'Request failed'); } }); } export function changeMyEmail(currentPassword: string, email: string): Promise { return fetch('/api/customers/me/email', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword, email }) }).then(res => handle(res)); } export function resendVerificationEmail(): Promise { // 204 on success, so handle() would throw parsing an empty body. The failure // path must still reject: the server's message distinguishes "already // verified" from the rate limit's "check your spam folder", and both are // worth showing rather than replacing with something generic. return fetch('/api/customers/resend-verification', { method: 'POST' }).then(async (res) => { if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'Request failed'); } }); }