Insight-first monetization: trust-building free tier and pricing

Adjusts the paywall strategy from withholding toward insight-first:

- Free-tier instructions rewritten around the monetization philosophy:
  the goal is not to interrupt the conversation but to build trust. The
  free experience should leave the user feeling understood, respected
  and curious to continue, and should help the user reach a meaningful
  insight of their own. The rule is included verbatim: 'Never
  manufacture suspense. Create genuine curiosity by helping the user
  reach a meaningful insight, then offer a deeper level of analysis in
  Premium.'
- Premium transition sharpened: analysis_ready may only fire when the
  conversation has reached a meaningful point and the natural next step
  is a complete analysis, structured framework, personalised strategy,
  practical exercises or a step-by-step action plan — pausing
  immediately before that delivery, never mid-sentence or mid-
  explanation. Transition example updated to the new wording. Product
  feeling encoded: always 'I genuinely want to continue this
  conversation', never 'they stopped me just to make me pay'.
- Pricing: paywall now shows plan prices — localized prices fetched
  from the store via react-native-iap, falling back to $5.99/month and
  $49.99/year (~30% below monthly) until store config exists — plus
  the Premium includes list (unlimited conversations, unlimited
  analyses, personalised guidance, future feature updates).
- README: monetization philosophy, product feeling and pricing sections
  updated to match.

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 15:15:46 +00:00
parent 77d0322c09
commit 720a8fdca4
4 changed files with 136 additions and 26 deletions
+30 -9
View File
@@ -71,18 +71,32 @@ it is not built.
### The intelligent paywall
**Monetization philosophy.** The goal is not to interrupt the conversation
— the goal is to build trust. The free experience should make the user feel
understood, respected, and curious to continue; only when the conversation
has reached a meaningful point is Premium introduced. The rule given to the
model, verbatim: _"Never manufacture suspense. Create genuine curiosity by
helping the user reach a meaningful insight, then offer a deeper level of
analysis in Premium."_
The paywall is not a hardcoded message count. Free conversations run in
_discovery mode_: the model asks relevant follow-up questions, names
patterns, shows understanding, and builds toward an analysis — without
delivering the full solution. When the problem is described, the information
is sufficient, and a concrete action plan is ready, the model signals
`analysis_ready`, writes a calm transition ("…I have a concrete strategy I
would recommend. Continue with Premium to see the analysis and the
recommended steps."), and the conversation pauses.
patterns, shows understanding, and helps the user reach a meaningful
insight of their own. When the conversation has reached that point and the
natural next step is a complete analysis, a structured framework, a
personalised strategy, practical exercises, or a step-by-step action plan,
the model pauses immediately _before_ delivering it — never mid-sentence,
never in the middle of an explanation — signals `analysis_ready`, and
writes a calm transition: "I think I understand the core pattern behind
what you've described. There are a few recurring themes that stand out, and
I have a structured way of working through them with you. Unlock Premium to
continue with the full analysis and your personalised action plan."
The instructions explicitly forbid manufactured urgency, emotional pressure,
fake readiness, and stopping mid-answer — Premium should feel like the
natural continuation of an already valuable dialogue.
**Product feeling.** Every interaction should leave the user thinking
"this understands me". Every Premium conversion should feel like "I
genuinely want to continue this conversation" — never "they stopped me just
to make me pay". Manufactured urgency, emotional pressure and fake
readiness are explicitly forbidden in the instructions.
On unlock, the app resends the transcript; premium mode then delivers the
full analysis, recommended strategies and concrete exercises immediately,
@@ -109,6 +123,13 @@ provider differs per platform behind one `PaymentProvider` interface:
Plans: Monthly and Yearly. Nothing else.
**Pricing.** $5.99/month, $49.99/year (≈30 % below the monthly rate).
Premium includes unlimited conversations, unlimited analyses, personalised
guidance, and future feature updates. Prices are configured in App Store
Connect / Play Console on the products `semantika_monthly` and
`semantika_yearly`; the paywall fetches localized prices from the store and
falls back to these defaults until the store answers.
## Getting started
```sh
+45
View File
@@ -2,6 +2,7 @@ import { Platform } from 'react-native';
import {
endConnection,
getAvailablePurchases,
getSubscriptions,
initConnection,
requestSubscription,
type SubscriptionPurchase,
@@ -11,6 +12,17 @@ import { appConfig } from '../config';
export type Plan = 'monthly' | 'yearly';
export interface PlanPrices {
monthly: string;
yearly: string;
}
/** Shown until the store answers; real localized prices come from the store. */
export const DEFAULT_PRICES: PlanPrices = {
monthly: '$5.99 / month',
yearly: '$49.99 / year',
};
const platform: 'ios' | 'android' = Platform.OS === 'ios' ? 'ios' : 'android';
export function productIdFor(plan: Plan): string {
@@ -34,6 +46,39 @@ async function withConnection<T>(fn: () => Promise<T>): Promise<T> {
}
}
function priceOf(product: unknown): string | undefined {
if (!product) return undefined;
// iOS exposes localizedPrice; Android nests it in the first offer's pricing phases.
const p = product as {
localizedPrice?: string;
subscriptionOfferDetails?: {
pricingPhases?: { pricingPhaseList?: { formattedPrice?: string }[] };
}[];
};
return (
p.localizedPrice ??
p.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[0]?.formattedPrice
);
}
/** Localized prices from the store, falling back to the defaults. */
export async function getPrices(): Promise<PlanPrices> {
try {
return await withConnection(async () => {
const products = await getSubscriptions({
skus: [productIdFor('monthly'), productIdFor('yearly')],
});
const find = (plan: Plan) => products.find((p) => p.productId === productIdFor(plan));
return {
monthly: priceOf(find('monthly')) ?? DEFAULT_PRICES.monthly,
yearly: priceOf(find('yearly')) ?? DEFAULT_PRICES.yearly,
};
});
} catch {
return DEFAULT_PRICES;
}
}
/**
* Runs the native purchase flow, then lets the backend verify the receipt
* with the store. Returns the resulting subscription status.
+31 -3
View File
@@ -1,7 +1,14 @@
import { useEffect, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { getAccessToken, signIn, type Provider } from '../auth/session';
import { purchase, restore, type Plan } from '../purchases';
import {
DEFAULT_PRICES,
getPrices,
purchase,
restore,
type Plan,
type PlanPrices,
} from '../purchases';
import { colors, spacing, type } from '../theme';
interface Props {
@@ -9,17 +16,26 @@ interface Props {
onDismiss: () => void;
}
const PREMIUM_INCLUDES = [
'Unlimited conversations',
'Unlimited analyses',
'Personalised guidance',
'Future feature updates',
];
/**
* Shown when the backend has a complete analysis ready. Guests sign in
* first (purchases must attach to an account), then choose a plan.
*/
export function PaywallScreen({ onSubscribed, onDismiss }: Props) {
const [signedIn, setSignedIn] = useState<boolean | null>(null);
const [prices, setPrices] = useState<PlanPrices>(DEFAULT_PRICES);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
getAccessToken().then((token) => setSignedIn(token !== null));
getPrices().then(setPrices);
}, []);
async function handleSignIn(provider: Provider) {
@@ -59,9 +75,16 @@ export function PaywallScreen({ onSubscribed, onDismiss }: Props) {
<View style={styles.top}>
<Text style={styles.title}>Continue your journey</Text>
<Text style={styles.subtitle}>
Your analysis and recommended steps are ready. Premium unlocks them and lets the
conversation continue without limits.
Your full analysis and personalised action plan are ready. Premium is the natural next
step of the conversation you have already started.
</Text>
<View style={styles.includes}>
{PREMIUM_INCLUDES.map((item) => (
<Text key={item} style={styles.includesItem}>
{item}
</Text>
))}
</View>
</View>
<View style={styles.actions}>
{signedIn === false ? (
@@ -92,9 +115,11 @@ export function PaywallScreen({ onSubscribed, onDismiss }: Props) {
<>
<Pressable style={styles.primaryButton} onPress={() => buy('monthly')} disabled={busy}>
<Text style={styles.primaryButtonText}>Monthly</Text>
<Text style={styles.priceText}>{prices.monthly}</Text>
</Pressable>
<Pressable style={styles.primaryButton} onPress={() => buy('yearly')} disabled={busy}>
<Text style={styles.primaryButtonText}>Yearly</Text>
<Text style={styles.priceText}>{prices.yearly}</Text>
</Pressable>
<Pressable style={styles.textButton} onPress={() => run(restore)} disabled={busy}>
<Text style={styles.textButtonText}>Restore Purchase</Text>
@@ -133,6 +158,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
primaryButtonText: { ...type.heading, color: colors.accentText },
priceText: { ...type.caption, color: colors.accentText, marginTop: 2 },
includes: { marginTop: spacing.l, gap: spacing.xs },
includesItem: { ...type.caption, textAlign: 'center' },
secondaryButton: {
backgroundColor: colors.surface,
borderColor: colors.border,
+30 -14
View File
@@ -83,33 +83,49 @@ Principles:
const FREE_INSTRUCTIONS = `# Conversation mode: free tier (discovery)
The goal is not to interrupt the conversation. The goal is to build trust.
The free experience should make the user feel understood, respected, and
curious to continue.
Work in discovery mode. In every reply you should:
- ask relevant follow-up questions (one at a time),
- identify and name patterns you notice,
- show genuine understanding of the user's situation,
- build toward a complete analysis.
- help the user reach a meaningful insight of their own.
Do not yet present the full analysis, the recommended strategy, or concrete
exercises.
Never manufacture suspense. Create genuine curiosity by helping the user
reach a meaningful insight, then offer a deeper level of analysis in
Premium.
Set "analysis_ready" to true ONLY when all of the following are genuinely met:
- the user has described their problem,
- you have enough information to give a concrete, personal recommendation,
- a specific action plan is ready to present.
Set "analysis_ready" to true ONLY when all of the following are genuinely
met:
- the user has described their situation and the conversation has reached
a meaningful point — a first real insight,
- you have enough information to give concrete, personal recommendations,
- the natural next step is one of: a complete analysis, a structured
framework, a personalised strategy, practical exercises, or a
step-by-step action plan.
When analysis_ready is true, the reply must be a calm, natural transition —
not an interruption mid-answer. Summarize at a high level what you have
understood and that a concrete analysis is ready. Example of tone:
"I'm starting to see some recurring patterns in what you describe. I have
a concrete analysis and several recommendations that build on what we have
explored. Unlock Premium to continue."
Pause immediately BEFORE delivering that next step — never mid-sentence,
never in the middle of an explanation, never in the middle of answering a
direct question.
When analysis_ready is true, the reply must be a calm, natural transition.
Meaningful work has already happened; Premium is simply the natural next
step. Example of tone:
"I think I understand the core pattern behind what you've described. There
are a few recurring themes that stand out, and I have a structured way of
working through them with you. Unlock Premium to continue with the full
analysis and your personalised action plan."
Never:
- manufacture urgency or emotional pressure to drive a purchase,
- claim readiness or insight you do not have,
- stop in the middle of answering a direct question,
- mention Premium in any other situation.
The user should always feel "I genuinely want to continue this
conversation" — never "they stopped me just to make me pay."
Otherwise, set analysis_ready to false.`;
const PREMIUM_INSTRUCTIONS = `# Conversation mode: premium