From e5b81f46b08d9441c498cf7a468d541299b18075 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 22:37:25 +0000 Subject: [PATCH] Store readiness: in-app account deletion, privacy policy, terms, listing copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0118DaxZR36RpnY524vRqx3z --- LAUNCH.md | 11 +++- apps/mobile/App.tsx | 19 ++++++- apps/mobile/src/api/client.ts | 5 ++ apps/mobile/src/screens/ChatScreen.tsx | 54 ++++++++++++++++++- docs/store/listing.md | 74 ++++++++++++++++++++++++++ docs/store/privacy-policy.md | 62 +++++++++++++++++++++ docs/store/terms-of-service.md | 62 +++++++++++++++++++++ infra/lib/semantika-stack.ts | 2 +- services/api/src/db.ts | 6 +++ services/api/src/handler.ts | 15 +++++- services/api/test/handler.test.ts | 12 ++++- 11 files changed, 314 insertions(+), 8 deletions(-) create mode 100644 docs/store/listing.md create mode 100644 docs/store/privacy-policy.md create mode 100644 docs/store/terms-of-service.md diff --git a/LAUNCH.md b/LAUNCH.md index cb9a2e1..a09d1cb 100644 --- a/LAUNCH.md +++ b/LAUNCH.md @@ -46,8 +46,15 @@ the README). two subscriptions with the same ids and prices. Create a service account with Play Developer API access → its JSON key becomes `GOOGLE_SERVICE_ACCOUNT_JSON`. -- [ ] Store copy follows the honest-claims rule: inspiration, not proven - effects. Positioning: reflection partner — not therapy. +- [ ] Store copy: use `docs/store/listing.md` (already written to the + honest-claims rule — inspiration, not proven effects; reflection + partner, not therapy). +- [ ] Fill the [PLACEHOLDERS] in `docs/store/privacy-policy.md` and + `docs/store/terms-of-service.md`, host them on a public URL (GitHub + Pages is enough), and set the URLs in both store consoles. +- [ ] App Privacy / Data safety forms: mappings are in + `docs/store/listing.md`. In-app account deletion (App Store 5.1.1) + is implemented: Account → Delete account. ## 5. Builds diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index 9e2dbcc..c574b80 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { StatusBar } from 'expo-status-bar'; import type { ChatMessage } from './src/api/client'; +import { getAccessToken } from './src/auth/session'; import { ChatScreen } from './src/screens/ChatScreen'; import { PaywallScreen } from './src/screens/PaywallScreen'; @@ -15,6 +16,19 @@ export default function App() { const [messages, setMessages] = useState([]); const [paywalled, setPaywalled] = useState(false); const [deliverAnalysis, setDeliverAnalysis] = useState(false); + const [signedIn, setSignedIn] = useState(false); + + useEffect(() => { + getAccessToken().then((token) => setSignedIn(token !== null)); + }, []); + + function resetToSignedOut() { + setSignedIn(false); + setMessages([]); + setPaywalled(false); + setDeliverAnalysis(false); + setView('chat'); + } return ( <> @@ -28,11 +42,14 @@ export default function App() { onContinueWithPremium={() => setView('paywall')} deliverAnalysis={deliverAnalysis} onAnalysisDelivered={() => setDeliverAnalysis(false)} + signedIn={signedIn} + onSignedOut={resetToSignedOut} /> )} {view === 'paywall' && ( { + setSignedIn(true); setPaywalled(false); setDeliverAnalysis(true); setView('chat'); diff --git a/apps/mobile/src/api/client.ts b/apps/mobile/src/api/client.ts index 604192f..35e2ecc 100644 --- a/apps/mobile/src/api/client.ts +++ b/apps/mobile/src/api/client.ts @@ -89,6 +89,11 @@ export async function fetchMe(): Promise { return request('/me', {}, await authHeader()); } +/** Permanently deletes the account and its usage data (App Store 5.1.1). */ +export async function deleteAccount(): Promise { + await request<{ deleted: boolean }>('/me', { method: 'DELETE' }, await authHeader()); +} + export async function verifyPurchase(body: { platform: 'ios' | 'android'; productId: string; diff --git a/apps/mobile/src/screens/ChatScreen.tsx b/apps/mobile/src/screens/ChatScreen.tsx index 2797a1a..689252b 100644 --- a/apps/mobile/src/screens/ChatScreen.tsx +++ b/apps/mobile/src/screens/ChatScreen.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { + Alert, FlatList, KeyboardAvoidingView, Platform, @@ -9,7 +10,8 @@ import { TextInput, View, } from 'react-native'; -import { fetchSuggestions, sendChat, type ChatMessage } from '../api/client'; +import { deleteAccount, fetchSuggestions, sendChat, type ChatMessage } from '../api/client'; +import { signOut } from '../auth/session'; import { colors, spacing, type } from '../theme'; interface Props { @@ -22,6 +24,9 @@ interface Props { /** Set right after unlock: fetch the promised analysis automatically. */ deliverAnalysis: boolean; onAnalysisDelivered: () => void; + /** Shows the quiet Account affordance (sign out / delete account). */ + signedIn: boolean; + onSignedOut: () => void; } const SUGGESTIONS_SHOWN = 4; @@ -39,6 +44,8 @@ export function ChatScreen({ onContinueWithPremium, deliverAnalysis, onAnalysisDelivered, + signedIn, + onSignedOut, }: Props) { const [suggestions, setSuggestions] = useState([]); const [draft, setDraft] = useState(''); @@ -92,6 +99,42 @@ export function ChatScreen({ const showSuggestions = messages.length === 0 && suggestions.length > 0; + // No menus, no settings view: account management lives in two native + // dialogs behind one quiet link (in-app deletion per App Store 5.1.1). + function openAccount() { + Alert.alert('Account', undefined, [ + { + text: 'Sign out', + onPress: () => { + signOut().then(onSignedOut); + }, + }, + { + text: 'Delete account', + style: 'destructive', + onPress: () => + Alert.alert( + 'Delete account?', + 'This permanently deletes your account and usage data. Conversations are never stored. An active subscription is cancelled in your App Store or Google Play settings.', + [ + { + text: 'Delete', + style: 'destructive', + onPress: () => { + deleteAccount() + .then(() => signOut()) + .then(onSignedOut) + .catch(() => setError('Something went wrong. Please try again.')); + }, + }, + { text: 'Cancel', style: 'cancel' }, + ], + ), + }, + { text: 'Cancel', style: 'cancel' }, + ]); + } + return ( )} + {signedIn ? ( + + Account + + ) : null} ); } @@ -191,8 +239,10 @@ const styles = StyleSheet.create({ alignItems: 'flex-end', gap: spacing.s, paddingHorizontal: spacing.l, - paddingBottom: spacing.xl, + paddingBottom: spacing.m, }, + accountLink: { alignItems: 'center', paddingBottom: spacing.l }, + accountText: { ...type.caption }, input: { flex: 1, ...type.body, diff --git a/docs/store/listing.md b/docs/store/listing.md new file mode 100644 index 0000000..985bcba --- /dev/null +++ b/docs/store/listing.md @@ -0,0 +1,74 @@ +# Store listing copy + +All copy follows the honest-claims rule: neurosemantics/NLP as inspiration +and models for reflection — never "scientifically proven", never promised +results. Tone: calm, clear, no hype. + +## App Store (iOS) + +- **Name:** Semantika +- **Subtitle** (≤30 chars): `Think Beyond Thought` +- **Category:** Lifestyle (primary), Education (secondary) +- **Age rating:** 17+ (unrestricted web-like AI content; matches 18+ terms) +- **Keywords** (≤100 chars): + `reflection,thinking,clarity,communication,meaning,mindset,self insight,NLP,decision,conversation` +- **Promotional text:** + A quiet conversation partner for your thinking. Explore how you create + meaning — one structured conversation at a time. +- **Description:** + + Semantika is a minimalist AI conversation partner for reflection, + inspired by neurosemantic principles. + + Start a conversation about what is on your mind — a difficult + conversation ahead, a decision you keep postponing, a pattern you keep + repeating. Semantika listens, asks considered follow-up questions, and + helps you see how you interpret your situation. When the picture is + clear, it offers a structured analysis, concrete recommendations, and + practical exercises. + + No feeds. No streaks. No noise. One conversation at a time. + + — Begin immediately: no registration before your first question + — Conversations are never stored, and never used for training + — A calm, structured method: acknowledge, explore, lift, challenge + — Premium: full analyses, personalised action plans, unlimited dialogue + + Semantika is not therapy and does not replace professional care. Its + models are offered as perspectives for reflection, inspired by + neurosemantics and NLP. + + Subscriptions ($5.99/month or $49.99/year) renew automatically and can + be cancelled anytime in your App Store settings. + Privacy policy and terms: [PRIVACY_POLICY_URL] · [TERMS_URL] + +## Google Play (Android) + +- **App name:** Semantika +- **Short description** (≤80 chars): + `A quiet AI conversation partner for reflection, clarity and better decisions.` +- **Full description:** reuse the App Store description above. +- **Category:** Lifestyle + +## App Privacy (Apple questionnaire mapping) + +- **Contact info → Email address:** collected, linked to identity, for app + functionality only. No tracking. +- **Identifiers → User ID:** account id / device id, app functionality only. +- **Purchases:** subscription status, app functionality only. +- **User content (chat):** processed but **not collected/stored**; declare + as not collected (transient processing), and describe in the privacy + policy. +- **Tracking:** none. No ads, no data sold or shared for advertising. + +## Data safety (Google Play form) + +- Collected: email, user ids, purchase history (subscription status). +- Shared: none. Encrypted in transit: yes. Deletable: yes, in-app. +- Chat content: processed ephemerally, not stored. + +## Required URLs + +Host `privacy-policy.md` and `terms-of-service.md` on a public URL (GitHub +Pages is enough for the beta) and set them in both store consoles. Replace +the [PLACEHOLDERS] in both documents first. diff --git a/docs/store/privacy-policy.md b/docs/store/privacy-policy.md new file mode 100644 index 0000000..0b21287 --- /dev/null +++ b/docs/store/privacy-policy.md @@ -0,0 +1,62 @@ +# Semantika — Privacy Policy + +_Last updated: [DATE]. Controller: [COMPANY NAME, ADDRESS, ORG NUMBER]. +Contact: [support@semantika.app]._ + +Semantika is built to know as little about you as possible. This policy +describes exactly what we process, why, and what we never do. + +## What we store + +| Data | Purpose | Kept until | +| ----------------------------------------------------------- | ------------------------------------------- | -------------------------------------- | +| Email address and sign-in provider (Apple, Google or email) | Your account | Account deletion | +| Subscription status (`free`/`active`) | Unlocking Premium | Account deletion | +| Message counters and their reset date | Free-tier limits and abuse prevention | Account deletion | +| A random device identifier (guests only) | Letting you chat before creating an account | Removed when you delete the app's data | + +That is the complete list. We do not collect names, birthdays, locations, +contacts, photos, advertising identifiers, or analytics profiles. + +## Your conversations + +- Conversations are **never stored on our servers**. They live in your + app's memory and disappear when you close the conversation. +- To generate each reply, the current conversation is transmitted over + encrypted connections to our AI provider, **OpenAI** (acting as a data + processor), and processed transiently. Under OpenAI's API terms, API data + is **not used to train their models**. +- We never use your conversations for advertising, profiling, or training + of any kind. +- Error logs never contain message content. + +## Payments + +Purchases are handled entirely by Apple (App Store) or Google (Google +Play). We never see your card details — only a receipt that we verify with +the store and your resulting subscription status. + +## Legal bases (GDPR) + +- **Performance of contract** — providing the service you signed up for. +- **Legitimate interest** — preventing abuse of the free tier. + +## Your rights + +You can access, correct, export, or erase your data at any time. Deleting +your account (**Account → Delete account** in the app) permanently removes +everything in the table above. Cancel an active subscription in your App +Store or Google Play settings — deleting the account does not cancel the +store subscription. For anything else, contact [support@semantika.app]. +You may also lodge a complaint with your supervisory authority (in Sweden: +IMY). + +## Security + +All traffic is encrypted (HTTPS). Authentication uses AWS Cognito. +Databases are encrypted at rest and unreachable from the public internet. + +## Changes + +If this policy changes materially, we will note it in the app before the +change takes effect. diff --git a/docs/store/terms-of-service.md b/docs/store/terms-of-service.md new file mode 100644 index 0000000..9eaa135 --- /dev/null +++ b/docs/store/terms-of-service.md @@ -0,0 +1,62 @@ +# Semantika — Terms of Service + +_Last updated: [DATE]. Provider: [COMPANY NAME, ADDRESS, ORG NUMBER]. +Contact: [support@semantika.app]._ + +## What Semantika is — and is not + +Semantika is an AI conversation partner for reflection, inspired by +neurosemantic and NLP models. It helps you explore how you create meaning, +interpret your experiences, and communicate. + +Semantika is **not** therapy, medical or psychological care, or +professional advice of any kind. Its models are offered as perspectives for +reflection — not as scientifically proven methods — and its replies are +generated by an AI system and may be incorrect. You remain responsible for +your decisions. + +**If you are in crisis or experiencing acute distress, do not use the app — +contact your local emergency number or a health professional immediately.** + +## Eligibility + +You must be at least 18 years old to use Semantika. + +## Subscriptions + +- Plans: Monthly and Yearly, purchased through Apple App Store or Google + Play. Prices are shown in the app before purchase. +- Subscriptions renew automatically until cancelled in your App Store or + Google Play settings, at least 24 hours before the current period ends. +- Deleting your account does not cancel a store subscription — cancel it in + the store first. +- Refunds are handled by Apple and Google under their policies. + +## Acceptable use + +Do not use the service unlawfully, attempt to disrupt or reverse-engineer +it, or circumvent usage limits. We may suspend accounts that do. + +## Privacy + +How we handle data is described in the [Privacy Policy](privacy-policy.md). +In short: conversations are never stored, and we keep the absolute minimum. + +## Liability + +To the extent permitted by law, Semantika is provided "as is" and our +liability is limited to the amount you paid for the service in the twelve +months preceding the claim. Nothing in these terms limits liability that +cannot be limited under applicable law. + +## Changes and termination + +We may update these terms; material changes will be announced in the app. +You can stop using the service and delete your account at any time +(**Account → Delete account**). + +## Governing law + +These terms are governed by the laws of Sweden, and disputes are resolved +by Swedish courts, without limiting mandatory consumer protections in your +country of residence. diff --git a/infra/lib/semantika-stack.ts b/infra/lib/semantika-stack.ts index 25939a3..b767774 100644 --- a/infra/lib/semantika-stack.ts +++ b/infra/lib/semantika-stack.ts @@ -156,7 +156,7 @@ export class SemantikaStack extends Stack { ); const api = new HttpApi(this, 'HttpApi', { defaultAuthorizer: authorizer }); const integration = new HttpLambdaIntegration('ApiIntegration', apiFunction); - api.addRoutes({ path: '/me', methods: [HttpMethod.GET], integration }); + api.addRoutes({ path: '/me', methods: [HttpMethod.GET, HttpMethod.DELETE], integration }); api.addRoutes({ path: '/chat', methods: [HttpMethod.POST], integration }); api.addRoutes({ path: '/subscription/verify', methods: [HttpMethod.POST], integration }); diff --git a/services/api/src/db.ts b/services/api/src/db.ts index 2388be0..08e8f1f 100644 --- a/services/api/src/db.ts +++ b/services/api/src/db.ts @@ -75,6 +75,12 @@ export async function incrementUsage(userId: string): Promise { 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 { + const db = await getPool(); + await db.query(`delete from users where id = $1`, [userId]); +} + export async function setSubscriptionStatus( userId: string, status: 'free' | 'active', diff --git a/services/api/src/handler.ts b/services/api/src/handler.ts index 934e5ce..4e3095a 100644 --- a/services/api/src/handler.ts +++ b/services/api/src/handler.ts @@ -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 ({ + 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', () => {