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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user