Bygg NeuroSemantics AI: minimal mobilapp, en backend, IaC och CI/CD

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
This commit is contained in:
Claude
2026-08-03 14:35:48 +00:00
parent 508cb534b0
commit 07fbeea09b
199 changed files with 13626 additions and 18001 deletions
+67
View File
@@ -0,0 +1,67 @@
import { Platform } from 'react-native';
import {
endConnection,
getAvailablePurchases,
initConnection,
requestSubscription,
type SubscriptionPurchase,
} from 'react-native-iap';
import { verifyPurchase } from '../api/client';
import { appConfig } from '../config';
export type Plan = 'monthly' | 'yearly';
const platform: 'ios' | 'android' = Platform.OS === 'ios' ? 'ios' : 'android';
export function productIdFor(plan: Plan): string {
if (platform === 'ios') {
return plan === 'monthly' ? appConfig.iosMonthlyProductId : appConfig.iosYearlyProductId;
}
return plan === 'monthly' ? appConfig.androidMonthlyProductId : appConfig.androidYearlyProductId;
}
function receiptOf(purchase: SubscriptionPurchase): string {
// iOS: base64 app receipt. Android: Play Billing purchase token.
return platform === 'ios' ? purchase.transactionReceipt : (purchase.purchaseToken ?? '');
}
async function withConnection<T>(fn: () => Promise<T>): Promise<T> {
await initConnection();
try {
return await fn();
} finally {
await endConnection();
}
}
/**
* Runs the native purchase flow, then lets the backend verify the receipt
* with the store. Returns the resulting subscription status.
*/
export async function purchase(plan: Plan): Promise<'free' | 'active'> {
const productId = productIdFor(plan);
return withConnection(async () => {
await requestSubscription({ sku: productId });
const purchases = await getAvailablePurchases();
const match = purchases.find((p) => p.productId === productId);
if (!match) throw new Error('Purchase not found after transaction');
const result = await verifyPurchase({ platform, productId, receipt: receiptOf(match) });
return result.subscriptionStatus;
});
}
/** Re-checks existing purchases with the store ("Restore Purchase"). */
export async function restore(): Promise<'free' | 'active'> {
return withConnection(async () => {
const purchases = await getAvailablePurchases();
for (const p of purchases) {
const result = await verifyPurchase({
platform,
productId: p.productId,
receipt: receiptOf(p),
});
if (result.subscriptionStatus === 'active') return 'active';
}
return 'free';
});
}