Add chat-first onboarding and intelligent context-based paywall

Replaces the hardcoded message limit with a model-driven paywall and
removes registration before the first question:

- Onboarding: the app opens directly into the chat. Dynamic conversation
  starters are served by GET /suggestions (services/api/suggestions.json),
  updatable with a deploy — no app release needed. Guests chat via
  POST /guest/chat, identified by an app-generated device id; sign-in
  moves to the paywall, where a purchase must attach to an account.
- Free experience: the model runs in discovery mode — follow-up
  questions, pattern identification, visible understanding — building an
  analysis without delivering the full solution.
- Intelligent paywall: the model returns structured output (reply +
  analysis_ready). Only when the problem is described, the information is
  sufficient and an action plan is ready does it write a calm transition
  and pause the conversation. Manufactured urgency, emotional pressure,
  fake readiness and mid-answer stops are explicitly forbidden.
- Premium: on unlock the app resends the transcript and the backend
  immediately delivers the full analysis, strategies and exercises, then
  the dialogue continues without restriction.
- The old FREE_MESSAGE_LIMIT becomes MESSAGE_CAP (default 200/30 days),
  kept purely as an abuse backstop — it is not the paywall.
- WelcomeScreen removed; the app is now two views (Chat, Paywall).

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:42:44 +00:00
parent 07fbeea09b
commit 6b7f0263e4
11 changed files with 498 additions and 221 deletions
+31 -15
View File
@@ -1,29 +1,45 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { getAccessToken } from './src/auth/session';
import type { ChatMessage } from './src/api/client';
import { ChatScreen } from './src/screens/ChatScreen';
import { PaywallScreen } from './src/screens/PaywallScreen';
import { WelcomeScreen } from './src/screens/WelcomeScreen';
type View = 'loading' | 'welcome' | 'chat' | 'paywall';
/**
* Three views, no navigation library. The backend decides when the paywall
* appears (HTTP 402 on /chat).
* Two views, no navigation library. The user lands directly in the chat —
* no registration before the first question. The backend decides the paywall
* moment (the analysis is ready); after unlock the conversation continues
* with the full analysis delivered immediately.
*/
export default function App() {
const [view, setView] = useState<View>('loading');
useEffect(() => {
getAccessToken().then((token) => setView(token ? 'chat' : 'welcome'));
}, []);
const [view, setView] = useState<'chat' | 'paywall'>('chat');
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [paywalled, setPaywalled] = useState(false);
const [deliverAnalysis, setDeliverAnalysis] = useState(false);
return (
<>
<StatusBar style="dark" />
{view === 'welcome' && <WelcomeScreen onSignedIn={() => setView('chat')} />}
{view === 'chat' && <ChatScreen onLimitReached={() => setView('paywall')} />}
{view === 'paywall' && <PaywallScreen onSubscribed={() => setView('chat')} />}
{view === 'chat' && (
<ChatScreen
messages={messages}
setMessages={setMessages}
paywalled={paywalled}
onPaywallTriggered={() => setPaywalled(true)}
onContinueWithPremium={() => setView('paywall')}
deliverAnalysis={deliverAnalysis}
onAnalysisDelivered={() => setDeliverAnalysis(false)}
/>
)}
{view === 'paywall' && (
<PaywallScreen
onSubscribed={() => {
setPaywalled(false);
setDeliverAnalysis(true);
setView('chat');
}}
onDismiss={() => setView('chat')}
/>
)}
</>
);
}