fix(dev): Vite-API-Proxy, Auth, Stripe-Mails und Backend-Erweiterungen
- Client: API-Basis-URL (joinApiUrl, /v1-Falle), Vite strictPort + Proxy 127.0.0.1, Nicht-JSON-Fehler - Server: /api-404 ohne Wildcard-Bug, SPA-Fallback, Auth-Middleware, Cron, Mailer, Crypto - Routen: OAuth-State, Email/Stripe/Analytics; client/.env.example Made-with: Cursor
This commit is contained in:
@@ -1,4 +1,62 @@
|
||||
const API_BASE = import.meta.env.VITE_API_URL || '/api'
|
||||
import { getApiJwt } from './appwrite'
|
||||
|
||||
/**
|
||||
* Endpoints in this file are paths like `/subscription/status`.
|
||||
* Express mounts the API under `/api`, so the base must end with `/api`.
|
||||
* If VITE_API_URL is `http://localhost:3000` (missing /api), requests would 404.
|
||||
*/
|
||||
function resolveApiBase(): string {
|
||||
const raw = (import.meta.env.VITE_API_URL as string | undefined)?.trim()
|
||||
if (!raw) return '/api'
|
||||
|
||||
if (raw.startsWith('/')) {
|
||||
const p = raw.replace(/\/+$/, '') || '/api'
|
||||
return p
|
||||
}
|
||||
|
||||
if (!/^https?:\/\//i.test(raw)) {
|
||||
const p = raw.replace(/^\/+/, '').replace(/\/+$/, '') || 'api'
|
||||
return p.startsWith('api') ? `/${p}` : `/api`
|
||||
}
|
||||
|
||||
const normalized = raw.replace(/\/+$/, '')
|
||||
try {
|
||||
const u = new URL(normalized)
|
||||
const path = u.pathname.replace(/\/$/, '') || '/'
|
||||
// Same host as Vite + /v1 = Appwrite-Proxy; never append /api (would hit /v1/api/… → 404)
|
||||
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 === '') {
|
||||
return `${normalized}/api`
|
||||
}
|
||||
if (path.endsWith('/api')) {
|
||||
return normalized
|
||||
}
|
||||
return `${normalized}/api`
|
||||
} catch {
|
||||
return '/api'
|
||||
}
|
||||
}
|
||||
|
||||
const API_BASE = resolveApiBase()
|
||||
|
||||
/** Root-relative or absolute API URL; avoids `api/foo` (relative to current route). */
|
||||
function joinApiUrl(base: string, endpoint: string): string {
|
||||
const ep = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
|
||||
if (/^https?:\/\//i.test(base)) {
|
||||
return `${base.replace(/\/+$/, '')}${ep}`
|
||||
}
|
||||
let b = base.trim()
|
||||
if (!b.startsWith('/')) {
|
||||
b = `/${b.replace(/^\/+/, '')}`
|
||||
}
|
||||
b = b.replace(/\/+$/, '') || '/api'
|
||||
return `${b}${ep}`
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success?: boolean
|
||||
@@ -17,32 +75,58 @@ async function fetchApi<T>(
|
||||
options?: RequestInit
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
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 = joinApiUrl(API_BASE, endpoint)
|
||||
|
||||
const response = await fetch(urlJoined, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
headers,
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
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 (!response.ok || data.success === false) {
|
||||
return {
|
||||
error: data.error || {
|
||||
code: 'UNKNOWN',
|
||||
message: `HTTP ${response.status}`
|
||||
}
|
||||
if (!isJson) {
|
||||
return {
|
||||
error: {
|
||||
code: response.status === 404 ? 'NOT_FOUND' : 'INVALID_RESPONSE',
|
||||
message:
|
||||
response.status === 404
|
||||
? 'API 404: backend unreachable or wrong port — check server is running and VITE_DEV_API_ORIGIN matches PORT.'
|
||||
: `Expected JSON, got ${ct || 'unknown'} (HTTP ${response.status})`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: data.data || data }
|
||||
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) {
|
||||
return {
|
||||
error: {
|
||||
code: 'NETWORK_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Network error'
|
||||
}
|
||||
return {
|
||||
error: {
|
||||
code: 'NETWORK_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Network error',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,32 +135,34 @@ export const api = {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EMAIL ACCOUNTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getEmailAccounts(userId: string) {
|
||||
|
||||
async getEmailAccounts() {
|
||||
return fetchApi<Array<{
|
||||
id: string
|
||||
email: string
|
||||
provider: 'gmail' | 'outlook' | 'imap'
|
||||
connected: boolean
|
||||
lastSync?: string
|
||||
}>>(`/email/accounts?userId=${userId}`)
|
||||
}>>('/email/accounts')
|
||||
},
|
||||
|
||||
async connectEmailAccount(userId: string, provider: 'gmail' | 'outlook', email: string, accessToken: string, refreshToken?: string) {
|
||||
async connectEmailAccount(provider: 'gmail' | 'outlook', email: string, accessToken: string, refreshToken?: string) {
|
||||
return fetchApi<{ accountId: string }>('/email/connect', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, provider, email, accessToken, refreshToken }),
|
||||
body: JSON.stringify({ provider, email, accessToken, refreshToken }),
|
||||
})
|
||||
},
|
||||
|
||||
async connectImapAccount(
|
||||
userId: string,
|
||||
params: { email: string; password: string; imapHost?: string; imapPort?: number; imapSecure?: boolean }
|
||||
) {
|
||||
async connectImapAccount(params: {
|
||||
email: string
|
||||
password: string
|
||||
imapHost?: string
|
||||
imapPort?: number
|
||||
imapSecure?: boolean
|
||||
}) {
|
||||
return fetchApi<{ accountId: string }>('/email/connect', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
provider: 'imap',
|
||||
email: params.email,
|
||||
accessToken: params.password,
|
||||
@@ -87,8 +173,8 @@ export const api = {
|
||||
})
|
||||
},
|
||||
|
||||
async disconnectEmailAccount(accountId: string, userId: string) {
|
||||
return fetchApi<{ success: boolean }>(`/email/accounts/${accountId}?userId=${userId}`, {
|
||||
async disconnectEmailAccount(accountId: string) {
|
||||
return fetchApi<{ success: boolean }>(`/email/accounts/${accountId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
@@ -97,17 +183,17 @@ export const api = {
|
||||
// EMAIL STATS & SORTING
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getEmailStats(userId: string) {
|
||||
async getEmailStats() {
|
||||
return fetchApi<{
|
||||
totalSorted: number
|
||||
todaySorted: number
|
||||
weekSorted: number
|
||||
categories: Record<string, number>
|
||||
timeSaved: number
|
||||
}>(`/email/stats?userId=${userId}`)
|
||||
}>('/email/stats')
|
||||
},
|
||||
|
||||
async sortEmails(userId: string, accountId: string, maxEmails?: number, processAll?: boolean) {
|
||||
async sortEmails(accountId: string, maxEmails?: number, processAll?: boolean) {
|
||||
return fetchApi<{
|
||||
sorted: number
|
||||
inboxCleared: number
|
||||
@@ -127,11 +213,10 @@ export const api = {
|
||||
}>
|
||||
}>('/email/sort', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, accountId, maxEmails, processAll }),
|
||||
body: JSON.stringify({ accountId, maxEmails, processAll }),
|
||||
})
|
||||
},
|
||||
|
||||
// Demo sorting without account (for quick tests)
|
||||
async sortDemo(count: number = 10) {
|
||||
return fetchApi<{
|
||||
sorted: number
|
||||
@@ -152,8 +237,7 @@ export const api = {
|
||||
})
|
||||
},
|
||||
|
||||
// Connect demo account
|
||||
async connectDemoAccount(userId: string) {
|
||||
async connectDemoAccount() {
|
||||
return fetchApi<{
|
||||
accountId: string
|
||||
email: string
|
||||
@@ -161,11 +245,10 @@ export const api = {
|
||||
message?: string
|
||||
}>('/email/connect-demo', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
// Get categories
|
||||
async getCategories() {
|
||||
return fetchApi<Array<{
|
||||
id: string
|
||||
@@ -177,8 +260,7 @@ export const api = {
|
||||
}>>('/email/categories')
|
||||
},
|
||||
|
||||
// Get today's digest
|
||||
async getDigest(userId: string) {
|
||||
async getDigest() {
|
||||
return fetchApi<{
|
||||
date: string
|
||||
totalSorted: number
|
||||
@@ -188,11 +270,10 @@ export const api = {
|
||||
highlights: Array<{ type: string; count: number; message: string }>
|
||||
suggestions: Array<{ type: string; message: string }>
|
||||
hasData: boolean
|
||||
}>(`/email/digest?userId=${userId}`)
|
||||
}>('/email/digest')
|
||||
},
|
||||
|
||||
// Get digest history
|
||||
async getDigestHistory(userId: string, days: number = 7) {
|
||||
async getDigestHistory(days: number = 7) {
|
||||
return fetchApi<{
|
||||
days: number
|
||||
digests: Array<{
|
||||
@@ -207,15 +288,15 @@ export const api = {
|
||||
inboxCleared: number
|
||||
timeSavedMinutes: number
|
||||
}
|
||||
}>(`/email/digest/history?userId=${userId}&days=${days}`)
|
||||
}>(`/email/digest/history?days=${days}`)
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// OAUTH
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getOAuthUrl(provider: 'gmail' | 'outlook', userId: string) {
|
||||
return fetchApi<{ url: string }>(`/oauth/${provider}/connect?userId=${userId}`)
|
||||
async getOAuthUrl(provider: 'gmail' | 'outlook') {
|
||||
return fetchApi<{ url: string }>(`/oauth/${provider}/connect`)
|
||||
},
|
||||
|
||||
async getOAuthStatus() {
|
||||
@@ -229,10 +310,11 @@ export const api = {
|
||||
// SUBSCRIPTION
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getSubscriptionStatus(userId: string) {
|
||||
async getSubscriptionStatus() {
|
||||
return fetchApi<{
|
||||
status: string
|
||||
plan: string
|
||||
planDisplayName?: string
|
||||
isFreeTier: boolean
|
||||
emailsUsedThisMonth?: number
|
||||
emailsLimit?: number
|
||||
@@ -245,34 +327,34 @@ export const api = {
|
||||
}
|
||||
currentPeriodEnd?: string
|
||||
cancelAtPeriodEnd?: boolean
|
||||
}>(`/subscription/status?userId=${userId}`)
|
||||
}>('/subscription/status')
|
||||
},
|
||||
|
||||
async createSubscriptionCheckout(plan: string, userId: string, email?: string) {
|
||||
async createSubscriptionCheckout(plan: string, email?: string) {
|
||||
return fetchApi<{ url: string; sessionId: string }>('/subscription/checkout', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, plan, email }),
|
||||
body: JSON.stringify({ plan, email }),
|
||||
})
|
||||
},
|
||||
|
||||
async createPortalSession(userId: string) {
|
||||
async createPortalSession() {
|
||||
return fetchApi<{ url: string }>('/subscription/portal', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
async cancelSubscription(userId: string) {
|
||||
async cancelSubscription() {
|
||||
return fetchApi<{ success: boolean }>('/subscription/cancel', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
async reactivateSubscription(userId: string) {
|
||||
async reactivateSubscription() {
|
||||
return fetchApi<{ success: boolean }>('/subscription/reactivate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -280,16 +362,21 @@ export const api = {
|
||||
// USER PREFERENCES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getUserPreferences(userId: string) {
|
||||
async getUserPreferences() {
|
||||
return fetchApi<{
|
||||
vipSenders: Array<{ email: string; name?: string }>
|
||||
blockedSenders: string[]
|
||||
customRules: Array<{ condition: string; category: string }>
|
||||
priorityTopics: string[]
|
||||
}>(`/preferences?userId=${userId}`)
|
||||
profile?: {
|
||||
displayName?: string
|
||||
timezone?: string
|
||||
notificationPrefs?: Record<string, unknown>
|
||||
}
|
||||
}>('/preferences')
|
||||
},
|
||||
|
||||
async saveUserPreferences(userId: string, preferences: {
|
||||
async saveUserPreferences(preferences: {
|
||||
vipSenders?: Array<{ email: string; name?: string }>
|
||||
blockedSenders?: string[]
|
||||
customRules?: Array<{ condition: string; category: string }>
|
||||
@@ -298,7 +385,7 @@ export const api = {
|
||||
}) {
|
||||
return fetchApi<{ success: boolean }>('/preferences', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, ...preferences }),
|
||||
body: JSON.stringify(preferences),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -306,7 +393,7 @@ export const api = {
|
||||
// AI CONTROL
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getAIControlSettings(userId: string) {
|
||||
async getAIControlSettings() {
|
||||
return fetchApi<{
|
||||
enabledCategories: string[]
|
||||
categoryActions: Record<string, 'inbox' | 'archive_read' | 'star'>
|
||||
@@ -314,10 +401,10 @@ export const api = {
|
||||
cleanup?: unknown
|
||||
categoryAdvanced?: Record<string, unknown>
|
||||
version?: number
|
||||
}>(`/preferences/ai-control?userId=${userId}`)
|
||||
}>('/preferences/ai-control')
|
||||
},
|
||||
|
||||
async saveAIControlSettings(userId: string, settings: {
|
||||
async saveAIControlSettings(settings: {
|
||||
enabledCategories?: string[]
|
||||
categoryActions?: Record<string, 'inbox' | 'archive_read' | 'star'>
|
||||
autoDetectCompanies?: boolean
|
||||
@@ -327,33 +414,24 @@ export const api = {
|
||||
}) {
|
||||
return fetchApi<{ success: boolean }>('/preferences/ai-control', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, ...settings }),
|
||||
body: JSON.stringify(settings),
|
||||
})
|
||||
},
|
||||
|
||||
// Cleanup Preview - shows what would be cleaned up without actually doing it
|
||||
// TODO: Backend endpoint needs to be implemented
|
||||
// GET /api/preferences/ai-control/cleanup/preview?userId=xxx
|
||||
// Response: { preview: Array<{id, subject, from, date, reason}> }
|
||||
async getCleanupPreview(userId: string) {
|
||||
// TODO: Implement backend endpoint
|
||||
async getCleanupPreview(accountId: string) {
|
||||
return fetchApi<{
|
||||
preview: Array<{
|
||||
messages: Array<{
|
||||
id: string
|
||||
subject: string
|
||||
from: string
|
||||
date: string
|
||||
reason: 'read' | 'promotion'
|
||||
}>
|
||||
}>(`/preferences/ai-control/cleanup/preview?userId=${userId}`)
|
||||
count: number
|
||||
}>(`/email/${accountId}/cleanup/preview`)
|
||||
},
|
||||
|
||||
// Run cleanup now - executes cleanup for the user
|
||||
// POST /api/preferences/ai-control/cleanup/run
|
||||
// Body: { userId: string }
|
||||
// Response: { success: boolean, data: { readItems: number, promotions: number } }
|
||||
async runCleanup(userId: string) {
|
||||
// Uses existing /api/email/cleanup endpoint
|
||||
async runCleanup() {
|
||||
return fetchApi<{
|
||||
usersProcessed: number
|
||||
emailsProcessed: {
|
||||
@@ -363,40 +441,47 @@ export const api = {
|
||||
errors: Array<{ userId: string; error: string }>
|
||||
}>('/email/cleanup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
// Get cleanup status - last run info and counts
|
||||
// TODO: Backend endpoint needs to be implemented
|
||||
// GET /api/preferences/ai-control/cleanup/status?userId=xxx
|
||||
// Response: { lastRun?: string, lastRunCounts?: { readItems: number, promotions: number } }
|
||||
async getCleanupStatus(userId: string) {
|
||||
// TODO: Implement backend endpoint
|
||||
async getCleanupStatus(accountId: string) {
|
||||
return fetchApi<{
|
||||
lastRun?: string
|
||||
lastRunCounts?: {
|
||||
readItems: number
|
||||
promotions: number
|
||||
}
|
||||
}>(`/preferences/ai-control/cleanup/status?userId=${userId}`)
|
||||
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(userId: string) {
|
||||
async getCompanyLabels() {
|
||||
return fetchApi<Array<{
|
||||
id?: string
|
||||
name: string
|
||||
condition: string
|
||||
enabled: boolean
|
||||
category?: string
|
||||
}>>(`/preferences/company-labels?userId=${userId}`)
|
||||
}>>('/preferences/company-labels')
|
||||
},
|
||||
|
||||
async saveCompanyLabel(userId: string, companyLabel: {
|
||||
async saveCompanyLabel(companyLabel: {
|
||||
id?: string
|
||||
name: string
|
||||
condition: string
|
||||
@@ -411,12 +496,12 @@ export const api = {
|
||||
category?: string
|
||||
}>('/preferences/company-labels', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, companyLabel }),
|
||||
body: JSON.stringify({ companyLabel }),
|
||||
})
|
||||
},
|
||||
|
||||
async deleteCompanyLabel(userId: string, labelId: string) {
|
||||
return fetchApi<{ success: boolean }>(`/preferences/company-labels/${labelId}?userId=${userId}`, {
|
||||
async deleteCompanyLabel(labelId: string) {
|
||||
return fetchApi<{ success: boolean }>(`/preferences/company-labels/${labelId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
@@ -425,43 +510,36 @@ export const api = {
|
||||
// ME / ADMIN
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getMe(email: string) {
|
||||
return fetchApi<{ isAdmin: boolean }>(`/me?email=${encodeURIComponent(email)}`)
|
||||
async getMe() {
|
||||
return fetchApi<{ isAdmin: boolean }>('/me')
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// NAME LABELS (Workers – Admin only)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getNameLabels(userId: string, email: string) {
|
||||
async getNameLabels() {
|
||||
return fetchApi<Array<{
|
||||
id?: string
|
||||
name: string
|
||||
email?: string
|
||||
keywords?: string[]
|
||||
enabled: boolean
|
||||
}>>(`/preferences/name-labels?userId=${userId}&email=${encodeURIComponent(email)}`)
|
||||
}>>('/preferences/name-labels')
|
||||
},
|
||||
|
||||
async saveNameLabel(
|
||||
userId: string,
|
||||
userEmail: string,
|
||||
nameLabel: { id?: string; name: string; email?: string; keywords?: string[]; enabled: boolean }
|
||||
) {
|
||||
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({ userId, email: userEmail, nameLabel }),
|
||||
body: JSON.stringify({ nameLabel }),
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
async deleteNameLabel(userId: string, userEmail: string, labelId: string) {
|
||||
return fetchApi<{ success: boolean }>(
|
||||
`/preferences/name-labels/${labelId}?userId=${userId}&email=${encodeURIComponent(userEmail)}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
async deleteNameLabel(labelId: string) {
|
||||
return fetchApi<{ success: boolean }>(`/preferences/name-labels/${labelId}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -523,36 +601,36 @@ export const api = {
|
||||
// ONBOARDING
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getOnboardingStatus(userId: string) {
|
||||
async getOnboardingStatus() {
|
||||
return fetchApi<{
|
||||
onboarding_step: string
|
||||
completedSteps: string[]
|
||||
first_value_seen_at?: string
|
||||
skipped_at?: string
|
||||
}>(`/onboarding/status?userId=${userId}`)
|
||||
}>('/onboarding/status')
|
||||
},
|
||||
|
||||
async updateOnboardingStep(userId: string, step: string, completedSteps: string[] = []) {
|
||||
async updateOnboardingStep(step: string, completedSteps: string[] = []) {
|
||||
return fetchApi<{ step: string; completedSteps: string[] }>('/onboarding/step', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, step, completedSteps }),
|
||||
body: JSON.stringify({ step, completedSteps }),
|
||||
})
|
||||
},
|
||||
|
||||
async skipOnboarding(userId: string) {
|
||||
async skipOnboarding() {
|
||||
return fetchApi<{ skipped: boolean }>('/onboarding/skip', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
async resumeOnboarding(userId: string) {
|
||||
async resumeOnboarding() {
|
||||
return fetchApi<{
|
||||
onboarding_step: string
|
||||
completedSteps: string[]
|
||||
}>('/onboarding/resume', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -560,10 +638,10 @@ export const api = {
|
||||
// ACCOUNT MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async deleteAccount(userId: string) {
|
||||
async deleteAccount() {
|
||||
return fetchApi<{ success: boolean }>('/account/delete', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ userId }),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -571,17 +649,17 @@ export const api = {
|
||||
// REFERRALS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async getReferralCode(userId: string) {
|
||||
async getReferralCode() {
|
||||
return fetchApi<{
|
||||
referralCode: string
|
||||
referralCount: number
|
||||
}>(`/referrals/code?userId=${userId}`)
|
||||
}>('/referrals/code')
|
||||
},
|
||||
|
||||
async trackReferral(userId: string, referralCode: string) {
|
||||
async trackReferral(referralCode: string) {
|
||||
return fetchApi<{ success: boolean }>('/referrals/track', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId, referralCode }),
|
||||
body: JSON.stringify({ referralCode }),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user