Store readiness: in-app account deletion, privacy policy, terms, listing copy

Closes the remaining store-compliance gaps before App Store / Play
submission:

- Account deletion (App Store guideline 5.1.1): DELETE /me removes the
  user row (usage cascades). The app exposes it through one quiet
  'Account' caption link under the chat, visible only when signed in,
  driving two native dialogs (Sign out / Delete account with a
  destructive confirm) — no new views, no menus, minimalism intact.
  Deletion signs out and resets the app; copy notes that store
  subscriptions are cancelled in App Store / Play settings.
- docs/store/privacy-policy.md: the complete data inventory (matching
  the actual schema), transient OpenAI processing with no training, no
  profiling or ads, GDPR legal bases and rights, in-app erasure.
- docs/store/terms-of-service.md: not-therapy positioning with crisis
  guidance, AI-generated-content caveat, 18+ eligibility,
  auto-renewal/cancellation terms, liability, Swedish governing law.
- docs/store/listing.md: App Store and Play copy written to the
  honest-claims rule (subtitle 'Think Beyond Thought', keywords,
  descriptions), plus App Privacy and Data safety questionnaire
  mappings.
- LAUNCH.md updated: host the policy/terms, set store URLs, use the
  prepared listing copy.
- Tests: 30 passing (adds DELETE /me coverage).

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 22:37:25 +00:00
parent bf5c4e5f00
commit e5b81f46b0
11 changed files with 314 additions and 8 deletions
+6
View File
@@ -75,6 +75,12 @@ export async function incrementUsage(userId: string): Promise<void> {
await db.query(`update usage set messages_used = messages_used + 1 where user_id = $1`, [userId]);
}
/** Permanently removes the user; the usage row cascades. */
export async function deleteUser(userId: string): Promise<void> {
const db = await getPool();
await db.query(`delete from users where id = $1`, [userId]);
}
export async function setSubscriptionStatus(
userId: string,
status: 'free' | 'active',
+14 -1
View File
@@ -1,7 +1,14 @@
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { generateReply, type ChatMessage } from './chat.js';
import { config } from './config.js';
import { getOrCreateUser, getUsage, incrementUsage, resetUsage, type UserRow } from './db.js';
import {
deleteUser,
getOrCreateUser,
getUsage,
incrementUsage,
resetUsage,
type UserRow,
} from './db.js';
import { verifyAndApplyPurchase } from './subscription/index.js';
import type { VerifyPurchaseRequest } from './subscription/types.js';
import { canSendMessage, shouldResetUsage } from './usage.js';
@@ -142,6 +149,12 @@ export async function handler(event: APIGatewayProxyEventV2): Promise<APIGateway
switch (route) {
case 'GET /me':
return await handleMe(user);
case 'DELETE /me':
// In-app account deletion (App Store guideline 5.1.1). Removes the
// user and usage rows; store subscriptions are cancelled by the user
// in App Store / Play settings.
await deleteUser(user.id);
return json(200, { deleted: true });
case 'POST /chat':
return await handleChat(user, event.body);
case 'POST /subscription/verify':
+11 -1
View File
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../src/db.js', () => ({
deleteUser: vi.fn(),
getOrCreateUser: vi.fn(),
getUsage: vi.fn(),
incrementUsage: vi.fn(),
@@ -14,7 +15,7 @@ vi.mock('../src/subscription/index.js', () => ({
}));
import { generateReply } from '../src/chat.js';
import { getOrCreateUser, getUsage, incrementUsage } from '../src/db.js';
import { deleteUser, getOrCreateUser, getUsage, incrementUsage } from '../src/db.js';
import { handler } from '../src/handler.js';
import { verifyAndApplyPurchase } from '../src/subscription/index.js';
@@ -101,6 +102,15 @@ describe('authorization', () => {
});
expect(getOrCreateUser).toHaveBeenCalledWith('sub-1', 'a@b.se', 'apple');
});
it('deletes the account on DELETE /me', async () => {
const { status, body } = await call('DELETE', '/me', {
claims: { sub: 'sub-1', email: 'a@b.se' },
});
expect(status).toBe(200);
expect(body.deleted).toBe(true);
expect(deleteUser).toHaveBeenCalledWith('user-1');
});
});
describe('chat and the intelligent paywall', () => {