784 lines
26 KiB
TypeScript
784 lines
26 KiB
TypeScript
import { getApiJwt } from './appwrite'
|
||
|
||
/**
|
||
* Replace every `/api/api` in the path with `/api` until stable (avoids Express catch-all 404).
|
||
* Call on every outgoing API URL before `fetch`.
|
||
*/
|
||
export function collapseDoubleApi(url: string): string {
|
||
if (!url) return url
|
||
if (/^https?:\/\//i.test(url)) {
|
||
try {
|
||
const u = new URL(url)
|
||
let p = u.pathname
|
||
let prev = ''
|
||
while (p !== prev && p.includes('/api/api')) {
|
||
prev = p
|
||
p = p.replace(/\/api\/api/g, '/api')
|
||
}
|
||
// z. B. //api → /api (sonst 404 auf dem Server)
|
||
p = p.replace(/\/+/g, '/')
|
||
if (p !== '/' && !p.startsWith('/')) {
|
||
p = `/${p}`
|
||
}
|
||
u.pathname = p
|
||
return u.toString()
|
||
} catch {
|
||
return url
|
||
}
|
||
}
|
||
let s = url
|
||
let prev = ''
|
||
while (s !== prev && s.includes('/api/api')) {
|
||
prev = s
|
||
s = s.replace(/\/api\/api/g, '/api')
|
||
}
|
||
s = s.replace(/\/+/g, '/')
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* `import.meta.env.VITE_DEV_BACKEND_ORIGIN` (from Vite define) must be origin ONLY, e.g. `http://127.0.0.1:3030`.
|
||
* Do NOT set a trailing `/api` in .env / .env.local (`VITE_DEV_API_ORIGIN`) — the client appends `/api` once.
|
||
*/
|
||
function stripTrailingApiFromOrigin(origin: string): string {
|
||
let o = origin.replace(/\/+$/, '')
|
||
while (o.endsWith('/api')) {
|
||
o = o.slice(0, -4).replace(/\/+$/, '')
|
||
}
|
||
return o
|
||
}
|
||
|
||
/**
|
||
* Endpoints in this file are paths like `/subscription/status`.
|
||
* Express mounts the API under `/api`, so the base must end with `/api`.
|
||
*/
|
||
function resolveApiBase(): string {
|
||
const raw = (import.meta.env.VITE_API_URL as string | undefined)?.trim()
|
||
/** Empty / unset → use Vite define (VITE_DEV_BACKEND_ORIGIN) or relative /api */
|
||
const hasExplicitViteApiUrl = Boolean(raw)
|
||
|
||
if (import.meta.env.DEV && !hasExplicitViteApiUrl) {
|
||
const origin = (
|
||
import.meta.env as { VITE_DEV_BACKEND_ORIGIN?: string }
|
||
).VITE_DEV_BACKEND_ORIGIN?.trim()
|
||
if (origin) {
|
||
const o = stripTrailingApiFromOrigin(origin).replace(/\/+$/, '')
|
||
const withApi = `${o}/api`
|
||
return collapseDoubleApi(withApi)
|
||
}
|
||
}
|
||
|
||
if (!hasExplicitViteApiUrl) return collapseDoubleApi('/api')
|
||
|
||
if (!raw) return collapseDoubleApi('/api')
|
||
|
||
if (raw.startsWith('/')) {
|
||
const p = raw.replace(/\/+$/, '') || '/api'
|
||
return collapseDoubleApi(p)
|
||
}
|
||
|
||
if (!/^https?:\/\//i.test(raw)) {
|
||
const p = raw.replace(/^\/+/, '').replace(/\/+$/, '') || 'api'
|
||
const rel = p.startsWith('api') ? `/${p}` : `/api`
|
||
return collapseDoubleApi(rel)
|
||
}
|
||
|
||
const normalized = collapseDoubleApi(raw.replace(/\/+$/, '').trim())
|
||
try {
|
||
const u = new URL(normalized)
|
||
const path = u.pathname.replace(/\/+/g, '/').replace(/\/$/, '') || '/'
|
||
const localVite =
|
||
/^(localhost|127\.0\.0\.1)$/i.test(u.hostname) &&
|
||
(u.port === '5173' || (u.port === '' && u.hostname === 'localhost'))
|
||
if (path === '/v1' && localVite) {
|
||
return '/api'
|
||
}
|
||
if (path === '/' || path === '') {
|
||
const originOnly = normalized.replace(/\/+$/, '')
|
||
return collapseDoubleApi(`${originOnly}/api`)
|
||
}
|
||
if (path.endsWith('/api')) {
|
||
return collapseDoubleApi(normalized)
|
||
}
|
||
const originOnly = normalized.replace(/\/+$/, '')
|
||
return collapseDoubleApi(`${originOnly}/api`)
|
||
} catch {
|
||
return '/api'
|
||
}
|
||
}
|
||
|
||
export const API_BASE = collapseDoubleApi(resolveApiBase())
|
||
|
||
/** Join API base (ends with `/api`) and endpoint (`/email/...`). If endpoint starts with `/api`, strip it once so we never produce `/api/api`. */
|
||
function joinApiUrl(base: string, endpoint: string): string {
|
||
let ep = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
|
||
if (ep === '/api' || ep.startsWith('/api/')) {
|
||
ep = ep === '/api' ? '/' : ep.slice(4)
|
||
if (ep !== '' && !ep.startsWith('/')) {
|
||
ep = `/${ep}`
|
||
}
|
||
}
|
||
if (/^https?:\/\//i.test(base)) {
|
||
const joined = `${base.replace(/\/+$/, '')}${ep}`
|
||
return collapseDoubleApi(joined)
|
||
}
|
||
let b = base.trim()
|
||
if (!b.startsWith('/')) {
|
||
b = `/${b.replace(/^\/+/, '')}`
|
||
}
|
||
b = b.replace(/\/+$/, '') || '/api'
|
||
return collapseDoubleApi(`${b}${ep}`)
|
||
}
|
||
|
||
interface ApiResponse<T> {
|
||
success?: boolean
|
||
data?: T
|
||
error?: {
|
||
code: string
|
||
message: string
|
||
fields?: Record<string, string[]>
|
||
limit?: number
|
||
used?: number
|
||
}
|
||
}
|
||
|
||
async function fetchApi<T>(
|
||
endpoint: string,
|
||
options?: RequestInit
|
||
): Promise<ApiResponse<T>> {
|
||
try {
|
||
const headers: Record<string, string> = {
|
||
...(options?.headers as Record<string, string>),
|
||
}
|
||
if (!headers['Content-Type']) {
|
||
headers['Content-Type'] = 'application/json'
|
||
}
|
||
const jwt = await getApiJwt()
|
||
if (jwt) {
|
||
headers['Authorization'] = `Bearer ${jwt}`
|
||
}
|
||
|
||
const urlJoined = collapseDoubleApi(joinApiUrl(API_BASE, endpoint))
|
||
|
||
const response = await fetch(urlJoined, {
|
||
...options,
|
||
headers,
|
||
})
|
||
|
||
const ct = response.headers.get('content-type') || ''
|
||
const isJson = ct.includes('application/json')
|
||
const data = isJson
|
||
? await response.json()
|
||
: { success: false as const, error: undefined }
|
||
|
||
if (!isJson) {
|
||
const devHint =
|
||
import.meta.env.DEV && response.status === 404
|
||
? ` API_BASE=${API_BASE}`
|
||
: ''
|
||
return {
|
||
error: {
|
||
code: response.status === 404 ? 'NOT_FOUND' : 'INVALID_RESPONSE',
|
||
message:
|
||
response.status === 404
|
||
? `API 404: Backend antwortet nicht (falscher Port oder alter Prozess). Server starten: cd server && npm run start. PORT wie in server/.env.${devHint}`
|
||
: `Expected JSON, got ${ct || 'unknown'} (HTTP ${response.status})`,
|
||
},
|
||
}
|
||
}
|
||
|
||
if (!response.ok || data.success === false) {
|
||
return {
|
||
error: data.error || {
|
||
code: 'UNKNOWN',
|
||
message: `HTTP ${response.status}`,
|
||
},
|
||
}
|
||
}
|
||
|
||
return { success: true, data: data.data ?? data }
|
||
} catch (error) {
|
||
const base = error instanceof Error ? error.message : 'Network error'
|
||
const devHint =
|
||
import.meta.env.DEV
|
||
? ` API_BASE=${API_BASE} — Ist der Server an diesem Port gestartet? (cd server && npm run start)`
|
||
: ''
|
||
return {
|
||
error: {
|
||
code: 'NETWORK_ERROR',
|
||
message: `${base}${devHint}`,
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
export const api = {
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// EMAIL ACCOUNTS
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getEmailAccounts() {
|
||
return fetchApi<Array<{
|
||
id: string
|
||
email: string
|
||
provider: 'gmail' | 'outlook' | 'imap'
|
||
connected: boolean
|
||
lastSync?: string
|
||
}>>('/email/accounts')
|
||
},
|
||
|
||
async connectEmailAccount(provider: 'gmail' | 'outlook', email: string, accessToken: string, refreshToken?: string) {
|
||
return fetchApi<{ accountId: string }>('/email/connect', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ provider, email, accessToken, refreshToken }),
|
||
})
|
||
},
|
||
|
||
async connectImapAccount(params: {
|
||
email: string
|
||
password: string
|
||
imapHost?: string
|
||
imapPort?: number
|
||
imapSecure?: boolean
|
||
}) {
|
||
return fetchApi<{ accountId: string }>('/email/connect', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
provider: 'imap',
|
||
email: params.email,
|
||
accessToken: params.password,
|
||
imapHost: params.imapHost,
|
||
imapPort: params.imapPort,
|
||
imapSecure: params.imapSecure,
|
||
}),
|
||
})
|
||
},
|
||
|
||
async disconnectEmailAccount(accountId: string) {
|
||
return fetchApi<{ success: boolean }>(`/email/accounts/${accountId}`, {
|
||
method: 'DELETE',
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// EMAIL STATS & SORTING
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getEmailStats() {
|
||
return fetchApi<{
|
||
totalSorted: number
|
||
todaySorted: number
|
||
weekSorted: number
|
||
categories: Record<string, number>
|
||
timeSaved: number
|
||
}>('/email/stats')
|
||
},
|
||
|
||
async sortEmails(accountId: string, maxEmails?: number, processAll?: boolean) {
|
||
return fetchApi<{
|
||
sorted: number
|
||
inboxCleared: number
|
||
categories: Record<string, number>
|
||
timeSaved: { minutes: number; formatted: string }
|
||
highlights: Array<{ type: string; count: number; message: string }>
|
||
suggestions: Array<{ type: string; message: string }>
|
||
provider?: string
|
||
isDemo?: boolean
|
||
isFirstRun?: boolean
|
||
suggestedRules?: Array<{
|
||
type: string
|
||
name: string
|
||
description: string
|
||
confidence: number
|
||
action?: { name?: string }
|
||
}>
|
||
}>('/email/sort', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ accountId, maxEmails, processAll }),
|
||
})
|
||
},
|
||
|
||
async recoverEmails(accountId: string) {
|
||
return fetchApi<{
|
||
recovered: number
|
||
folders: Array<{ folder: string; count: number }>
|
||
message: string
|
||
}>(`/email/recover/${accountId}`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
/** Move mail from sort-related folders (Junk, Archive, MailFlow/…) back to INBOX and strip $MailFlow-* keywords (IMAP only). */
|
||
async reSortEmails(accountId: string) {
|
||
return fetchApi<{
|
||
recovered: number
|
||
folders: Array<{ folder: string; count: number }>
|
||
mailFlowKeywordsStripped: number
|
||
message: string
|
||
}>(`/email/re-sort/${accountId}`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
async sortDemo(count: number = 10) {
|
||
return fetchApi<{
|
||
sorted: number
|
||
emails: Array<{
|
||
from: string
|
||
subject: string
|
||
snippet: string
|
||
category: string
|
||
categoryName: string
|
||
confidence?: number
|
||
reason?: string
|
||
}>
|
||
categories: Record<string, number>
|
||
aiEnabled: boolean
|
||
}>('/email/sort-demo', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ count }),
|
||
})
|
||
},
|
||
|
||
async connectDemoAccount() {
|
||
return fetchApi<{
|
||
accountId: string
|
||
email: string
|
||
provider: string
|
||
message?: string
|
||
}>('/email/connect-demo', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
async getCategories() {
|
||
return fetchApi<Array<{
|
||
id: string
|
||
name: string
|
||
description: string
|
||
color: string
|
||
action: string
|
||
priority: number
|
||
}>>('/email/categories')
|
||
},
|
||
|
||
async getDigest() {
|
||
return fetchApi<{
|
||
date: string
|
||
totalSorted: number
|
||
inboxCleared: number
|
||
timeSavedMinutes: number
|
||
stats: Record<string, number>
|
||
highlights: Array<{ type: string; count: number; message: string }>
|
||
suggestions: Array<{ type: string; message: string }>
|
||
hasData: boolean
|
||
}>('/email/digest')
|
||
},
|
||
|
||
async getDigestHistory(days: number = 7) {
|
||
return fetchApi<{
|
||
days: number
|
||
digests: Array<{
|
||
date: string
|
||
totalSorted: number
|
||
inboxCleared: number
|
||
timeSavedMinutes: number
|
||
stats: Record<string, number>
|
||
}>
|
||
totals: {
|
||
totalSorted: number
|
||
inboxCleared: number
|
||
timeSavedMinutes: number
|
||
}
|
||
}>(`/email/digest/history?days=${days}`)
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// OAUTH
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getOAuthUrl(provider: 'gmail' | 'outlook') {
|
||
return fetchApi<{ url: string }>(`/oauth/${provider}/connect`)
|
||
},
|
||
|
||
async getOAuthStatus() {
|
||
return fetchApi<{
|
||
gmail: { enabled: boolean; scopes: string[] }
|
||
outlook: { enabled: boolean; scopes: string[] }
|
||
}>('/oauth/status')
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// SUBSCRIPTION
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getSubscriptionStatus() {
|
||
return fetchApi<{
|
||
status: string
|
||
plan: string
|
||
planDisplayName?: string
|
||
isFreeTier: boolean
|
||
emailsUsedThisMonth?: number
|
||
emailsLimit?: number
|
||
features: {
|
||
emailAccounts: number
|
||
emailsPerDay: number
|
||
historicalSync: boolean
|
||
customRules: boolean
|
||
prioritySupport: boolean
|
||
}
|
||
currentPeriodEnd?: string
|
||
cancelAtPeriodEnd?: boolean
|
||
}>('/subscription/status')
|
||
},
|
||
|
||
async createSubscriptionCheckout(plan: string, email?: string) {
|
||
return fetchApi<{ url: string; sessionId: string }>('/subscription/checkout', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ plan, email }),
|
||
})
|
||
},
|
||
|
||
async createPortalSession() {
|
||
return fetchApi<{ url: string }>('/subscription/portal', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
async cancelSubscription() {
|
||
return fetchApi<{ success: boolean }>('/subscription/cancel', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
async reactivateSubscription() {
|
||
return fetchApi<{ success: boolean }>('/subscription/reactivate', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// USER PREFERENCES
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getUserPreferences() {
|
||
return fetchApi<{
|
||
vipSenders: Array<{ email: string; name?: string }>
|
||
blockedSenders: string[]
|
||
customRules: Array<{ condition: string; category: string }>
|
||
priorityTopics: string[]
|
||
profile?: {
|
||
displayName?: string
|
||
timezone?: string
|
||
notificationPrefs?: Record<string, unknown>
|
||
}
|
||
}>('/preferences')
|
||
},
|
||
|
||
async saveUserPreferences(preferences: {
|
||
vipSenders?: Array<{ email: string; name?: string }>
|
||
blockedSenders?: string[]
|
||
customRules?: Array<{ condition: string; category: string }>
|
||
priorityTopics?: string[]
|
||
companyLabels?: Array<{ name: string; condition?: string; category: string; enabled: boolean }>
|
||
}) {
|
||
return fetchApi<{ success: boolean }>('/preferences', {
|
||
method: 'POST',
|
||
body: JSON.stringify(preferences),
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// AI CONTROL
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getAIControlSettings() {
|
||
return fetchApi<{
|
||
enabledCategories: string[]
|
||
categoryActions: Record<string, 'inbox' | 'archive_read' | 'star'>
|
||
autoDetectCompanies: boolean
|
||
cleanup?: unknown
|
||
categoryAdvanced?: Record<string, unknown>
|
||
version?: number
|
||
}>('/preferences/ai-control')
|
||
},
|
||
|
||
async saveAIControlSettings(settings: {
|
||
enabledCategories?: string[]
|
||
categoryActions?: Record<string, 'inbox' | 'archive_read' | 'star'>
|
||
autoDetectCompanies?: boolean
|
||
cleanup?: unknown
|
||
categoryAdvanced?: Record<string, unknown>
|
||
version?: number
|
||
}) {
|
||
return fetchApi<{ success: boolean }>('/preferences/ai-control', {
|
||
method: 'POST',
|
||
body: JSON.stringify(settings),
|
||
})
|
||
},
|
||
|
||
async getCleanupPreview(accountId: string) {
|
||
return fetchApi<{
|
||
messages: Array<{
|
||
id: string
|
||
subject: string
|
||
from: string
|
||
date: string
|
||
reason: 'read' | 'promotion'
|
||
}>
|
||
count: number
|
||
}>(`/email/${accountId}/cleanup/preview`)
|
||
},
|
||
|
||
async runCleanup() {
|
||
return fetchApi<{
|
||
usersProcessed: number
|
||
emailsProcessed: {
|
||
readItems: number
|
||
promotions: number
|
||
}
|
||
errors: Array<{ userId: string; error: string }>
|
||
}>('/email/cleanup', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
async getCleanupStatus(accountId: string) {
|
||
return fetchApi<{
|
||
lastRun?: string
|
||
lastRunCounts?: {
|
||
readItems: number
|
||
promotions: number
|
||
}
|
||
lastErrors?: string[]
|
||
}>(`/email/${accountId}/cleanup/status`)
|
||
},
|
||
|
||
async updateProfile(payload: {
|
||
displayName?: string
|
||
timezone?: string
|
||
notificationPrefs?: Record<string, unknown>
|
||
}) {
|
||
return fetchApi<{ success: boolean }>('/preferences/profile', {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(payload),
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// COMPANY LABELS
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getCompanyLabels() {
|
||
return fetchApi<Array<{
|
||
id?: string
|
||
name: string
|
||
condition: string
|
||
enabled: boolean
|
||
category?: string
|
||
}>>('/preferences/company-labels')
|
||
},
|
||
|
||
async saveCompanyLabel(companyLabel: {
|
||
id?: string
|
||
name: string
|
||
condition: string
|
||
enabled: boolean
|
||
category?: string
|
||
}) {
|
||
return fetchApi<{
|
||
id?: string
|
||
name: string
|
||
condition: string
|
||
enabled: boolean
|
||
category?: string
|
||
}>('/preferences/company-labels', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ companyLabel }),
|
||
})
|
||
},
|
||
|
||
async deleteCompanyLabel(labelId: string) {
|
||
return fetchApi<{ success: boolean }>(`/preferences/company-labels/${labelId}`, {
|
||
method: 'DELETE',
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// ME / ADMIN
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getMe() {
|
||
return fetchApi<{ isAdmin: boolean }>('/me')
|
||
},
|
||
|
||
async resetSortData(email: string) {
|
||
return fetchApi<{
|
||
reset: boolean
|
||
deleted?: { stats: number; digests: number; usage: number }
|
||
imapCleared?: number
|
||
}>('/admin/reset-user-sort-data', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ email }),
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// NAME LABELS (Workers – Admin only)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getNameLabels() {
|
||
return fetchApi<Array<{
|
||
id?: string
|
||
name: string
|
||
email?: string
|
||
keywords?: string[]
|
||
enabled: boolean
|
||
}>>('/preferences/name-labels')
|
||
},
|
||
|
||
async saveNameLabel(nameLabel: { id?: string; name: string; email?: string; keywords?: string[]; enabled: boolean }) {
|
||
return fetchApi<{ id?: string; name: string; email?: string; keywords?: string[]; enabled: boolean }>(
|
||
'/preferences/name-labels',
|
||
{
|
||
method: 'POST',
|
||
body: JSON.stringify({ nameLabel }),
|
||
}
|
||
)
|
||
},
|
||
|
||
async deleteNameLabel(labelId: string) {
|
||
return fetchApi<{ success: boolean }>(`/preferences/name-labels/${labelId}`, { method: 'DELETE' })
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// PRODUCTS & QUESTIONS (Legacy)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getProducts() {
|
||
return fetchApi<unknown[]>('/products')
|
||
},
|
||
|
||
async getQuestions(productSlug: string) {
|
||
return fetchApi<unknown[]>(`/questions?productSlug=${productSlug}`)
|
||
},
|
||
|
||
async createSubmission(productSlug: string, answers: Record<string, unknown>) {
|
||
return fetchApi<{ submissionId: string }>('/submissions', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ productSlug, answers }),
|
||
})
|
||
},
|
||
|
||
async createCheckout(submissionId: string) {
|
||
return fetchApi<{ url: string; sessionId: string }>('/checkout', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ submissionId }),
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// CONFIG
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getConfig() {
|
||
return fetchApi<{
|
||
features: {
|
||
gmail: boolean
|
||
outlook: boolean
|
||
ai: boolean
|
||
}
|
||
pricing: {
|
||
basic: { price: number; currency: string; accounts: number }
|
||
pro: { price: number; currency: string; accounts: number }
|
||
business: { price: number; currency: string; accounts: number }
|
||
}
|
||
}>('/config')
|
||
},
|
||
|
||
async healthCheck() {
|
||
return fetchApi<{
|
||
status: string
|
||
timestamp: string
|
||
version: string
|
||
environment: string
|
||
uptime: number
|
||
}>('/health')
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// ONBOARDING
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getOnboardingStatus() {
|
||
return fetchApi<{
|
||
onboarding_step: string
|
||
completedSteps: string[]
|
||
first_value_seen_at?: string
|
||
skipped_at?: string
|
||
}>('/onboarding/status')
|
||
},
|
||
|
||
async updateOnboardingStep(step: string, completedSteps: string[] = []) {
|
||
return fetchApi<{ step: string; completedSteps: string[] }>('/onboarding/step', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ step, completedSteps }),
|
||
})
|
||
},
|
||
|
||
async skipOnboarding() {
|
||
return fetchApi<{ skipped: boolean }>('/onboarding/skip', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
async resumeOnboarding() {
|
||
return fetchApi<{
|
||
onboarding_step: string
|
||
completedSteps: string[]
|
||
}>('/onboarding/resume', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// ACCOUNT MANAGEMENT
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async deleteAccount() {
|
||
return fetchApi<{ success: boolean }>('/account/delete', {
|
||
method: 'DELETE',
|
||
body: JSON.stringify({}),
|
||
})
|
||
},
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// REFERRALS
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async getReferralCode() {
|
||
return fetchApi<{
|
||
referralCode: string
|
||
referralCount: number
|
||
}>('/referrals/code')
|
||
},
|
||
|
||
async trackReferral(referralCode: string) {
|
||
return fetchApi<{ success: boolean }>('/referrals/track', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ referralCode }),
|
||
})
|
||
},
|
||
}
|
||
|
||
export default api
|