07fbeea09b
Ersätter den tidigare webappen på denna branch med ett fokuserat monorepo: - apps/mobile: Expo/React Native med tre vyer (Welcome, Chat, Paywall), Cognito hosted UI-inloggning (Apple/Google/e-post) och In-App Purchase/Play Billing via en gemensam purchases-modul. - services/api: en enda Lambda-backend — OpenAI Responses API med Markdown-kunskapsbas som systeminstruktioner, free tier-gräns i PostgreSQL (HTTP 402 -> paywall) och kvittoverifiering bakom ett delat PaymentProvider-interface (Apple/Google, Stripe kan läggas till för webb senare). - infra: AWS CDK-stack med API Gateway (JWT-authorizer), Lambda, Cognito, Aurora Serverless v2 och Secrets Manager. - db/migrations: minimal datamodell (users + usage), inga konversationer sparas. - GitHub Actions: CI (lint, typecheck, test) och deploy från main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118DaxZR36RpnY524vRqx3z
68 lines
1.7 KiB
TypeScript
68 lines
1.7 KiB
TypeScript
import { appConfig } from '../config';
|
|
import { getAccessToken } from '../auth/session';
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public readonly status: number,
|
|
public readonly code: string,
|
|
) {
|
|
super(`API error ${status}: ${code}`);
|
|
}
|
|
}
|
|
|
|
// Mirrors the backend contract in services/api/src/handler.ts.
|
|
export interface ChatMessage {
|
|
role: 'user' | 'assistant';
|
|
content: string;
|
|
}
|
|
|
|
export interface Me {
|
|
subscriptionStatus: 'free' | 'active';
|
|
messagesUsed: number;
|
|
freeMessageLimit: number;
|
|
}
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const token = await getAccessToken();
|
|
if (!token) throw new ApiError(401, 'not_signed_in');
|
|
|
|
const response = await fetch(`${appConfig.apiUrl}${path}`, {
|
|
...init,
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
...init?.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let code = 'unknown';
|
|
try {
|
|
code = ((await response.json()) as { error?: string }).error ?? 'unknown';
|
|
} catch {
|
|
// keep 'unknown'
|
|
}
|
|
throw new ApiError(response.status, code);
|
|
}
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
export function fetchMe(): Promise<Me> {
|
|
return request<Me>('/me');
|
|
}
|
|
|
|
export function sendChat(messages: ChatMessage[]): Promise<{ reply: string }> {
|
|
return request<{ reply: string }>('/chat', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ messages }),
|
|
});
|
|
}
|
|
|
|
export function verifyPurchase(body: {
|
|
platform: 'ios' | 'android';
|
|
productId: string;
|
|
receipt: string;
|
|
}): Promise<{ subscriptionStatus: 'free' | 'active' }> {
|
|
return request('/subscription/verify', { method: 'POST', body: JSON.stringify(body) });
|
|
}
|