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:
@@ -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';
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user